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