featherbit/plugins/native/
attach_consumer_label.rs1use async_trait::async_trait;
12use std::collections::HashMap;
13
14use crate::context::Context;
15use crate::plugins::{Plugin, PluginOutput, PluginResult};
16use crate::vars::template::Template;
17
18pub struct AttachConsumerLabelPlugin {
25 header_prefix: Template,
28}
29
30impl AttachConsumerLabelPlugin {
31 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
45 let header_prefix = config
46 .get("header_prefix")
47 .and_then(|v| v.as_str())
48 .unwrap_or("X-Consumer-");
49 let header_prefix = Template::parse(header_prefix).0;
52
53 Ok(Self { header_prefix })
54 }
55}
56
57#[async_trait]
58impl Plugin for AttachConsumerLabelPlugin {
59 fn plugin_type(&self) -> &str {
60 "attach-consumer-label"
61 }
62
63 async fn execute(&self, mut ctx: Context) -> PluginResult {
64 if let Some(labels) = ctx
66 .message
67 .get("consumer.labels")
68 .and_then(|v| v.as_object())
69 .cloned()
70 {
71 let prefix = self.header_prefix.render(&ctx).into_owned();
72 for (key, value) in labels {
73 if let Some(value) = value.as_str() {
74 let header = format!("{}{}", prefix, key).to_lowercase();
75 ctx.request.headers.insert(header, vec![value.to_string()]);
76 }
77 }
78 }
79
80 Ok(PluginOutput::success(ctx))
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
88 use bytes::Bytes;
89
90 fn ctx(labels: Option<serde_json::Value>) -> Context {
91 let mut message = HashMap::new();
92 if let Some(labels) = labels {
93 message.insert("consumer.name".to_string(), serde_json::json!("alice"));
94 message.insert("consumer.labels".to_string(), labels);
95 }
96 Context {
97 request: GatewayRequest {
98 method: "GET".to_string(),
99 path: "/".to_string(),
100 host: "h".to_string(),
101 scheme: "http".to_string(),
102 headers: HashMap::new(),
103 query_params: HashMap::new(),
104 body: Bytes::new(),
105 remote_addr: "1.2.3.4:5".to_string(),
106 protocol: Protocol::Http1,
107 },
108 response: GatewayResponse {
109 status_code: 0,
110 headers: HashMap::new(),
111 body: Bytes::new(),
112 stream: None,
113 },
114 message,
115 errors: Vec::new(),
116 }
117 }
118
119 fn plugin(config: serde_json::Value) -> AttachConsumerLabelPlugin {
120 let map: HashMap<String, serde_json::Value> = serde_json::from_value(config).unwrap();
121 AttachConsumerLabelPlugin::from_config(&map).unwrap()
122 }
123
124 #[tokio::test]
125 async fn test_attaches_labels_with_default_prefix() {
126 let p = plugin(serde_json::json!({}));
127 let out = p
128 .execute(ctx(Some(
129 serde_json::json!({ "tier": "gold", "region": "eu" }),
130 )))
131 .await
132 .unwrap();
133 let headers = &out.context.request.headers;
134 assert_eq!(
135 headers.get("x-consumer-tier"),
136 Some(&vec!["gold".to_string()])
137 );
138 assert_eq!(
139 headers.get("x-consumer-region"),
140 Some(&vec!["eu".to_string()])
141 );
142 }
143
144 #[tokio::test]
145 async fn test_custom_prefix() {
146 let p = plugin(serde_json::json!({ "header_prefix": "X-Label-" }));
147 let out = p
148 .execute(ctx(Some(serde_json::json!({ "tier": "gold" }))))
149 .await
150 .unwrap();
151 assert_eq!(
152 out.context.request.headers.get("x-label-tier"),
153 Some(&vec!["gold".to_string()])
154 );
155 }
156
157 #[tokio::test]
158 async fn test_header_prefix_renders_template() {
159 let p = plugin(serde_json::json!({ "header_prefix": "X-{{request.headers.x-realm}}-" }));
160 let mut c = ctx(Some(serde_json::json!({ "tier": "gold" })));
161 c.request
162 .headers
163 .insert("x-realm".to_string(), vec!["Eu".to_string()]);
164 let out = p.execute(c).await.unwrap();
165 assert_eq!(
166 out.context.request.headers.get("x-eu-tier"),
167 Some(&vec!["gold".to_string()])
168 );
169 }
170
171 #[tokio::test]
172 async fn test_no_consumer_is_passthrough() {
173 let p = plugin(serde_json::json!({}));
174 let out = p.execute(ctx(None)).await.unwrap();
175 assert!(out
177 .context
178 .request
179 .headers
180 .keys()
181 .all(|k| !k.starts_with("x-consumer-")));
182 }
183}