Skip to main content

rskit_codec/
codec.rs

1use std::fmt;
2
3use rskit_errors::{AppError, AppResult};
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7
8/// Object-safe contract for a structured-text codec over the [`Value`] model.
9///
10/// Implementations encode and decode one on-disk/text representation (TOML,
11/// JSON, …) to and from [`serde_json::Value`]. The trait is intentionally
12/// object-safe so a codec can be held as `Arc<dyn Codec>` and selected at
13/// runtime; type-driven conversions live in the free functions [`encode`] and
14/// [`decode`].
15pub trait Codec: fmt::Debug + Send + Sync + 'static {
16    /// A short identifier for diagnostics (for example `"toml"`).
17    fn name(&self) -> &'static str;
18
19    /// Encode a value tree into this codec's textual representation.
20    ///
21    /// # Errors
22    ///
23    /// Returns a typed [`AppError`] (cause preserved) when `value` cannot be
24    /// represented in this format.
25    fn encode_value(&self, value: &Value) -> AppResult<String>;
26
27    /// Decode text into a value tree.
28    ///
29    /// # Errors
30    ///
31    /// Returns a typed [`AppError`] (cause preserved) when `contents` is
32    /// malformed for this format.
33    fn decode_value(&self, contents: &str) -> AppResult<Value>;
34}
35
36/// Encode any [`Serialize`] value using `codec`.
37///
38/// The value is first converted to the canonical [`Value`] tree, then encoded.
39///
40/// # Errors
41///
42/// Returns a typed [`AppError`] (cause preserved) when the value cannot be
43/// converted to the value model or encoded by `codec`.
44pub fn encode<T>(codec: &dyn Codec, value: &T) -> AppResult<String>
45where
46    T: Serialize + ?Sized,
47{
48    let value = serde_json::to_value(value).map_err(|err| {
49        AppError::invalid_input("codec", "failed to convert value into the value model")
50            .with_cause(err)
51    })?;
52    codec.encode_value(&value)
53}
54
55/// Decode text into any [`DeserializeOwned`] type using `codec`.
56///
57/// The text is decoded to the canonical [`Value`] tree, then deserialized into
58/// `T`. This honors `#[serde(deny_unknown_fields)]` on `T`.
59///
60/// # Errors
61///
62/// Returns a typed [`AppError`] (cause preserved) when `contents` is malformed
63/// or does not match `T` (including unknown fields under
64/// `deny_unknown_fields`).
65pub fn decode<T>(codec: &dyn Codec, contents: &str) -> AppResult<T>
66where
67    T: DeserializeOwned,
68{
69    let value = codec.decode_value(contents)?;
70    T::deserialize(value).map_err(|err| {
71        AppError::invalid_input("codec", "failed to deserialize value").with_cause(err)
72    })
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::JsonCodec;
79    use serde::Deserialize;
80
81    #[derive(Debug, PartialEq, Serialize, Deserialize)]
82    struct Settings {
83        name: String,
84        retries: u32,
85    }
86
87    #[test]
88    fn encode_then_decode_round_trips_typed_value() {
89        let settings = Settings {
90            name: "svc".to_owned(),
91            retries: 3,
92        };
93
94        let encoded = encode(&JsonCodec, &settings).unwrap();
95        let decoded: Settings = decode(&JsonCodec, &encoded).unwrap();
96
97        assert_eq!(decoded, settings);
98    }
99
100    #[test]
101    fn encode_reports_unrepresentable_values() {
102        use std::collections::HashMap;
103
104        // A map keyed by a non-string type cannot be represented in the JSON
105        // value model, so conversion fails before the codec is invoked.
106        let map: HashMap<(i32, i32), i32> = HashMap::from([((1, 2), 3)]);
107        let err = encode(&JsonCodec, &map).unwrap_err();
108
109        assert!(err.to_string().contains("value model"));
110    }
111
112    #[test]
113    fn decode_reports_type_mismatches() {
114        // Valid JSON, but the shape does not match the target type.
115        let err = decode::<Settings>(&JsonCodec, "[1, 2, 3]").unwrap_err();
116
117        assert!(err.to_string().contains("deserialize"));
118    }
119
120    #[test]
121    fn decode_propagates_codec_parse_errors() {
122        let err = decode::<Settings>(&JsonCodec, "{ not json").unwrap_err();
123
124        assert!(err.to_string().contains("parse"));
125    }
126}