Skip to main content

rskit_codec/
json.rs

1use rskit_errors::{AppError, AppResult};
2use serde_json::Value;
3
4use crate::codec::Codec;
5
6/// JSON output layout for [`JsonCodec`].
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[non_exhaustive]
9pub enum JsonStyle {
10    /// Human-readable, indented output.
11    #[default]
12    Pretty,
13    /// Minimal single-line output for machine streams (newline-delimited JSON,
14    /// length-framed payloads) where size and one-value-per-line matter.
15    Compact,
16}
17
18/// Built-in JSON codec.
19///
20/// Always available because [`serde_json`] backs the crate's value model.
21/// Defaults to pretty-printed output; [`JsonCodec::compact`] emits minimal
22/// single-line JSON for machine-readable streams.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct JsonCodec {
25    style: JsonStyle,
26}
27
28impl JsonCodec {
29    /// A pretty-printing JSON codec.
30    #[must_use]
31    pub const fn pretty() -> Self {
32        Self {
33            style: JsonStyle::Pretty,
34        }
35    }
36
37    /// A compact (single-line) JSON codec for machine streams.
38    #[must_use]
39    pub const fn compact() -> Self {
40        Self {
41            style: JsonStyle::Compact,
42        }
43    }
44}
45
46impl Codec for JsonCodec {
47    fn name(&self) -> &'static str {
48        "json"
49    }
50
51    fn encode_value(&self, value: &Value) -> AppResult<String> {
52        let result = match self.style {
53            JsonStyle::Pretty => serde_json::to_string_pretty(value),
54            JsonStyle::Compact => serde_json::to_string(value),
55        };
56        result.map_err(|err| {
57            AppError::invalid_input("codec", "failed to serialize value as JSON").with_cause(err)
58        })
59    }
60
61    fn decode_value(&self, contents: &str) -> AppResult<Value> {
62        serde_json::from_str(contents)
63            .map_err(|err| AppError::invalid_input("codec", "failed to parse JSON").with_cause(err))
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn round_trips_value() {
73        let codec = JsonCodec::default();
74        let value: Value = serde_json::json!({ "a": 1, "nested": { "b": true } });
75
76        let encoded = codec.encode_value(&value).unwrap();
77        let decoded = codec.decode_value(&encoded).unwrap();
78
79        assert_eq!(decoded, value);
80        assert_eq!(codec.name(), "json");
81    }
82
83    #[test]
84    fn pretty_is_multiline_compact_is_single_line() {
85        let value: Value = serde_json::json!({ "a": 1, "b": 2 });
86        assert!(
87            JsonCodec::pretty()
88                .encode_value(&value)
89                .unwrap()
90                .contains('\n')
91        );
92        assert!(
93            !JsonCodec::compact()
94                .encode_value(&value)
95                .unwrap()
96                .contains('\n')
97        );
98    }
99
100    #[test]
101    fn rejects_malformed_input() {
102        let err = JsonCodec::default().decode_value("{ not json").unwrap_err();
103        assert!(err.to_string().contains("parse"));
104    }
105}