1use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum CastRowKind {
22 Output,
25 Input,
29}
30
31impl CastRowKind {
32 #[must_use]
34 pub fn as_str(self) -> &'static str {
35 match self {
36 CastRowKind::Output => "o",
37 CastRowKind::Input => "i",
38 }
39 }
40}
41
42#[derive(Clone, Debug, PartialEq)]
44pub struct CastRow {
45 pub t: f64,
47 pub kind: CastRowKind,
48 pub payload: String,
52}
53
54#[derive(Debug, Error)]
55pub enum CastParseError {
56 #[error("row is not valid json: {0}")]
57 InvalidJson(String),
58 #[error("row is not a JSON array")]
59 NotAnArray,
60 #[error("row has {0} elements, expected at least 3")]
61 TooFewElements(usize),
62 #[error("row[0] (time) is not a number")]
63 BadTime,
64 #[error("row[1] (kind) is not a string")]
65 BadKindShape,
66 #[error("row[1] kind `{0}` is not 'o' or 'i'")]
67 UnknownKind(String),
68 #[error("row[2] (payload) is not a string")]
69 BadPayload,
70 #[error("row is the asciinema header (JSON object, not array)")]
71 HeaderRow,
72}
73
74impl CastRow {
75 pub fn parse(line: &str) -> Result<Self, CastParseError> {
79 let trimmed = line.trim_start();
80 if trimmed.starts_with('{') {
81 return Err(CastParseError::HeaderRow);
82 }
83 let v: serde_json::Value = serde_json::from_str(line)
84 .map_err(|e| CastParseError::InvalidJson(e.to_string()))?;
85 let arr = v.as_array().ok_or(CastParseError::NotAnArray)?;
86 if arr.len() < 3 {
87 return Err(CastParseError::TooFewElements(arr.len()));
88 }
89 let t = arr[0].as_f64().ok_or(CastParseError::BadTime)?;
90 let kind_str = arr[1].as_str().ok_or(CastParseError::BadKindShape)?;
91 let kind = match kind_str {
92 "o" => CastRowKind::Output,
93 "i" => CastRowKind::Input,
94 other => return Err(CastParseError::UnknownKind(other.into())),
95 };
96 let payload = arr[2]
97 .as_str()
98 .ok_or(CastParseError::BadPayload)?
99 .to_string();
100 Ok(CastRow { t, kind, payload })
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn parses_output_row() {
110 let r = CastRow::parse(r#"[0.5,"o","hi"]"#).unwrap();
111 assert_eq!(r.t, 0.5);
112 assert_eq!(r.kind, CastRowKind::Output);
113 assert_eq!(r.payload, "hi");
114 }
115
116 #[test]
117 fn parses_input_row() {
118 let r = CastRow::parse(r#"[1.0,"i","x"]"#).unwrap();
119 assert_eq!(r.kind, CastRowKind::Input);
120 }
121
122 #[test]
123 fn header_row_returns_specific_error() {
124 let err = CastRow::parse(r#"{"version":2}"#).unwrap_err();
125 assert!(matches!(err, CastParseError::HeaderRow));
126 }
127
128 #[test]
129 fn malformed_json_returns_invalid_json() {
130 let err = CastRow::parse("not json").unwrap_err();
131 assert!(matches!(err, CastParseError::InvalidJson(_)));
132 }
133
134 #[test]
135 fn too_few_elements_returns_specific_error() {
136 let err = CastRow::parse(r#"[0.0, "o"]"#).unwrap_err();
137 assert!(matches!(err, CastParseError::TooFewElements(2)));
138 }
139
140 #[test]
141 fn unknown_kind_surfaces_value() {
142 let err = CastRow::parse(r#"[0.0,"r","payload"]"#).unwrap_err();
143 match err {
144 CastParseError::UnknownKind(s) => assert_eq!(s, "r"),
145 other => panic!("expected UnknownKind, got {other:?}"),
146 }
147 }
148
149 #[test]
150 fn bad_time_when_string_in_time_slot() {
151 let err = CastRow::parse(r#"["zero","o","x"]"#).unwrap_err();
152 assert!(matches!(err, CastParseError::BadTime));
153 }
154
155 #[test]
156 fn bad_payload_when_array_in_payload_slot() {
157 let err = CastRow::parse(r#"[0.0,"o",[1,2,3]]"#).unwrap_err();
158 assert!(matches!(err, CastParseError::BadPayload));
159 }
160
161 #[test]
162 fn kind_label_round_trip() {
163 assert_eq!(CastRowKind::Output.as_str(), "o");
164 assert_eq!(CastRowKind::Input.as_str(), "i");
165 }
166
167 #[test]
168 fn not_an_array_when_top_level_is_string() {
169 let err = CastRow::parse(r#""hi""#).unwrap_err();
170 assert!(matches!(err, CastParseError::NotAnArray));
171 }
172}