1#[cfg(feature = "primitives")]
8mod impls {
9 use crate::{CanonicalDecode, CanonicalEncode, CodecError, DecodeSource, EncodeSink};
10 use alloc::string::{String, ToString};
11 use reliakit_primitives::{
12 BoundedStr, ByteSize, Email, HexString, HttpUrl, HumanDuration, NonEmptyStr, NonEmptyVec,
13 Percent, Port, PositiveInt, SemVer, Slug, Uuid,
14 };
15
16 fn invalid_primitive() -> CodecError {
17 CodecError::invalid_value("decoded value failed reliakit-primitives validation")
18 }
19
20 macro_rules! impl_string_primitive {
21 ($ty:ty, $ctor:expr) => {
22 impl CanonicalEncode for $ty {
23 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
24 self.as_str().encode(writer)
25 }
26 }
27
28 impl CanonicalDecode for $ty {
29 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
30 let value = String::decode(reader)?;
31 $ctor(value).map_err(|_| invalid_primitive())
32 }
33 }
34 };
35 }
36
37 impl_string_primitive!(NonEmptyStr, NonEmptyStr::new);
38 impl_string_primitive!(Email, Email::new);
39 impl_string_primitive!(HttpUrl, HttpUrl::new);
40 impl_string_primitive!(Slug, Slug::new);
41 impl_string_primitive!(HexString, HexString::new);
42
43 impl<const MIN: usize, const MAX: usize> CanonicalEncode for BoundedStr<MIN, MAX> {
44 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
45 self.as_str().encode(writer)
46 }
47 }
48
49 impl<const MIN: usize, const MAX: usize> CanonicalDecode for BoundedStr<MIN, MAX> {
50 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
51 let value = String::decode(reader)?;
52 Self::new(value).map_err(|_| invalid_primitive())
53 }
54 }
55
56 impl CanonicalEncode for Port {
57 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
58 self.get().encode(writer)
59 }
60 }
61
62 impl CanonicalDecode for Port {
63 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
64 Self::new(u16::decode(reader)?).map_err(|_| invalid_primitive())
65 }
66 }
67
68 impl CanonicalEncode for Percent {
69 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
70 self.get().encode(writer)
71 }
72 }
73
74 impl CanonicalDecode for Percent {
75 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
76 Self::new(u8::decode(reader)?).map_err(|_| invalid_primitive())
77 }
78 }
79
80 impl CanonicalEncode for PositiveInt {
81 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
82 self.get().encode(writer)
83 }
84 }
85
86 impl CanonicalDecode for PositiveInt {
87 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
88 Self::new(u64::decode(reader)?).map_err(|_| invalid_primitive())
89 }
90 }
91
92 impl CanonicalEncode for ByteSize {
93 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
94 self.as_bytes().encode(writer)
95 }
96 }
97
98 impl CanonicalDecode for ByteSize {
99 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
100 Ok(Self::from_bytes(u64::decode(reader)?))
101 }
102 }
103
104 impl<T: CanonicalEncode> CanonicalEncode for NonEmptyVec<T> {
105 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
106 let len = u32::try_from(self.len()).map_err(|_| {
107 CodecError::length_overflow("non-empty vector length exceeds u32::MAX items")
108 })?;
109 len.encode(writer)?;
110 for item in self.iter() {
111 item.encode(writer)?;
112 }
113 Ok(())
114 }
115 }
116
117 impl<T: CanonicalDecode> CanonicalDecode for NonEmptyVec<T> {
118 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
119 Self::new(alloc::vec::Vec::<T>::decode(reader)?).map_err(|_| invalid_primitive())
120 }
121 }
122
123 impl CanonicalEncode for Uuid {
124 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
125 self.as_bytes().encode(writer)
126 }
127 }
128
129 impl CanonicalDecode for Uuid {
130 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
131 let bytes = <[u8; 16]>::decode(reader)?;
132 let text = format_uuid(bytes);
133 Self::parse(&text).map_err(|_| invalid_primitive())
134 }
135 }
136
137 impl CanonicalEncode for SemVer {
138 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
139 self.to_string().encode(writer)
140 }
141 }
142
143 impl CanonicalDecode for SemVer {
144 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
145 let value = String::decode(reader)?;
146 Self::parse(&value).map_err(|_| invalid_primitive())
147 }
148 }
149
150 impl CanonicalEncode for HumanDuration {
151 fn encode<W: EncodeSink + ?Sized>(&self, writer: &mut W) -> Result<(), CodecError> {
152 self.to_string().encode(writer)
153 }
154 }
155
156 impl CanonicalDecode for HumanDuration {
157 fn decode<R: DecodeSource + ?Sized>(reader: &mut R) -> Result<Self, CodecError> {
158 let value = String::decode(reader)?;
159 Self::parse(&value).map_err(|_| invalid_primitive())
160 }
161 }
162
163 fn format_uuid(bytes: [u8; 16]) -> String {
164 const HEX: &[u8; 16] = b"0123456789abcdef";
165 let mut out = String::with_capacity(36);
166 for (idx, byte) in bytes.iter().copied().enumerate() {
167 if matches!(idx, 4 | 6 | 8 | 10) {
168 out.push('-');
169 }
170 out.push(HEX[(byte >> 4) as usize] as char);
171 out.push(HEX[(byte & 0x0f) as usize] as char);
172 }
173 out
174 }
175
176 }
179
180#[cfg(all(test, feature = "primitives"))]
181mod tests {
182 use crate::{decode_from_slice_exact, encode_to_vec, CodecErrorKind};
183 use alloc::string::ToString;
184 use alloc::vec;
185 use reliakit_primitives::{
186 BoundedStr, ByteSize, Email, HexString, HttpUrl, HumanDuration, NonEmptyStr, NonEmptyVec,
187 Percent, Port, PositiveInt, SemVer, Slug, Uuid,
188 };
189
190 #[test]
191 fn string_primitives_roundtrip_through_validation() {
192 let name = NonEmptyStr::new("api").unwrap();
193 let encoded = encode_to_vec(&name).unwrap();
194 assert_eq!(
195 decode_from_slice_exact::<NonEmptyStr>(&encoded).unwrap(),
196 name
197 );
198
199 let email = Email::new("ops@example.com").unwrap();
200 let encoded = encode_to_vec(&email).unwrap();
201 assert_eq!(decode_from_slice_exact::<Email>(&encoded).unwrap(), email);
202
203 let url = HttpUrl::new("https://example.com/health").unwrap();
204 let encoded = encode_to_vec(&url).unwrap();
205 assert_eq!(decode_from_slice_exact::<HttpUrl>(&encoded).unwrap(), url);
206
207 let slug = Slug::new("service-api").unwrap();
208 let encoded = encode_to_vec(&slug).unwrap();
209 assert_eq!(decode_from_slice_exact::<Slug>(&encoded).unwrap(), slug);
210
211 let hex = HexString::new("0xdeadBEEF").unwrap();
212 let encoded = encode_to_vec(&hex).unwrap();
213 assert_eq!(decode_from_slice_exact::<HexString>(&encoded).unwrap(), hex);
214
215 let bounded = BoundedStr::<3, 8>::new("service").unwrap();
216 let encoded = encode_to_vec(&bounded).unwrap();
217 assert_eq!(
218 decode_from_slice_exact::<BoundedStr<3, 8>>(&encoded).unwrap(),
219 bounded
220 );
221 }
222
223 #[test]
224 fn numeric_primitives_reject_invalid_decoded_values() {
225 assert_eq!(
226 decode_from_slice_exact::<Port>(&0u16.to_le_bytes())
227 .unwrap_err()
228 .kind(),
229 CodecErrorKind::InvalidValue
230 );
231 assert_eq!(
232 decode_from_slice_exact::<Percent>(&[101])
233 .unwrap_err()
234 .kind(),
235 CodecErrorKind::InvalidValue
236 );
237 }
238
239 #[test]
240 fn numeric_primitives_roundtrip() {
241 let port = Port::new(8080).unwrap();
242 assert_eq!(encode_to_vec(&port).unwrap(), 8080u16.to_le_bytes());
243 assert_eq!(
244 decode_from_slice_exact::<Port>(&8080u16.to_le_bytes()).unwrap(),
245 port
246 );
247
248 let percent = Percent::new(80).unwrap();
249 assert_eq!(encode_to_vec(&percent).unwrap(), [80]);
250 assert_eq!(decode_from_slice_exact::<Percent>(&[80]).unwrap(), percent);
251
252 let positive = PositiveInt::new(9).unwrap();
253 assert_eq!(encode_to_vec(&positive).unwrap(), 9u64.to_le_bytes());
254 assert_eq!(
255 decode_from_slice_exact::<PositiveInt>(&9u64.to_le_bytes()).unwrap(),
256 positive
257 );
258
259 let size = ByteSize::from_mb(2);
260 assert_eq!(
261 encode_to_vec(&size).unwrap(),
262 (2 * 1024 * 1024u64).to_le_bytes()
263 );
264 assert_eq!(
265 decode_from_slice_exact::<ByteSize>(&(2 * 1024 * 1024u64).to_le_bytes()).unwrap(),
266 size
267 );
268 }
269
270 #[test]
271 fn primitive_validation_failures_are_decode_errors() {
272 let empty_string = encode_to_vec("").unwrap();
273 assert_eq!(
274 decode_from_slice_exact::<NonEmptyStr>(&empty_string)
275 .unwrap_err()
276 .kind(),
277 CodecErrorKind::InvalidValue
278 );
279 assert_eq!(
280 decode_from_slice_exact::<Email>(&empty_string)
281 .unwrap_err()
282 .kind(),
283 CodecErrorKind::InvalidValue
284 );
285 assert_eq!(
286 decode_from_slice_exact::<HttpUrl>(&empty_string)
287 .unwrap_err()
288 .kind(),
289 CodecErrorKind::InvalidValue
290 );
291 assert_eq!(
292 decode_from_slice_exact::<Slug>(&empty_string)
293 .unwrap_err()
294 .kind(),
295 CodecErrorKind::InvalidValue
296 );
297 assert_eq!(
298 decode_from_slice_exact::<HexString>(&empty_string)
299 .unwrap_err()
300 .kind(),
301 CodecErrorKind::InvalidValue
302 );
303 assert_eq!(
304 decode_from_slice_exact::<BoundedStr<3, 8>>(&empty_string)
305 .unwrap_err()
306 .kind(),
307 CodecErrorKind::InvalidValue
308 );
309 assert_eq!(
310 decode_from_slice_exact::<PositiveInt>(&0u64.to_le_bytes())
311 .unwrap_err()
312 .kind(),
313 CodecErrorKind::InvalidValue
314 );
315 }
316
317 #[test]
318 fn uuid_encodes_raw_bytes_canonically() {
319 let uuid = Uuid::parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
320 let encoded = encode_to_vec(&uuid).unwrap();
321 assert_eq!(encoded, uuid.as_bytes());
322 assert_eq!(decode_from_slice_exact::<Uuid>(&encoded).unwrap(), uuid);
323 }
324
325 #[test]
326 fn structured_primitives_roundtrip_through_text_forms() {
327 let version = SemVer::parse("1.2.3-beta.1+build.5").unwrap();
328 let encoded = encode_to_vec(&version).unwrap();
329 assert_eq!(encoded, encode_to_vec(&version.to_string()).unwrap());
330 assert_eq!(
331 decode_from_slice_exact::<SemVer>(&encoded).unwrap(),
332 version
333 );
334
335 let duration = HumanDuration::parse("1h30m45s").unwrap();
336 let encoded = encode_to_vec(&duration).unwrap();
337 assert_eq!(encoded, encode_to_vec(&duration.to_string()).unwrap());
338 assert_eq!(
339 decode_from_slice_exact::<HumanDuration>(&encoded).unwrap(),
340 duration
341 );
342
343 let invalid = encode_to_vec("not-semver").unwrap();
344 assert_eq!(
345 decode_from_slice_exact::<SemVer>(&invalid)
346 .unwrap_err()
347 .kind(),
348 CodecErrorKind::InvalidValue
349 );
350 assert_eq!(
351 decode_from_slice_exact::<HumanDuration>(&invalid)
352 .unwrap_err()
353 .kind(),
354 CodecErrorKind::InvalidValue
355 );
356 }
357
358 #[test]
359 fn non_empty_vec_decode_validates_non_empty() {
360 let values = NonEmptyVec::new(vec![1u8, 2, 3]).unwrap();
361 let encoded = encode_to_vec(&values).unwrap();
362 assert_eq!(
363 decode_from_slice_exact::<NonEmptyVec<u8>>(&encoded).unwrap(),
364 values
365 );
366
367 assert_eq!(
368 decode_from_slice_exact::<NonEmptyVec<u8>>(&0u32.to_le_bytes())
369 .unwrap_err()
370 .kind(),
371 CodecErrorKind::InvalidValue
372 );
373 }
374}