cratestack_client_rust/
codec.rs1use 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 #[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}