1use std::io::Write;
8
9use crate::types::*;
10
11pub struct Emitter<W: Write> {
13 out: W,
14}
15
16impl<W: Write> Emitter<W> {
17 pub fn new(out: W) -> Self {
18 Self { out }
19 }
20
21 pub fn emit_document(&mut self, doc: &Document) -> std::io::Result<()> {
23 self.emit_header(&doc.header)?;
24 for (i, section) in doc.sections.iter().enumerate() {
25 if i > 0 {
26 self.emit_section_break()?;
27 }
28 self.emit_section(section)?;
29 }
30 Ok(())
31 }
32
33 pub fn emit_header(&mut self, header: &Header) -> std::io::Result<()> {
35 write!(self.out, "#!sif v{}", header.version)?;
36 for (key, value) in &header.attributes {
37 if value.contains(' ') || value.contains('"') {
38 write!(self.out, " {}=\"{}\"", key, escape_quoted(value))?;
39 } else {
40 write!(self.out, " {}={}", key, value)?;
41 }
42 }
43 writeln!(self.out)
44 }
45
46 pub fn emit_section_break(&mut self) -> std::io::Result<()> {
48 writeln!(self.out, "---")
49 }
50
51 pub fn emit_section_id(&mut self, id: &str) -> std::io::Result<()> {
53 writeln!(self.out, "§{}", id)
54 }
55
56 pub fn emit_section(&mut self, section: &Section) -> std::io::Result<()> {
58 if let Some(ref id) = section.id {
59 self.emit_section_id(id)?;
60 }
61 for directive in §ion.directives {
62 self.emit_directive(directive)?;
63 }
64 if let Some(ref schema) = section.schema {
65 self.emit_schema(schema)?;
66 }
67 for template in §ion.templates {
68 self.emit_template(template)?;
69 }
70 for block in §ion.blocks {
71 self.emit_block(block)?;
72 }
73 for record in §ion.records {
74 self.emit_record(record)?;
75 }
76 Ok(())
77 }
78
79 pub fn emit_schema(&mut self, schema: &Schema) -> std::io::Result<()> {
81 write!(self.out, "#schema")?;
82 for field in &schema.fields {
83 write!(self.out, " ")?;
84 if field.deprecated {
85 write!(self.out, "∅")?;
86 }
87 write!(self.out, "{}:{}", field.name, field.field_type)?;
88 if let Some(ref sem) = field.semantic {
89 write!(self.out, ":{}", sem)?;
90 }
91 if !field.modifiers.is_empty() {
92 write!(self.out, "|")?;
93 for (i, m) in field.modifiers.iter().enumerate() {
94 if i > 0 {
95 write!(self.out, ",")?;
96 }
97 write!(self.out, "{}", m.name)?;
98 if let Some(ref v) = m.value {
99 if v.contains(',') || v.contains('|') || v.contains(' ') || v.contains('=')
100 {
101 write!(self.out, "=\"{}\"", escape_quoted(v))?;
102 } else {
103 write!(self.out, "={}", v)?;
104 }
105 }
106 }
107 }
108 }
109 writeln!(self.out)
110 }
111
112 pub fn emit_record(&mut self, record: &Record) -> std::io::Result<()> {
114 match record.cdc_op {
115 CdcOp::Insert => {}
116 CdcOp::Update => write!(self.out, "Δ")?,
117 CdcOp::Delete => write!(self.out, "∅")?,
118 }
119 for (i, value) in record.values.iter().enumerate() {
120 if i > 0 {
121 write!(self.out, "\t")?;
122 }
123 emit_value(&mut self.out, value, i == 0)?;
124 }
125 writeln!(self.out)
126 }
127
128 pub fn emit_directive(&mut self, directive: &Directive) -> std::io::Result<()> {
130 match directive {
131 Directive::Context(text) => writeln!(self.out, "#context {}", text),
132 Directive::Source(text) => writeln!(self.out, "#source {}", text),
133 Directive::License(text) => writeln!(self.out, "#license {}", text),
134 Directive::Sort { field, direction } => {
135 let dir = match direction {
136 SortDirection::Asc => "asc",
137 SortDirection::Desc => "desc",
138 };
139 writeln!(self.out, "#sort {} {}", field, dir)
140 }
141 Directive::Filter(text) => writeln!(self.out, "#filter {}", text),
142 Directive::Limit(n) => writeln!(self.out, "#limit {}", n),
143 Directive::Truncated(attrs) => {
144 write!(self.out, "#truncated")?;
145 for (k, v) in attrs {
146 write!(self.out, " {}={}", k, v)?;
147 }
148 writeln!(self.out)
149 }
150 Directive::Relation { from, to } => {
151 write!(self.out, "#relation ")?;
152 emit_field_ref(&mut self.out, from)?;
153 write!(self.out, " -> ")?;
154 emit_field_ref(&mut self.out, to)?;
155 writeln!(self.out)
156 }
157 Directive::Recall => writeln!(self.out, "#recall schema"),
158 Directive::Error(text) => writeln!(self.out, "#error {}", text),
159 Directive::Unknown { name, content } => {
160 if content.is_empty() {
161 writeln!(self.out, "#{}", name)
162 } else {
163 writeln!(self.out, "#{} {}", name, content)
164 }
165 }
166 }
167 }
168
169 pub fn emit_block(&mut self, block: &Block) -> std::io::Result<()> {
171 write!(self.out, "#block {}", block.block_type.as_str())?;
172 for (k, v) in &block.attributes {
173 if v.contains(' ') {
174 write!(self.out, " {}=\"{}\"", k, escape_quoted(v))?;
175 } else {
176 write!(self.out, " {}={}", k, v)?;
177 }
178 }
179 writeln!(self.out)?;
180 write!(self.out, "{}", block.content)?;
181 if !block.content.ends_with('\n') {
182 writeln!(self.out)?;
183 }
184 writeln!(self.out, "#/block")
185 }
186
187 pub fn emit_template(&mut self, template: &Template) -> std::io::Result<()> {
189 writeln!(self.out, "#template {}", template.name)?;
190 write!(self.out, "{}", template.body)?;
191 if !template.body.ends_with('\n') {
192 writeln!(self.out)?;
193 }
194 writeln!(self.out, "#/template")
195 }
196
197 pub fn emit_recall(&mut self) -> std::io::Result<()> {
199 writeln!(self.out, "#recall schema")
200 }
201
202 pub fn writer_mut(&mut self) -> &mut W {
204 &mut self.out
205 }
206
207 pub fn into_inner(self) -> W {
209 self.out
210 }
211}
212
213fn emit_field_ref<W: Write>(out: &mut W, r: &FieldRef) -> std::io::Result<()> {
214 if let Some(ref section) = r.section {
215 write!(out, "§{}.{}", section, r.field)
216 } else {
217 write!(out, "{}", r.field)
218 }
219}
220
221fn emit_value<W: Write>(out: &mut W, value: &Value, is_first_field: bool) -> std::io::Result<()> {
223 match value {
224 Value::Null => write!(out, "_"),
225 Value::Bool(true) => write!(out, "T"),
226 Value::Bool(false) => write!(out, "F"),
227 Value::Int(n) => write!(out, "{}", n),
228 Value::Uint(n) => write!(out, "{}", n),
229 Value::Float(n) => {
230 let s = n.to_string();
231 if s.contains('.') {
232 write!(out, "{}", s)
233 } else {
234 write!(out, "{}.0", s)
235 }
236 }
237 Value::Str(s) => emit_string(out, s, is_first_field),
238 Value::Date(s) | Value::DateTime(s) | Value::Duration(s) => write!(out, "{}", s),
239 Value::Bytes(b) => write!(out, "{}", crate::types::base64_encode(b)),
240 Value::Enum(s) => write!(out, "{}", s),
241 Value::Array(arr) => {
242 write!(out, "[")?;
243 for (i, v) in arr.iter().enumerate() {
244 if i > 0 {
245 write!(out, ",")?;
246 }
247 emit_value(out, v, false)?;
248 }
249 write!(out, "]")
250 }
251 Value::Map(entries) => {
252 write!(out, "{{")?;
253 for (i, (k, v)) in entries.iter().enumerate() {
254 if i > 0 {
255 write!(out, ",")?;
256 }
257 write!(out, "{}:", k)?;
258 emit_value(out, v, false)?;
259 }
260 write!(out, "}}")
261 }
262 }
263}
264
265fn emit_string<W: Write>(out: &mut W, s: &str, is_first_field: bool) -> std::io::Result<()> {
267 if needs_quoting(s, is_first_field) {
268 write!(out, "\"{}\"", escape_quoted(s))
269 } else {
270 write!(out, "{}", s)
271 }
272}
273
274fn needs_quoting(s: &str, is_first_field: bool) -> bool {
276 if s.is_empty() {
277 return true;
278 }
279 if s.starts_with('"') {
280 return true;
281 }
282 if s.starts_with(' ') || s.ends_with(' ') {
283 return true;
284 }
285 if is_first_field && s.starts_with('#') {
286 return true;
287 }
288 if is_first_field && s == "---" {
289 return true;
290 }
291 for c in s.chars() {
292 if c == '\t' || c == '\n' || c == ',' || c == '[' || c == ']' || c == '{' || c == '}' {
293 return true;
294 }
295 }
296 false
297}
298
299fn escape_quoted(s: &str) -> String {
301 let mut out = String::with_capacity(s.len());
302 for c in s.chars() {
303 match c {
304 '\n' => out.push_str("\\n"),
305 '\t' => out.push_str("\\t"),
306 '\\' => out.push_str("\\\\"),
307 '"' => out.push_str("\\\""),
308 _ => out.push(c),
309 }
310 }
311 out
312}
313
314pub fn emit_to_string(doc: &Document) -> String {
316 let mut buf = Vec::new();
317 let mut emitter = Emitter::new(&mut buf);
318 emitter.emit_document(doc).expect("write to Vec never fails");
319 String::from_utf8(buf).expect("SIF output is always UTF-8")
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::parse::parse;
326 use std::collections::HashMap;
327
328 #[test]
329 fn test_roundtrip() {
330 let input = "\
331#!sif v1
332#context Test data
333#schema id:uint:id name:str active:bool tags:str[]
3341\talice\tT\t[admin,user]
3352\tbob\tF\t[]
336";
337 let doc = parse(input).unwrap();
338 let output = emit_to_string(&doc);
339 let reparsed = parse(&output).unwrap();
340
341 assert_eq!(doc.sections[0].records, reparsed.sections[0].records);
342 }
343
344 #[test]
345 fn test_string_quoting() {
346 assert!(!needs_quoting("hello", false));
347 assert!(needs_quoting("", false));
348 assert!(needs_quoting("has\ttab", false));
349 assert!(needs_quoting("has\nnewline", false));
350 assert!(needs_quoting("[bracket", false));
351 assert!(needs_quoting("{brace}", false));
352 assert!(needs_quoting("has,comma", false));
353 assert!(needs_quoting(" leading space", false));
354 assert!(!needs_quoting("#hash", false));
355 assert!(needs_quoting("#hash", true));
356 assert!(needs_quoting("---", true));
357 }
358
359 #[test]
360 fn test_emit_blocks() {
361 let doc = Document {
362 header: Header {
363 version: 1,
364 attributes: HashMap::new(),
365 },
366 sections: vec![Section {
367 id: None,
368 directives: vec![],
369 schema: None,
370 records: vec![],
371 blocks: vec![Block {
372 block_type: BlockType::Code,
373 attributes: vec![("language".to_string(), "rust".to_string())],
374 content: "fn main() {}".to_string(),
375 }],
376 templates: vec![],
377 }],
378 };
379 let output = emit_to_string(&doc);
380 assert!(output.contains("#block code language=rust"));
381 assert!(output.contains("fn main() {}"));
382 assert!(output.contains("#/block"));
383 }
384
385 #[test]
386 fn test_emit_cdc() {
387 let doc = Document {
388 header: Header {
389 version: 1,
390 attributes: HashMap::new(),
391 },
392 sections: vec![Section {
393 id: None,
394 directives: vec![],
395 schema: Some(Schema {
396 fields: vec![
397 FieldDef {
398 name: "id".to_string(),
399 field_type: Type::Uint,
400 semantic: Some("id".to_string()),
401 deprecated: false,
402 modifiers: vec![],
403 },
404 FieldDef {
405 name: "name".to_string(),
406 field_type: Type::Str,
407 semantic: None,
408 deprecated: false,
409 modifiers: vec![],
410 },
411 ],
412 }),
413 records: vec![
414 Record {
415 values: vec![Value::Uint(1), Value::Str("alice".to_string())],
416 cdc_op: CdcOp::Insert,
417 },
418 Record {
419 values: vec![Value::Uint(1), Value::Str("alice2".to_string())],
420 cdc_op: CdcOp::Update,
421 },
422 Record {
423 values: vec![Value::Uint(1), Value::Str("alice2".to_string())],
424 cdc_op: CdcOp::Delete,
425 },
426 ],
427 blocks: vec![],
428 templates: vec![],
429 }],
430 };
431 let output = emit_to_string(&doc);
432 assert!(output.contains("1\talice\n"));
433 assert!(output.contains("Δ1\talice2\n"));
434 assert!(output.contains("∅1\talice2\n"));
435 }
436}