Skip to main content

reliakit_codec/
primitives.rs

1//! Optional integrations for `reliakit-primitives`.
2//!
3//! These implementations are available with the `primitives` feature. Decoding
4//! always uses public constructors or parsers so primitive invariants are
5//! preserved.
6
7#[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    // Float-backed primitives are intentionally not implemented in v0.1 because
177    // the codec format does not define float encoding.
178}
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}