Skip to main content

omni_dev/transcript/format/
json.rs

1//! JSON renderer — pretty-printed serialisation of the full
2//! [`Transcript`] struct.
3
4use crate::transcript::error::{Result, TranscriptError};
5use crate::transcript::source::Transcript;
6
7/// Serialise `transcript` to a pretty-printed JSON string.
8///
9/// Errors only if `serde_json::to_string_pretty` fails (in practice
10/// unreachable for `Transcript`, since every field is JSON-serialisable,
11/// but the API returns `Result` to keep the contract uniform across
12/// formats).
13pub fn render(transcript: &Transcript) -> Result<String> {
14    serde_json::to_string_pretty(transcript)
15        .map(|s| {
16            let mut s = s;
17            s.push('\n');
18            s
19        })
20        .map_err(|e| TranscriptError::ParseError(format!("json serialisation failed: {e}")))
21}
22
23#[cfg(test)]
24#[allow(clippy::unwrap_used, clippy::expect_used)]
25mod tests {
26    use super::*;
27    use crate::transcript::cue::Cue;
28    use crate::transcript::source::TrackKind;
29
30    fn fixture(cues: Vec<Cue>) -> Transcript {
31        Transcript {
32            source: "mock".into(),
33            locator_id: "abc".into(),
34            language: "en".into(),
35            kind: TrackKind::Manual,
36            cues,
37        }
38    }
39
40    #[test]
41    fn empty_cues_serialises() {
42        let out = render(&fixture(vec![])).unwrap();
43        let value: serde_json::Value = serde_json::from_str(&out).unwrap();
44        assert_eq!(value["cues"], serde_json::json!([]));
45    }
46
47    #[test]
48    fn includes_top_level_metadata() {
49        let out = render(&fixture(vec![])).unwrap();
50        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
51        assert_eq!(v["source"], "mock");
52        assert_eq!(v["locator_id"], "abc");
53        assert_eq!(v["language"], "en");
54        assert_eq!(v["kind"], "manual");
55    }
56
57    #[test]
58    fn cues_have_timing_fields() {
59        let out = render(&fixture(vec![Cue::new(123, 456, "hi")])).unwrap();
60        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
61        let cue = &v["cues"][0];
62        assert_eq!(cue["start_ms"], 123);
63        assert_eq!(cue["end_ms"], 456);
64        assert_eq!(cue["text"], "hi");
65    }
66
67    #[test]
68    fn output_is_pretty_printed() {
69        let out = render(&fixture(vec![Cue::new(0, 1, "x")])).unwrap();
70        // Pretty output has indentation and one field per line.
71        assert!(out.contains("\n  "));
72    }
73
74    #[test]
75    fn output_ends_with_trailing_newline() {
76        let out = render(&fixture(vec![])).unwrap();
77        assert!(out.ends_with('\n'));
78    }
79
80    #[test]
81    fn round_trips_through_serde() {
82        let original = fixture(vec![
83            Cue::new(0, 1_000, "hello"),
84            Cue::new(1_000, 2_500, "world\nlines"),
85        ]);
86        let out = render(&original).unwrap();
87        let back: Transcript = serde_json::from_str(&out).unwrap();
88        assert_eq!(original, back);
89    }
90
91    #[test]
92    fn track_kind_translated_serialised_lowercase() {
93        let mut t = fixture(vec![]);
94        t.kind = TrackKind::Translated;
95        let out = render(&t).unwrap();
96        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
97        assert_eq!(v["kind"], "translated");
98    }
99
100    #[test]
101    fn special_characters_in_text_are_escaped() {
102        let t = fixture(vec![Cue::new(0, 1, "quote \" backslash \\ newline\n")]);
103        let out = render(&t).unwrap();
104        // The raw output contains escape sequences; deserialising restores
105        // the original bytes.
106        let back: Transcript = serde_json::from_str(&out).unwrap();
107        assert_eq!(back.cues[0].text, "quote \" backslash \\ newline\n");
108    }
109}