Skip to main content

sif_parser/
emit.rs

1// SIF Core v1 — Emitter.
2//
3// Writes SIF documents to any `Write` target.
4//
5// Reference: SIF-SPEC.md §25.1 (For Emitters).
6
7use std::io::Write;
8
9use crate::types::*;
10
11/// A SIF document emitter.
12pub 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    /// Write a complete document.
22    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    /// Write the header line.
34    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    /// Write a section break.
47    pub fn emit_section_break(&mut self) -> std::io::Result<()> {
48        writeln!(self.out, "---")
49    }
50
51    /// Write a section identifier.
52    pub fn emit_section_id(&mut self, id: &str) -> std::io::Result<()> {
53        writeln!(self.out, "§{}", id)
54    }
55
56    /// Write a complete section.
57    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 &section.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 &section.templates {
68            self.emit_template(template)?;
69        }
70        for block in &section.blocks {
71            self.emit_block(block)?;
72        }
73        for record in &section.records {
74            self.emit_record(record)?;
75        }
76        Ok(())
77    }
78
79    /// Write a `#schema` directive.
80    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    /// Write a data record.
113    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    /// Write a directive.
129    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    /// Write a block.
170    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    /// Write a template.
188    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    /// Write a `#recall schema` directive.
198    pub fn emit_recall(&mut self) -> std::io::Result<()> {
199        writeln!(self.out, "#recall schema")
200    }
201
202    /// Get a mutable reference to the underlying writer.
203    pub fn writer_mut(&mut self) -> &mut W {
204        &mut self.out
205    }
206
207    /// Consume the emitter and return the underlying writer.
208    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
221/// Emit a single value, quoting strings when necessary per §11.
222fn 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
265/// Emit a string, quoting when required per §11.
266fn 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
274/// Check if a string value needs quoting per §11.
275fn 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
299/// Escape a string for quoting (§11.1).
300fn 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
314/// Convenience: emit a document to a String.
315pub 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}