pdk_script/bindings/
attributes.rs

1// Copyright (c) 2025, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5use crate::constants::{
6    DESTINATION_ADDRESS, METHOD_HEADER, PATH_HEADER, REQUEST_PROTOCOL, REQUEST_SCHEME,
7    SCHEME_HEADER, SOURCE_ADDRESS, STATUS_CODE_HEADER,
8};
9use classy::event::HeadersAccessor;
10use classy::hl::{HeadersHandler, HttpClientResponse, PropertyAccessor};
11use pel::runtime::value::Value as PelValue;
12use std::collections::HashMap;
13use url::Url;
14
15/// Binding for the top-level `attributes` variable.
16pub trait AttributesBinding {
17    /// Returns the entire headers map.
18    fn extract_headers(&self) -> HashMap<String, String>;
19
20    /// Returns a single header.
21    fn extract_header(&self, key: &str) -> Option<String>;
22
23    /// Returns a map with the query parameters.
24    fn extract_query_params(&self) -> HashMap<String, String> {
25        self.extract_header(PATH_HEADER)
26            .and_then(fake_url)
27            .map(|url| {
28                url.query_pairs()
29                    .map(|(k, v)| (k.to_string(), v.to_string()))
30                    .collect()
31            })
32            .unwrap_or_default()
33    }
34
35    /// Returns the `attributes.method` value.
36    fn method(&self) -> Option<String> {
37        self.extract_header(METHOD_HEADER)
38    }
39
40    /// Returns the `attributes.path` value.
41    fn path(&self) -> Option<String> {
42        let mut url = self.extract_header(PATH_HEADER).and_then(fake_url)?;
43        url.set_query(None);
44        Some(url.path().to_string())
45    }
46
47    /// Returns the `uri` value.
48    fn uri(&self) -> Option<String> {
49        self.extract_header(PATH_HEADER)
50    }
51
52    /// Returns the `attributes.remoteAddress` value.
53    fn remote_address(&self) -> Option<String> {
54        None
55    }
56
57    /// Returns the `attributes.localAddress` value.
58    fn local_address(&self) -> Option<String> {
59        None
60    }
61
62    /// Returns the `attributes.queryString` value.
63    fn query_string(&self) -> Option<String> {
64        let path = self.extract_header(PATH_HEADER)?;
65        fake_url(path)?.query().map(str::to_string)
66    }
67
68    /// Returns the `attributes.scheme` value.
69    fn scheme(&self) -> Option<String> {
70        self.extract_header(SCHEME_HEADER)
71    }
72
73    /// Returns the `attributes.version` value.
74    fn version(&self) -> Option<String> {
75        None
76    }
77
78    /// Returns the `attributes.statusCode` value.
79    fn status_code(&self) -> Option<u32> {
80        self.extract_header(STATUS_CODE_HEADER)
81            .and_then(|value| value.parse::<u32>().ok())
82    }
83}
84
85impl AttributesBinding for HttpClientResponse {
86    fn extract_headers(&self) -> HashMap<String, String> {
87        self.headers().clone()
88    }
89
90    fn extract_header(&self, key: &str) -> Option<String> {
91        self.header(key).cloned()
92    }
93}
94
95/// A wrapper that implements the [`AttributesBinding`] trait for the handlers provided by the PDK.
96pub struct HandlerAttributesBinding<'a> {
97    handler: &'a dyn HeadersHandler,
98    properties: Option<&'a dyn PropertyAccessor>,
99}
100
101impl<'a> HandlerAttributesBinding<'a> {
102    /// Will create a binding that will resolve all properties of the attributes variable.
103    pub fn new(handler: &'a dyn HeadersHandler, properties: &'a dyn PropertyAccessor) -> Self {
104        Self {
105            handler,
106            properties: Some(properties),
107        }
108    }
109
110    /// Will create a binding that won't be able to resolve remote_address, local_address, scheme or version
111    pub fn partial(handler: &'a dyn HeadersHandler) -> Self {
112        Self {
113            handler,
114            properties: None,
115        }
116    }
117}
118
119impl AttributesBinding for HandlerAttributesBinding<'_> {
120    fn extract_headers(&self) -> HashMap<String, String> {
121        self.handler.headers().into_iter().collect()
122    }
123
124    fn extract_header(&self, key: &str) -> Option<String> {
125        self.handler.header(key)
126    }
127
128    fn remote_address(&self) -> Option<String> {
129        self.properties.and_then(remote_address)
130    }
131
132    fn local_address(&self) -> Option<String> {
133        self.properties.and_then(local_address)
134    }
135
136    fn scheme(&self) -> Option<String> {
137        self.properties.and_then(scheme)
138    }
139
140    fn version(&self) -> Option<String> {
141        self.properties.and_then(version)
142    }
143}
144
145/// A wrapper that implements the [`AttributesBinding`] trait for the different accessors for classy framework.
146#[doc(hidden)]
147pub struct AccessorAttributesBinding<'a> {
148    accessor: &'a dyn HeadersAccessor,
149    properties: Option<&'a dyn PropertyAccessor>,
150}
151
152impl<'a> AccessorAttributesBinding<'a> {
153    /// Will create a binding that will resolve all properties of the attributes variable.
154    pub fn new(handler: &'a dyn HeadersAccessor, properties: &'a dyn PropertyAccessor) -> Self {
155        Self {
156            accessor: handler,
157            properties: Some(properties),
158        }
159    }
160
161    /// Will create a binding that won't be able to resolve remote_address, local_address, scheme or version
162    pub fn partial(accessor: &'a dyn HeadersAccessor) -> Self {
163        Self {
164            accessor,
165            properties: None,
166        }
167    }
168}
169
170impl AttributesBinding for AccessorAttributesBinding<'_> {
171    fn extract_headers(&self) -> HashMap<String, String> {
172        self.accessor.headers().into_iter().collect()
173    }
174
175    fn extract_header(&self, key: &str) -> Option<String> {
176        self.accessor.header(key)
177    }
178
179    fn remote_address(&self) -> Option<String> {
180        self.properties.and_then(remote_address)
181    }
182
183    fn local_address(&self) -> Option<String> {
184        self.properties.and_then(local_address)
185    }
186
187    fn scheme(&self) -> Option<String> {
188        self.properties.and_then(scheme)
189    }
190
191    fn version(&self) -> Option<String> {
192        self.properties.and_then(version)
193    }
194}
195
196pub(crate) struct AttributesBindingAdapter<'a> {
197    delegate: &'a dyn AttributesBinding,
198}
199
200impl<'a> AttributesBindingAdapter<'a> {
201    pub fn new(delegate: &'a dyn AttributesBinding) -> Self {
202        Self { delegate }
203    }
204
205    pub fn extract_headers(&self) -> HashMap<String, PelValue> {
206        convert_maps(self.delegate.extract_headers())
207    }
208
209    pub fn extract_header(&self, key: &str) -> Option<PelValue> {
210        convert_string(self.delegate.extract_header(key))
211    }
212
213    pub fn extract_query_params(&self) -> HashMap<String, PelValue> {
214        convert_maps(self.delegate.extract_query_params())
215    }
216
217    pub fn method(&self) -> Option<PelValue> {
218        convert_string(self.delegate.method())
219    }
220
221    pub fn path(&self) -> Option<PelValue> {
222        convert_string(self.delegate.path())
223    }
224
225    pub fn uri(&self) -> Option<PelValue> {
226        convert_string(self.delegate.uri())
227    }
228
229    pub fn remote_address(&self) -> Option<PelValue> {
230        convert_string(self.delegate.remote_address())
231    }
232
233    pub fn local_address(&self) -> Option<PelValue> {
234        convert_string(self.delegate.local_address())
235    }
236
237    pub fn query_string(&self) -> Option<PelValue> {
238        convert_string(self.delegate.query_string())
239    }
240
241    pub fn scheme(&self) -> Option<PelValue> {
242        convert_string(self.delegate.scheme())
243    }
244
245    pub fn version(&self) -> Option<PelValue> {
246        convert_string(self.delegate.version())
247    }
248
249    pub fn status_code(&self) -> Option<PelValue> {
250        self.delegate
251            .status_code()
252            .map(|s| PelValue::number(s as f64))
253    }
254}
255
256fn fake_url(uri: String) -> Option<Url> {
257    Url::parse("http://fake_base").ok()?.join(uri.as_str()).ok()
258}
259fn convert_maps(map: HashMap<String, String>) -> HashMap<String, PelValue> {
260    map.into_iter()
261        .map(|(key, val)| (key, PelValue::string(val)))
262        .collect()
263}
264
265fn convert_string(val: Option<String>) -> Option<PelValue> {
266    val.map(PelValue::string)
267}
268
269fn remote_address(properties: &dyn PropertyAccessor) -> Option<String> {
270    properties
271        .read_property(SOURCE_ADDRESS)
272        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
273}
274
275fn local_address(properties: &dyn PropertyAccessor) -> Option<String> {
276    properties
277        .read_property(DESTINATION_ADDRESS)
278        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
279}
280
281fn scheme(properties: &dyn PropertyAccessor) -> Option<String> {
282    properties
283        .read_property(REQUEST_SCHEME)
284        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
285}
286
287fn version(properties: &dyn PropertyAccessor) -> Option<String> {
288    properties
289        .read_property(REQUEST_PROTOCOL)
290        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
291}