Skip to main content

cratestack_client_rust/
codec.rs

1use cratestack_codec_cbor::CborCodec;
2#[cfg(feature = "codec-json")]
3use cratestack_codec_json::JsonCodec;
4use cratestack_core::{CoolCodec, CoolError};
5use serde::de::DeserializeOwned;
6
7pub(crate) const CBOR_SEQUENCE_CONTENT_TYPE: &str = "application/cbor-seq";
8
9pub trait HttpClientCodec: CoolCodec {
10    fn accept_header_value(&self) -> &'static str;
11
12    fn sequence_accept_header_value(&self) -> &'static str;
13
14    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CoolError>
15    where
16        T: DeserializeOwned;
17
18    fn decode_sequence_response<T>(
19        &self,
20        content_type: &str,
21        body: &[u8],
22    ) -> Result<Vec<T>, CoolError>
23    where
24        T: DeserializeOwned;
25}
26
27impl HttpClientCodec for CborCodec {
28    fn accept_header_value(&self) -> &'static str {
29        // With `codec-json` disabled the client cannot actually
30        // decode a JSON response — drop it from the Accept header
31        // so a server with content negotiation chooses CBOR (or
32        // fails on no acceptable representation) rather than
33        // sending JSON the client will then error on.
34        #[cfg(feature = "codec-json")]
35        {
36            "application/cbor, application/json"
37        }
38        #[cfg(not(feature = "codec-json"))]
39        {
40            "application/cbor"
41        }
42    }
43
44    fn sequence_accept_header_value(&self) -> &'static str {
45        #[cfg(feature = "codec-json")]
46        {
47            "application/cbor-seq, application/cbor, application/json"
48        }
49        #[cfg(not(feature = "codec-json"))]
50        {
51            "application/cbor-seq, application/cbor"
52        }
53    }
54
55    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CoolError>
56    where
57        T: DeserializeOwned,
58    {
59        if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
60            self.decode(body)
61        } else {
62            #[cfg(feature = "codec-json")]
63            if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
64                return JsonCodec.decode(body);
65            }
66            Err(CoolError::Codec(format!(
67                "unsupported response Content-Type {content_type}"
68            )))
69        }
70    }
71
72    fn decode_sequence_response<T>(
73        &self,
74        content_type: &str,
75        body: &[u8],
76    ) -> Result<Vec<T>, CoolError>
77    where
78        T: DeserializeOwned,
79    {
80        if media_type_matches(content_type, CBOR_SEQUENCE_CONTENT_TYPE) {
81            decode_cbor_sequence(body)
82        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
83            self.decode(body)
84        } else {
85            #[cfg(feature = "codec-json")]
86            if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
87                return JsonCodec.decode(body);
88            }
89            Err(CoolError::Codec(format!(
90                "unsupported response Content-Type {content_type}"
91            )))
92        }
93    }
94}
95
96#[cfg(feature = "codec-json")]
97impl HttpClientCodec for JsonCodec {
98    fn accept_header_value(&self) -> &'static str {
99        "application/json, application/cbor"
100    }
101
102    fn sequence_accept_header_value(&self) -> &'static str {
103        "application/cbor-seq, application/json, application/cbor"
104    }
105
106    fn decode_response<T>(&self, content_type: &str, body: &[u8]) -> Result<T, CoolError>
107    where
108        T: DeserializeOwned,
109    {
110        if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
111            self.decode(body)
112        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
113            CborCodec.decode(body)
114        } else {
115            Err(CoolError::Codec(format!(
116                "unsupported response Content-Type {content_type}"
117            )))
118        }
119    }
120
121    fn decode_sequence_response<T>(
122        &self,
123        content_type: &str,
124        body: &[u8],
125    ) -> Result<Vec<T>, CoolError>
126    where
127        T: DeserializeOwned,
128    {
129        if media_type_matches(content_type, CBOR_SEQUENCE_CONTENT_TYPE) {
130            decode_cbor_sequence(body)
131        } else if media_type_matches(content_type, JsonCodec::CONTENT_TYPE) {
132            self.decode(body)
133        } else if media_type_matches(content_type, CborCodec::CONTENT_TYPE) {
134            CborCodec.decode(body)
135        } else {
136            Err(CoolError::Codec(format!(
137                "unsupported response Content-Type {content_type}"
138            )))
139        }
140    }
141}
142
143pub(crate) fn media_type_matches(candidate: &str, expected: &str) -> bool {
144    candidate.split(';').next().unwrap_or(candidate).trim() == expected
145}
146
147pub(crate) fn decode_cbor_sequence<T>(bytes: &[u8]) -> Result<Vec<T>, CoolError>
148where
149    T: DeserializeOwned,
150{
151    let mut values = Vec::new();
152    let mut offset = 0usize;
153    while offset < bytes.len() {
154        let mut deserializer = minicbor_serde::Deserializer::new(&bytes[offset..]);
155        values.push(T::deserialize(&mut deserializer).map_err(|error| {
156            CoolError::Codec(format!("failed to decode CBOR sequence body: {error}"))
157        })?);
158        let consumed = deserializer.decoder().position();
159        if consumed == 0 {
160            return Err(CoolError::Codec(
161                "failed to decode CBOR sequence body: decoder made no progress".to_owned(),
162            ));
163        }
164        offset += consumed;
165    }
166    Ok(values)
167}