ocpi_kit/transport/
headers.rs1use core::fmt;
9
10use http::{HeaderMap, HeaderName, HeaderValue};
11
12use crate::types::{PartyRef, Url};
13
14pub const AUTHORIZATION: HeaderName = HeaderName::from_static("authorization");
16
17pub const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
22
23pub const X_CORRELATION_ID: HeaderName = HeaderName::from_static("x-correlation-id");
28
29pub const OCPI_TO_PARTY_ID: HeaderName = HeaderName::from_static("ocpi-to-party-id");
31pub const OCPI_TO_COUNTRY_CODE: HeaderName = HeaderName::from_static("ocpi-to-country-code");
33pub const OCPI_FROM_PARTY_ID: HeaderName = HeaderName::from_static("ocpi-from-party-id");
35pub const OCPI_FROM_COUNTRY_CODE: HeaderName = HeaderName::from_static("ocpi-from-country-code");
37
38pub const LINK: HeaderName = HeaderName::from_static("link");
40pub const X_TOTAL_COUNT: HeaderName = HeaderName::from_static("x-total-count");
42pub const X_LIMIT: HeaderName = HeaderName::from_static("x-limit");
44
45pub const LOCATION: HeaderName = HeaderName::from_static("location");
47
48pub const APPLICATION_JSON: &str = "application/json";
53
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
78pub struct RequestIds {
79 pub request_id: String,
81 pub correlation_id: String,
83}
84
85impl RequestIds {
86 #[must_use]
90 pub fn generate() -> Self {
91 Self {
92 request_id: uuid::Uuid::new_v4().to_string(),
93 correlation_id: uuid::Uuid::new_v4().to_string(),
94 }
95 }
96
97 #[must_use]
100 pub fn forwarded(&self) -> Self {
101 Self { request_id: uuid::Uuid::new_v4().to_string(), correlation_id: self.correlation_id.clone() }
102 }
103
104 #[must_use]
109 pub fn from_headers_or_generate(headers: &HeaderMap) -> Self {
110 Self {
111 request_id: header_str(headers, &X_REQUEST_ID)
112 .map_or_else(|| uuid::Uuid::new_v4().to_string(), ToOwned::to_owned),
113 correlation_id: header_str(headers, &X_CORRELATION_ID)
114 .map_or_else(|| uuid::Uuid::new_v4().to_string(), ToOwned::to_owned),
115 }
116 }
117
118 #[must_use]
120 pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
121 Some(Self {
122 request_id: header_str(headers, &X_REQUEST_ID)?.to_owned(),
123 correlation_id: header_str(headers, &X_CORRELATION_ID)?.to_owned(),
124 })
125 }
126
127 pub fn write_to(&self, headers: &mut HeaderMap) {
129 if let Ok(v) = HeaderValue::from_str(&self.request_id) {
130 headers.insert(X_REQUEST_ID, v);
131 }
132 if let Ok(v) = HeaderValue::from_str(&self.correlation_id) {
133 headers.insert(X_CORRELATION_ID, v);
134 }
135 }
136}
137
138impl fmt::Display for RequestIds {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "request={} correlation={}", self.request_id, self.correlation_id)
141 }
142}
143
144#[must_use]
146pub fn header_str<'a>(headers: &'a HeaderMap, name: &HeaderName) -> Option<&'a str> {
147 headers.get(name)?.to_str().ok()
148}
149
150#[must_use]
152pub fn header_u64(headers: &HeaderMap, name: &HeaderName) -> Option<u64> {
153 header_str(headers, name)?.trim().parse().ok()
154}
155
156#[must_use]
161pub fn header_party(
162 headers: &HeaderMap,
163 country_header: &HeaderName,
164 party_header: &HeaderName,
165) -> Option<PartyRef> {
166 let country = header_str(headers, country_header)?;
167 let party = header_str(headers, party_header)?;
168 PartyRef::new(country, party).ok()
169}
170
171#[must_use]
175pub fn link_next(url: &Url) -> String {
176 format!("<{}>; rel=\"next\"", url.as_str())
177}
178
179#[must_use]
192pub fn parse_link_next(value: &str) -> Option<Url> {
193 for entry in split_link_entries(value) {
194 let mut parts = entry.split(';');
195 let target = parts.next()?.trim();
196 let url = target.strip_prefix('<')?.strip_suffix('>')?;
197 for param in parts {
198 let param = param.trim();
199 let Some((key, val)) = param.split_once('=') else { continue };
200 if key.trim().eq_ignore_ascii_case("rel") {
201 let val = val.trim().trim_matches('"');
202 if val.eq_ignore_ascii_case("next") {
203 return Some(Url::new_lenient(url));
204 }
205 }
206 }
207 }
208 None
209}
210
211fn split_link_entries(value: &str) -> Vec<&str> {
213 let mut out = Vec::new();
214 let mut depth = 0usize;
215 let mut start = 0usize;
216 for (i, ch) in value.char_indices() {
217 match ch {
218 '<' => depth += 1,
219 '>' => depth = depth.saturating_sub(1),
220 ',' if depth == 0 => {
221 out.push(value[start..i].trim());
222 start = i + 1;
223 }
224 _ => {}
225 }
226 }
227 out.push(value[start..].trim());
228 out
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn a_hub_renews_the_request_id_and_keeps_the_correlation_id() {
237 let incoming = RequestIds::generate();
238 let forwarded = incoming.forwarded();
239 assert_ne!(incoming.request_id, forwarded.request_id);
240 assert_eq!(incoming.correlation_id, forwarded.correlation_id);
241 }
242
243 #[test]
244 fn headers_round_trip_through_a_header_map() {
245 let ids = RequestIds::generate();
246 let mut headers = HeaderMap::new();
247 ids.write_to(&mut headers);
248 assert_eq!(RequestIds::from_headers(&headers), Some(ids));
249 }
250
251 #[test]
252 fn missing_ids_are_generated_rather_than_refused() {
253 let ids = RequestIds::from_headers_or_generate(&HeaderMap::new());
254 assert!(!ids.request_id.is_empty() && !ids.correlation_id.is_empty());
255 assert_ne!(ids.request_id, ids.correlation_id);
256 }
257
258 #[test]
259 fn link_headers_round_trip_and_tolerate_the_unquoted_form() {
260 let url = Url::new("https://www.server.com/ocpi/cpo/2.3.0/cdrs/?offset=150&limit=50").unwrap();
261 let header = link_next(&url);
262 assert_eq!(
263 header,
264 r#"<https://www.server.com/ocpi/cpo/2.3.0/cdrs/?offset=150&limit=50>; rel="next""#
265 );
266 assert_eq!(parse_link_next(&header), Some(url.clone()));
267 assert_eq!(
268 parse_link_next(r"<https://e.com/a>; rel=next"),
269 Some(Url::new_lenient("https://e.com/a"))
270 );
271 }
272
273 #[test]
274 fn link_parsing_picks_next_out_of_several_entries() {
275 let header = r#"<https://e.com/a?x=1,2>; rel="prev", <https://e.com/b>; rel="next""#;
276 assert_eq!(parse_link_next(header), Some(Url::new_lenient("https://e.com/b")));
277 assert_eq!(parse_link_next("garbage"), None);
278 }
279
280 #[test]
281 fn party_headers_need_both_halves() {
282 let mut headers = HeaderMap::new();
283 headers.insert(OCPI_TO_COUNTRY_CODE, HeaderValue::from_static("NL"));
284 assert_eq!(header_party(&headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID), None);
285 headers.insert(OCPI_TO_PARTY_ID, HeaderValue::from_static("TNM"));
286 assert_eq!(
287 header_party(&headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID),
288 Some(PartyRef::new("NL", "TNM").unwrap())
289 );
290 }
291}