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