use core::fmt;
use http::{HeaderMap, HeaderName, HeaderValue};
use crate::types::{PartyRef, Url};
pub const AUTHORIZATION: HeaderName = HeaderName::from_static("authorization");
pub const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
pub const X_CORRELATION_ID: HeaderName = HeaderName::from_static("x-correlation-id");
pub const OCPI_TO_PARTY_ID: HeaderName = HeaderName::from_static("ocpi-to-party-id");
pub const OCPI_TO_COUNTRY_CODE: HeaderName = HeaderName::from_static("ocpi-to-country-code");
pub const OCPI_FROM_PARTY_ID: HeaderName = HeaderName::from_static("ocpi-from-party-id");
pub const OCPI_FROM_COUNTRY_CODE: HeaderName = HeaderName::from_static("ocpi-from-country-code");
pub const LINK: HeaderName = HeaderName::from_static("link");
pub const X_TOTAL_COUNT: HeaderName = HeaderName::from_static("x-total-count");
pub const X_LIMIT: HeaderName = HeaderName::from_static("x-limit");
pub const LOCATION: HeaderName = HeaderName::from_static("location");
pub const APPLICATION_JSON: &str = "application/json";
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RequestIds {
pub request_id: String,
pub correlation_id: String,
}
impl RequestIds {
#[must_use]
pub fn generate() -> Self {
Self {
request_id: uuid::Uuid::new_v4().to_string(),
correlation_id: uuid::Uuid::new_v4().to_string(),
}
}
#[must_use]
pub fn forwarded(&self) -> Self {
Self { request_id: uuid::Uuid::new_v4().to_string(), correlation_id: self.correlation_id.clone() }
}
#[must_use]
pub fn from_headers_or_generate(headers: &HeaderMap) -> Self {
Self {
request_id: header_str(headers, &X_REQUEST_ID)
.map_or_else(|| uuid::Uuid::new_v4().to_string(), ToOwned::to_owned),
correlation_id: header_str(headers, &X_CORRELATION_ID)
.map_or_else(|| uuid::Uuid::new_v4().to_string(), ToOwned::to_owned),
}
}
#[must_use]
pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
Some(Self {
request_id: header_str(headers, &X_REQUEST_ID)?.to_owned(),
correlation_id: header_str(headers, &X_CORRELATION_ID)?.to_owned(),
})
}
pub fn write_to(&self, headers: &mut HeaderMap) {
if let Ok(v) = HeaderValue::from_str(&self.request_id) {
headers.insert(X_REQUEST_ID, v);
}
if let Ok(v) = HeaderValue::from_str(&self.correlation_id) {
headers.insert(X_CORRELATION_ID, v);
}
}
}
impl fmt::Display for RequestIds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "request={} correlation={}", self.request_id, self.correlation_id)
}
}
#[must_use]
pub fn header_str<'a>(headers: &'a HeaderMap, name: &HeaderName) -> Option<&'a str> {
headers.get(name)?.to_str().ok()
}
#[must_use]
pub fn header_u64(headers: &HeaderMap, name: &HeaderName) -> Option<u64> {
header_str(headers, name)?.trim().parse().ok()
}
#[must_use]
pub fn header_party(
headers: &HeaderMap,
country_header: &HeaderName,
party_header: &HeaderName,
) -> Option<PartyRef> {
let country = header_str(headers, country_header)?;
let party = header_str(headers, party_header)?;
PartyRef::new(country, party).ok()
}
#[must_use]
pub fn link_next(url: &Url) -> String {
format!("<{}>; rel=\"next\"", url.as_str())
}
#[must_use]
pub fn parse_link_next(value: &str) -> Option<Url> {
for entry in split_link_entries(value) {
let mut parts = entry.split(';');
let target = parts.next()?.trim();
let url = target.strip_prefix('<')?.strip_suffix('>')?;
for param in parts {
let param = param.trim();
let Some((key, val)) = param.split_once('=') else { continue };
if key.trim().eq_ignore_ascii_case("rel") {
let val = val.trim().trim_matches('"');
if val.eq_ignore_ascii_case("next") {
return Some(Url::new_lenient(url));
}
}
}
}
None
}
fn split_link_entries(value: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, ch) in value.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
out.push(value[start..i].trim());
start = i + 1;
}
_ => {}
}
}
out.push(value[start..].trim());
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_hub_renews_the_request_id_and_keeps_the_correlation_id() {
let incoming = RequestIds::generate();
let forwarded = incoming.forwarded();
assert_ne!(incoming.request_id, forwarded.request_id);
assert_eq!(incoming.correlation_id, forwarded.correlation_id);
}
#[test]
fn headers_round_trip_through_a_header_map() {
let ids = RequestIds::generate();
let mut headers = HeaderMap::new();
ids.write_to(&mut headers);
assert_eq!(RequestIds::from_headers(&headers), Some(ids));
}
#[test]
fn missing_ids_are_generated_rather_than_refused() {
let ids = RequestIds::from_headers_or_generate(&HeaderMap::new());
assert!(!ids.request_id.is_empty() && !ids.correlation_id.is_empty());
assert_ne!(ids.request_id, ids.correlation_id);
}
#[test]
fn link_headers_round_trip_and_tolerate_the_unquoted_form() {
let url = Url::new("https://www.server.com/ocpi/cpo/2.3.0/cdrs/?offset=150&limit=50").unwrap();
let header = link_next(&url);
assert_eq!(
header,
r#"<https://www.server.com/ocpi/cpo/2.3.0/cdrs/?offset=150&limit=50>; rel="next""#
);
assert_eq!(parse_link_next(&header), Some(url.clone()));
assert_eq!(
parse_link_next(r"<https://e.com/a>; rel=next"),
Some(Url::new_lenient("https://e.com/a"))
);
}
#[test]
fn link_parsing_picks_next_out_of_several_entries() {
let header = r#"<https://e.com/a?x=1,2>; rel="prev", <https://e.com/b>; rel="next""#;
assert_eq!(parse_link_next(header), Some(Url::new_lenient("https://e.com/b")));
assert_eq!(parse_link_next("garbage"), None);
}
#[test]
fn party_headers_need_both_halves() {
let mut headers = HeaderMap::new();
headers.insert(OCPI_TO_COUNTRY_CODE, HeaderValue::from_static("NL"));
assert_eq!(header_party(&headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID), None);
headers.insert(OCPI_TO_PARTY_ID, HeaderValue::from_static("TNM"));
assert_eq!(
header_party(&headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID),
Some(PartyRef::new("NL", "TNM").unwrap())
);
}
}