omni_dev/transcript/
format.rs1use std::fmt;
8use std::str::FromStr;
9
10use crate::transcript::error::{Result, TranscriptError};
11use crate::transcript::source::Transcript;
12
13pub mod json;
14pub mod srt;
15pub mod txt;
16pub mod vtt;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum Format {
21 Srt,
23 Vtt,
26 Txt,
28 Json,
30}
31
32impl Format {
33 pub const ALL: &'static [Self] = &[Self::Srt, Self::Vtt, Self::Txt, Self::Json];
35
36 pub fn as_str(self) -> &'static str {
39 match self {
40 Self::Srt => "srt",
41 Self::Vtt => "vtt",
42 Self::Txt => "txt",
43 Self::Json => "json",
44 }
45 }
46
47 pub fn render(self, transcript: &Transcript) -> Result<String> {
54 match self {
55 Self::Srt => Ok(srt::render(&transcript.cues)),
56 Self::Vtt => Ok(vtt::render(&transcript.cues)),
57 Self::Txt => Ok(txt::render(&transcript.cues)),
58 Self::Json => json::render(transcript),
59 }
60 }
61}
62
63impl fmt::Display for Format {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(self.as_str())
66 }
67}
68
69impl FromStr for Format {
70 type Err = TranscriptError;
71
72 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
73 match s.to_ascii_lowercase().as_str() {
74 "srt" => Ok(Self::Srt),
75 "vtt" => Ok(Self::Vtt),
76 "txt" | "text" | "plain" => Ok(Self::Txt),
77 "json" => Ok(Self::Json),
78 other => Err(TranscriptError::ParseError(format!(
79 "unknown transcript format `{other}`; expected one of: srt, vtt, txt, json"
80 ))),
81 }
82 }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used)]
87mod tests {
88 use super::*;
89 use crate::transcript::cue::Cue;
90 use crate::transcript::source::TrackKind;
91
92 fn sample_transcript() -> Transcript {
93 Transcript {
94 source: "mock".into(),
95 locator_id: "abc".into(),
96 language: "en".into(),
97 kind: TrackKind::Manual,
98 cues: vec![Cue::new(0, 1000, "hello"), Cue::new(1000, 2500, "world")],
99 }
100 }
101
102 #[test]
103 fn as_str_round_trips_for_canonical_names() {
104 for &f in Format::ALL {
105 let parsed: Format = f.as_str().parse().unwrap();
106 assert_eq!(parsed, f);
107 }
108 }
109
110 #[test]
111 fn from_str_is_case_insensitive() {
112 assert_eq!(Format::from_str("SRT").unwrap(), Format::Srt);
113 assert_eq!(Format::from_str("Vtt").unwrap(), Format::Vtt);
114 assert_eq!(Format::from_str("JSON").unwrap(), Format::Json);
115 }
116
117 #[test]
118 fn from_str_accepts_txt_aliases() {
119 assert_eq!(Format::from_str("txt").unwrap(), Format::Txt);
120 assert_eq!(Format::from_str("text").unwrap(), Format::Txt);
121 assert_eq!(Format::from_str("plain").unwrap(), Format::Txt);
122 }
123
124 #[test]
125 fn from_str_unknown_errors_with_helpful_message() {
126 let err = Format::from_str("yaml").unwrap_err();
127 let msg = err.to_string();
128 assert!(msg.contains("yaml"));
129 assert!(msg.contains("srt"));
130 assert!(msg.contains("vtt"));
131 assert!(msg.contains("txt"));
132 assert!(msg.contains("json"));
133 }
134
135 #[test]
136 fn display_matches_as_str() {
137 for &f in Format::ALL {
138 assert_eq!(format!("{f}"), f.as_str());
139 }
140 }
141
142 #[test]
143 fn render_dispatches_to_srt() {
144 let out = Format::Srt.render(&sample_transcript()).unwrap();
145 assert!(out.contains("00:00:00,000 --> 00:00:01,000"));
146 assert!(out.contains("hello"));
147 }
148
149 #[test]
150 fn render_dispatches_to_vtt() {
151 let out = Format::Vtt.render(&sample_transcript()).unwrap();
152 assert!(out.starts_with("WEBVTT"));
153 assert!(out.contains("00:00:00.000 --> 00:00:01.000"));
154 }
155
156 #[test]
157 fn render_dispatches_to_txt() {
158 let out = Format::Txt.render(&sample_transcript()).unwrap();
159 assert_eq!(out, "hello\nworld\n");
160 }
161
162 #[test]
163 fn render_dispatches_to_json() {
164 let out = Format::Json.render(&sample_transcript()).unwrap();
165 let value: serde_json::Value = serde_json::from_str(&out).unwrap();
166 assert_eq!(value["language"], "en");
167 assert_eq!(value["cues"][0]["text"], "hello");
168 }
169
170 #[test]
171 fn all_constant_lists_each_variant_once() {
172 assert_eq!(Format::ALL.len(), 4);
173 let mut copy: Vec<Format> = Format::ALL.to_vec();
174 copy.sort_by_key(|f| f.as_str());
175 copy.dedup();
176 assert_eq!(copy.len(), 4);
177 }
178}