diff --git a/implementations/rust/preserves/src/value/text/writer.rs b/implementations/rust/preserves/src/value/text/writer.rs index a321240..0bdb05b 100644 --- a/implementations/rust/preserves/src/value/text/writer.rs +++ b/implementations/rust/preserves/src/value/text/writer.rs @@ -15,7 +15,12 @@ pub enum CommaStyle { Terminating, } -pub struct TextWriter(Suspendable, CommaStyle); +pub struct TextWriter { + w: Suspendable, + pub comma_style: CommaStyle, + pub indentation: usize, + indent: String, +} impl std::default::Default for CommaStyle { fn default() -> Self { @@ -25,15 +30,20 @@ impl std::default::Default for CommaStyle { impl TextWriter { pub fn new(w: W) -> Self { - TextWriter(Suspendable::new(w), CommaStyle::default()) + TextWriter { + w: Suspendable::new(w), + comma_style: CommaStyle::default(), + indentation: 0, + indent: "\n".to_owned(), + } } pub fn suspend(&mut self) -> Self { - TextWriter(self.0.suspend(), self.1) + TextWriter { w: self.w.suspend(), indent: self.indent.clone(), .. *self } } pub fn resume(&mut self, other: Self) { - self.0.resume(other.0) + self.w.resume(other.w) } pub fn write_stringlike_char_fallback( @@ -44,68 +54,109 @@ impl TextWriter { F: FnOnce(&mut W, char) -> io::Result<()> { match c { - '\\' => write!(self.0, "\\\\"), - '\x08' => write!(self.0, "\\b"), - '\x0c' => write!(self.0, "\\f"), - '\x0a' => write!(self.0, "\\n"), - '\x0d' => write!(self.0, "\\r"), - '\x09' => write!(self.0, "\\t"), - _ => f(&mut self.0, c), + '\\' => write!(self.w, "\\\\"), + '\x08' => write!(self.w, "\\b"), + '\x0c' => write!(self.w, "\\f"), + '\x0a' => write!(self.w, "\\n"), + '\x0d' => write!(self.w, "\\r"), + '\x09' => write!(self.w, "\\t"), + _ => f(&mut self.w, c), } } pub fn write_stringlike_char(&mut self, c: char) -> io::Result<()> { self.write_stringlike_char_fallback(c, |w, c| write!(w, "{}", c)) } + + pub fn add_indent(&mut self) { + for _ in 0 .. self.indentation { + self.indent.push(' ') + } + } + + pub fn del_indent(&mut self) { + if self.indentation > 0 { + self.indent.truncate(self.indent.len() - self.indentation) + } + } + + pub fn indent(&mut self) -> io::Result<()> { + if self.indentation > 0 { + write!(self.w, "{}", &self.indent) + } else { + Ok(()) + } + } + + pub fn indent_sp(&mut self) -> io::Result<()> { + if self.indentation > 0 { + write!(self.w, "{}", &self.indent) + } else { + write!(self.w, " ") + } + } } impl CompoundWriter for TextWriter { fn boundary(&mut self, b: &B::Type) -> io::Result<()> { match (b.closing.as_ref(), b.opening.as_ref()) { - (None, Some(B::Item::Annotation)) => - write!(self.0, "@"), - (Some(_), Some(B::Item::Annotation)) => - write!(self.0, " @"), - (Some(B::Item::Annotation), Some(B::Item::AnnotatedValue)) => - write!(self.0, " "), + (None, Some(B::Item::RecordLabel)) | + (Some(B::Item::RecordLabel), None) | + (Some(B::Item::RecordField), None) => + return Ok(()), + (_, Some(B::Item::RecordField)) => + return write!(self.w, " "), - (Some(B::Item::DictionaryKey), Some(B::Item::DictionaryValue)) => - write!(self.0, ": "), + (Some(B::Item::DictionaryKey), Some(B::Item::DictionaryValue)) => { + return write!(self.w, ": ") + } - (Some(B::Item::RecordLabel), Some(B::Item::RecordField)) | - (Some(B::Item::RecordField), Some(B::Item::RecordField)) => - write!(self.0, " "), + (None, Some(B::Item::Annotation)) => { + return write!(self.w, "@") + } + (Some(_), Some(B::Item::Annotation)) => { + return write!(self.w, " @") + } + (Some(B::Item::Annotation), Some(B::Item::AnnotatedValue)) => { + return write!(self.w, " ") + } + (Some(B::Item::AnnotatedValue), None) => + return Ok(()), - (Some(B::Item::DictionaryValue), Some(B::Item::DictionaryKey)) | - (Some(B::Item::SequenceValue), Some(B::Item::SequenceValue)) | - (Some(B::Item::SetValue), Some(B::Item::SetValue)) => - match self.1 { - CommaStyle::Separating | CommaStyle::Terminating => - write!(self.0, ", "), - CommaStyle::None => - write!(self.0, " "), - }, - - (Some(B::Item::DictionaryValue), None) | - (Some(B::Item::SequenceValue), None) | - (Some(B::Item::SetValue), None) => - match self.1 { - CommaStyle::Terminating => - write!(self.0, ","), - CommaStyle::Separating | CommaStyle::None => - Ok(()), - }, - - _ => - Ok(()) + _ => (), } + + match (b.closing.as_ref(), b.opening.as_ref()) { + (None, None) => (), + (None, Some(_)) => { + self.add_indent(); + self.indent()? + }, + (Some(_), Some(_)) => { + match self.comma_style { + CommaStyle::Separating | CommaStyle::Terminating => write!(self.w, ",")?, + CommaStyle::None => (), + } + self.indent_sp()? + } + (Some(_), None) => { + match self.comma_style { + CommaStyle::Terminating => write!(self.w, ",")?, + CommaStyle::Separating | CommaStyle::None => (), + } + self.del_indent(); + self.indent()? + } + } + + Ok(()) } } macro_rules! simple_writer_method { ($n:ident, $argty:ty) => (fn $n (&mut self, v: $argty) -> io::Result<()> { - write!(self.0, "{}", v) + write!(self.w, "{}", v) }); } @@ -127,16 +178,16 @@ impl Writer for TextWriter { } fn write_bool(&mut self, v: bool) -> io::Result<()> { - write!(self.0, "{}", if v { "#t" } else { "#f" }) + write!(self.w, "{}", if v { "#t" } else { "#f" }) } fn write_f32(&mut self, v: f32) -> io::Result<()> { - dtoa::write(&mut *self.0, v)?; - write!(self.0, "f") + dtoa::write(&mut *self.w, v)?; + write!(self.w, "f") } fn write_f64(&mut self, v: f64) -> io::Result<()> { - dtoa::write(&mut *self.0, v)?; + dtoa::write(&mut *self.w, v)?; Ok(()) } @@ -153,79 +204,79 @@ impl Writer for TextWriter { simple_writer_method!(write_int, &BigInt); fn write_string(&mut self, v: &str) -> io::Result<()> { - write!(self.0, "\"")?; + write!(self.w, "\"")?; for c in v.chars() { match c { - '"' => write!(self.0, "\\\"")?, + '"' => write!(self.w, "\\\"")?, _ => self.write_stringlike_char(c)?, } } - write!(self.0, "\"") + write!(self.w, "\"") } fn write_bytes(&mut self, v: &[u8]) -> io::Result<()> { - write!(self.0, "#[{}]", base64::encode_config(v, base64::STANDARD_NO_PAD)) + write!(self.w, "#[{}]", base64::encode_config(v, base64::STANDARD_NO_PAD)) } fn write_symbol(&mut self, v: &str) -> io::Result<()> { // FIXME: This regular expression is conservatively correct, but Anglo-chauvinistic. let re = regex::Regex::new("^[a-zA-Z~!$%^&*?_=+/.][-a-zA-Z~!$%^&*?_=+/.0-9]*$").unwrap(); if re.is_match(v) { - write!(self.0, "{}", v) + write!(self.w, "{}", v) } else { - write!(self.0, "|")?; + write!(self.w, "|")?; for c in v.chars() { match c { - '|' => write!(self.0, "\\|")?, + '|' => write!(self.w, "\\|")?, _ => self.write_stringlike_char(c)?, } } - write!(self.0, "|") + write!(self.w, "|") } } fn start_record(&mut self, _field_count: Option) -> io::Result { - write!(self.0, "<")?; + write!(self.w, "<")?; Ok(self.suspend()) } fn end_record(&mut self, rec: Self::RecWriter) -> io::Result<()> { self.resume(rec); - write!(self.0, ">") + write!(self.w, ">") } fn start_sequence(&mut self, _item_count: Option) -> io::Result { - write!(self.0, "[")?; + write!(self.w, "[")?; Ok(self.suspend()) } fn end_sequence(&mut self, seq: Self::SeqWriter) -> io::Result<()> { self.resume(seq); - write!(self.0, "]") + write!(self.w, "]") } fn start_set(&mut self, _item_count: Option) -> io::Result { - write!(self.0, "#{{")?; + write!(self.w, "#{{")?; Ok(self.suspend()) } fn end_set(&mut self, set: Self::SetWriter) -> io::Result<()> { self.resume(set); - write!(self.0, "}}") + write!(self.w, "}}") } fn start_dictionary(&mut self, _entry_count: Option) -> io::Result { - write!(self.0, "{{")?; + write!(self.w, "{{")?; Ok(self.suspend()) } fn end_dictionary(&mut self, dict: Self::DictWriter) -> io::Result<()> { self.resume(dict); - write!(self.0, "}}") + write!(self.w, "}}") } fn start_embedded(&mut self) -> io::Result { - write!(self.0, "#!")?; + write!(self.w, "#!")?; Ok(self.suspend()) } diff --git a/implementations/rust/preserves/tests/samples_tests.rs b/implementations/rust/preserves/tests/samples_tests.rs index 0320b42..d4e992a 100644 --- a/implementations/rust/preserves/tests/samples_tests.rs +++ b/implementations/rust/preserves/tests/samples_tests.rs @@ -67,7 +67,16 @@ fn decode_all(bytes: &'_ [u8]) -> io::Result> { let s = String::from_utf8(bs).unwrap(); preserves::value::text::annotated_iovalue_from_str(&s)? }; + let roundtripped_indented = { + let mut bs = Vec::new(); + let mut w = preserves::value::TextWriter::new(&mut bs); + w.indentation = 4; + preserves::ser::to_writer(&mut w, &from_text)?; + let s = String::from_utf8(bs).unwrap(); + preserves::value::text::annotated_iovalue_from_str(&s)? + }; assert_eq!(from_text, roundtripped); + assert_eq!(from_text, roundtripped_indented); Ok(()) }