1use faucet_core::Value;
10use serde::Serialize;
11
12#[derive(Debug, Clone, PartialEq)]
18pub enum SingerMessage {
19 Record { stream: String, record: Value },
21 Schema {
23 stream: String,
24 schema: Value,
25 key_properties: Option<Vec<String>>,
26 bookmark_properties: Option<Vec<String>>,
27 },
28 State { value: Value },
31 Other { message_type: String },
34}
35
36pub fn parse_line(line: &str) -> Result<SingerMessage, String> {
43 let trimmed = line.trim();
44 if trimmed.is_empty() {
45 return Err("blank line".to_string());
46 }
47 let value: Value =
48 faucet_core::fidelity::from_str(trimmed).map_err(|e| format!("not valid JSON: {e}"))?;
49 let obj = value
50 .as_object()
51 .ok_or_else(|| "message is not a JSON object".to_string())?;
52 let msg_type = obj
53 .get("type")
54 .and_then(Value::as_str)
55 .ok_or_else(|| "missing or non-string `type` field".to_string())?;
56
57 match msg_type {
58 "RECORD" => {
59 let stream = obj
60 .get("stream")
61 .and_then(Value::as_str)
62 .ok_or_else(|| "RECORD missing `stream`".to_string())?
63 .to_string();
64 let record = obj
65 .get("record")
66 .ok_or_else(|| "RECORD missing `record`".to_string())?
67 .clone();
68 if !record.is_object() {
69 return Err("RECORD `record` is not an object".to_string());
70 }
71 Ok(SingerMessage::Record { stream, record })
72 }
73 "SCHEMA" => {
74 let stream = obj
75 .get("stream")
76 .and_then(Value::as_str)
77 .ok_or_else(|| "SCHEMA missing `stream`".to_string())?
78 .to_string();
79 let schema = obj
80 .get("schema")
81 .ok_or_else(|| "SCHEMA missing `schema`".to_string())?
82 .clone();
83 Ok(SingerMessage::Schema {
84 stream,
85 schema,
86 key_properties: string_array(obj.get("key_properties")),
87 bookmark_properties: string_array(obj.get("bookmark_properties")),
88 })
89 }
90 "STATE" => {
91 let value = obj
92 .get("value")
93 .ok_or_else(|| "STATE missing `value`".to_string())?
94 .clone();
95 Ok(SingerMessage::State { value })
96 }
97 other => Ok(SingerMessage::Other {
98 message_type: other.to_string(),
99 }),
100 }
101}
102
103#[derive(Serialize)]
104struct SchemaLine<'a> {
105 #[serde(rename = "type")]
106 kind: &'static str,
107 stream: &'a str,
108 schema: &'a Value,
109 key_properties: &'a [String],
110}
111
112#[derive(Serialize)]
113struct RecordLine<'a> {
114 #[serde(rename = "type")]
115 kind: &'static str,
116 stream: &'a str,
117 record: &'a Value,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 version: Option<i64>,
120}
121
122#[derive(Serialize)]
123struct StateLine<'a> {
124 #[serde(rename = "type")]
125 kind: &'static str,
126 value: &'a Value,
127}
128
129#[derive(Serialize)]
130struct ActivateVersionLine<'a> {
131 #[serde(rename = "type")]
132 kind: &'static str,
133 stream: &'a str,
134 version: i64,
135}
136
137fn push_line<T: Serialize>(buf: &mut Vec<u8>, line: &T) {
138 serde_json::to_writer(&mut *buf, line).expect("Singer message serialization is infallible");
139 buf.push(b'\n');
140}
141
142pub fn write_schema(buf: &mut Vec<u8>, stream: &str, schema: &Value, key_properties: &[String]) {
144 push_line(
145 buf,
146 &SchemaLine {
147 kind: "SCHEMA",
148 stream,
149 schema,
150 key_properties,
151 },
152 );
153}
154
155pub fn write_record(buf: &mut Vec<u8>, stream: &str, record: &Value, version: Option<i64>) {
158 push_line(
159 buf,
160 &RecordLine {
161 kind: "RECORD",
162 stream,
163 record,
164 version,
165 },
166 );
167}
168
169pub fn write_state(buf: &mut Vec<u8>, value: &Value) {
171 push_line(
172 buf,
173 &StateLine {
174 kind: "STATE",
175 value,
176 },
177 );
178}
179
180pub fn write_activate_version(buf: &mut Vec<u8>, stream: &str, version: i64) {
183 push_line(
184 buf,
185 &ActivateVersionLine {
186 kind: "ACTIVATE_VERSION",
187 stream,
188 version,
189 },
190 );
191}
192
193fn string_array(v: Option<&Value>) -> Option<Vec<String>> {
195 v.and_then(Value::as_array).map(|arr| {
196 arr.iter()
197 .filter_map(|x| x.as_str().map(str::to_string))
198 .collect()
199 })
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use serde_json::json;
206
207 #[test]
208 fn parses_record() {
209 let m = parse_line(r#"{"type":"RECORD","stream":"s","record":{"id":1}}"#).unwrap();
210 assert_eq!(
211 m,
212 SingerMessage::Record {
213 stream: "s".into(),
214 record: json!({"id": 1})
215 }
216 );
217 }
218
219 #[test]
220 fn parses_schema_with_key_props() {
221 let m = parse_line(
222 r#"{"type":"SCHEMA","stream":"s","schema":{"type":"object"},"key_properties":["id"],"bookmark_properties":["updated_at"]}"#,
223 )
224 .unwrap();
225 match m {
226 SingerMessage::Schema {
227 stream,
228 key_properties,
229 bookmark_properties,
230 ..
231 } => {
232 assert_eq!(stream, "s");
233 assert_eq!(key_properties, Some(vec!["id".to_string()]));
234 assert_eq!(bookmark_properties, Some(vec!["updated_at".to_string()]));
235 }
236 _ => panic!("expected SCHEMA"),
237 }
238 }
239
240 #[test]
241 fn parses_state() {
242 let m = parse_line(r#"{"type":"STATE","value":{"bookmarks":{"s":{"v":5}}}}"#).unwrap();
243 assert_eq!(
244 m,
245 SingerMessage::State {
246 value: json!({"bookmarks":{"s":{"v":5}}})
247 }
248 );
249 }
250
251 #[test]
252 fn activate_version_and_batch_are_other() {
253 assert_eq!(
254 parse_line(r#"{"type":"ACTIVATE_VERSION","stream":"s","version":1}"#).unwrap(),
255 SingerMessage::Other {
256 message_type: "ACTIVATE_VERSION".into()
257 }
258 );
259 assert_eq!(
260 parse_line(r#"{"type":"BATCH","stream":"s"}"#).unwrap(),
261 SingerMessage::Other {
262 message_type: "BATCH".into()
263 }
264 );
265 }
266
267 #[test]
268 fn malformed_variants_error() {
269 assert!(parse_line("").is_err()); assert!(parse_line("not json").is_err()); assert!(parse_line("[1,2,3]").is_err()); assert!(parse_line(r#"{"stream":"s"}"#).is_err()); assert!(parse_line(r#"{"type":"RECORD","stream":"s"}"#).is_err()); assert!(parse_line(r#"{"type":"RECORD","record":{"id":1}}"#).is_err()); assert!(parse_line(r#"{"type":"RECORD","stream":"s","record":5}"#).is_err()); assert!(parse_line(r#"{"type":"STATE"}"#).is_err()); assert!(parse_line(r#"{"type":"SCHEMA","stream":"s"}"#).is_err()); }
279 #[test]
280 fn encoders_round_trip_through_the_parser() {
281 let mut buf = Vec::new();
282 write_schema(
283 &mut buf,
284 "s",
285 &json!({"type": "object"}),
286 &["id".to_string()],
287 );
288 write_record(&mut buf, "s", &json!({"id": 1}), None);
289 write_record(&mut buf, "s", &json!({"id": 2}), Some(7));
290 write_state(&mut buf, &json!({"k": 1}));
291 write_activate_version(&mut buf, "s", 7);
292 let text = String::from_utf8(buf).unwrap();
293 let lines: Vec<&str> = text.lines().collect();
294 assert_eq!(lines.len(), 5);
295 assert!(text.ends_with('\n'));
296 assert_eq!(
297 parse_line(lines[0]).unwrap(),
298 SingerMessage::Schema {
299 stream: "s".into(),
300 schema: json!({"type": "object"}),
301 key_properties: Some(vec!["id".into()]),
302 bookmark_properties: None,
303 }
304 );
305 assert_eq!(
306 parse_line(lines[1]).unwrap(),
307 SingerMessage::Record {
308 stream: "s".into(),
309 record: json!({"id": 1})
310 }
311 );
312 assert!(!lines[1].contains("version"));
313 let v: Value = serde_json::from_str(lines[2]).unwrap();
314 assert_eq!(v["version"], json!(7));
315 assert_eq!(
316 parse_line(lines[3]).unwrap(),
317 SingerMessage::State {
318 value: json!({"k": 1})
319 }
320 );
321 let v: Value = serde_json::from_str(lines[4]).unwrap();
322 assert_eq!(
323 v,
324 json!({"type": "ACTIVATE_VERSION", "stream": "s", "version": 7})
325 );
326 }
327}