ion_rs/text/
raw_text_writer.rs

1use std::io::{BufWriter, Write};
2
3use bigdecimal::BigDecimal;
4use chrono::{DateTime, FixedOffset};
5
6use crate::element::writer::TextKind;
7use crate::raw_symbol_token_ref::{AsRawSymbolTokenRef, RawSymbolTokenRef};
8use crate::result::{illegal_operation, IonResult};
9use crate::text::text_formatter::STRING_ESCAPE_CODES;
10use crate::types::{ContainerType, Decimal, Timestamp};
11use crate::writer::IonWriter;
12use crate::{Int, IonType, RawSymbolToken};
13
14pub struct RawTextWriterBuilder {
15    whitespace_config: WhitespaceConfig,
16}
17
18impl RawTextWriterBuilder {
19    /// Constructs a text Ion writer with the specified formatting. See [`TextKind`] for details.
20    pub fn new(text_kind: TextKind) -> RawTextWriterBuilder {
21        match text_kind {
22            TextKind::Compact => Self::compact(),
23            TextKind::Lines => Self::lines(),
24            TextKind::Pretty => Self::pretty(),
25        }
26    }
27
28    /// Constructs a text Ion writer with modest (but not strictly minimal) spacing.
29    ///
30    /// For example:
31    /// ```text
32    /// {foo: 1, bar: 2, baz: 3} [1, 2, 3] true "hello"
33    /// ```
34    pub fn compact() -> RawTextWriterBuilder {
35        RawTextWriterBuilder {
36            whitespace_config: COMPACT_WHITESPACE_CONFIG.clone(),
37        }
38    }
39
40    /// Constructs a 'lines' text Ion writer that adds UNIX and human-friendly newlines between
41    /// top-level values.
42    ///
43    /// For example:
44    /// ```text
45    /// {foo: 1, bar: 2, baz: 3}
46    /// [1, 2, 3]
47    /// true
48    /// "hello"
49    /// ```
50    // This doesn't solve the problem of final newlines. Should find a way to solve that some day.
51    //TODO: https://github.com/amazon-ion/ion-rust/issues/437
52    pub fn lines() -> RawTextWriterBuilder {
53        RawTextWriterBuilder {
54            whitespace_config: LINES_WHITESPACE_CONFIG.clone(),
55        }
56    }
57
58    /// Constructs a 'pretty' text Ion writer that adds human-friendly spacing between values.
59    ///
60    /// For example:
61    /// ```text
62    /// {
63    ///     foo: 1,
64    ///     bar: 2,
65    ///     baz: 3
66    /// }
67    /// [
68    ///     1,
69    ///     2,
70    ///     3
71    /// ]
72    /// true
73    /// "hello"
74    /// ```
75    pub fn pretty() -> RawTextWriterBuilder {
76        RawTextWriterBuilder {
77            whitespace_config: PRETTY_WHITESPACE_CONFIG.clone(),
78        }
79    }
80
81    pub fn with_space_between_top_level_values(
82        mut self,
83        space_between_top_level_values: &'static str,
84    ) -> RawTextWriterBuilder {
85        self.whitespace_config.space_between_top_level_values = space_between_top_level_values;
86        self
87    }
88
89    pub fn with_space_between_nested_values(
90        mut self,
91        space_between_values: &'static str,
92    ) -> RawTextWriterBuilder {
93        self.whitespace_config.space_between_nested_values = space_between_values;
94        self
95    }
96
97    pub fn with_indentation(mut self, indentation: &'static str) -> RawTextWriterBuilder {
98        self.whitespace_config.indentation = indentation;
99        self
100    }
101
102    pub fn with_space_after_field_name(
103        mut self,
104        space_after_field_name: &'static str,
105    ) -> RawTextWriterBuilder {
106        self.whitespace_config.space_after_field_name = space_after_field_name;
107        self
108    }
109
110    pub fn with_space_after_container_start(
111        mut self,
112        space_after_container_start: &'static str,
113    ) -> RawTextWriterBuilder {
114        self.whitespace_config.space_after_container_start = space_after_container_start;
115        self
116    }
117
118    /// Constructs a new instance of [RawTextWriter] that writes values to the provided io::Write
119    /// implementation.
120    pub fn build<W: Write>(self, sink: W) -> IonResult<RawTextWriter<W>> {
121        let raw_text_writer = RawTextWriter {
122            output: BufWriter::new(sink),
123            annotations: Vec::new(),
124            field_name: None,
125            containers: vec![EncodingLevel::default()],
126            // Should we validate here that all the strings in `whitespace_config` actually are
127            // semantically whitespace?
128            //TODO: https://github.com/amazon-ion/ion-rust/issues/438
129            whitespace_config: Box::new(self.whitespace_config),
130        };
131        // This method cannot currently fail. It returns an IonResult<_> to be consistent with the
132        // other builder APIs and to allow for fallible setup operations in the future.
133        Ok(raw_text_writer)
134    }
135}
136
137impl Default for RawTextWriterBuilder {
138    fn default() -> Self {
139        RawTextWriterBuilder::new(TextKind::Compact)
140    }
141}
142
143#[derive(Debug, PartialEq, Default)]
144struct EncodingLevel {
145    container_type: ContainerType,
146    child_count: usize,
147}
148
149#[derive(Clone)]
150struct WhitespaceConfig {
151    // Top-level values are independent of other values in the stream, we may separate differently
152    space_between_top_level_values: &'static str,
153    // Non-top-level values are within a container
154    space_between_nested_values: &'static str,
155    // Indentation is repeated before nested values, corresponding to the level of nesting
156    indentation: &'static str,
157    // e.g. after 'foo:' in "{foo: bar}"
158    space_after_field_name: &'static str,
159    // Between the container open and any value in it
160    space_after_container_start: &'static str,
161}
162
163static COMPACT_WHITESPACE_CONFIG: WhitespaceConfig = WhitespaceConfig {
164    // Single space between top level values
165    space_between_top_level_values: " ",
166    // Single space between values
167    space_between_nested_values: " ",
168    // No indentation
169    indentation: "",
170    // Single space between field names and values
171    space_after_field_name: " ",
172    // The first value in a container appears next to the opening delimiter
173    space_after_container_start: "",
174};
175
176static LINES_WHITESPACE_CONFIG: WhitespaceConfig = WhitespaceConfig {
177    // Each value appears on its own line
178    space_between_top_level_values: "\n",
179    // Otherwise use the compact/default layout from `DEFAULT_WS_CONFIG`
180    ..COMPACT_WHITESPACE_CONFIG
181};
182
183static PRETTY_WHITESPACE_CONFIG: WhitespaceConfig = WhitespaceConfig {
184    // Each top-level value starts on its own line
185    space_between_top_level_values: "\n",
186    // Each value appears on its own line
187    space_between_nested_values: "\n",
188    // Values get two spaces of indentation per level of depth
189    indentation: "  ",
190    // Field names and values are separated by a single space
191    space_after_field_name: " ",
192    // The first value in a container appears on a line by itself
193    space_after_container_start: "\n",
194};
195
196pub struct RawTextWriter<W: Write> {
197    output: BufWriter<W>,
198    annotations: Vec<RawSymbolToken>,
199    field_name: Option<RawSymbolToken>,
200    containers: Vec<EncodingLevel>,
201    whitespace_config: Box<WhitespaceConfig>,
202}
203
204impl<W: Write> RawTextWriter<W> {
205    /// Returns true if the RawTextWriter is currently positioned within a Struct.
206    pub fn is_in_struct(&self) -> bool {
207        self.parent_level().container_type == ContainerType::Struct
208    }
209
210    /// Returns the number of values that have already been written in this container.
211    /// Before the first value in a container is written, this returns `0`.
212    /// For the purposes of this method, the top level is considered a container.
213    fn index_within_parent(&self) -> usize {
214        self.parent_level().child_count
215    }
216
217    /// Returns the `&EncodingLevel` into which the RawTextWriter most recently stepped.
218    fn parent_level(&self) -> &EncodingLevel {
219        // `self.containers` is never empty; it always has at least the top level.
220        self.containers.last().unwrap()
221    }
222
223    /// Increments the value returned by [Self::index_within_parent].
224    fn increment_child_count(&mut self) {
225        let parent_level = self.containers.last_mut().unwrap();
226        parent_level.child_count += 1;
227    }
228
229    /// Called after each value is written to emit an appropriate delimiter before the next value.
230    fn write_value_delimiter(&mut self) -> IonResult<()> {
231        use ContainerType::*;
232        let delimiter = match self.parent_level().container_type {
233            TopLevel => "",
234            Struct | List => ",",
235            SExpression => "",
236        };
237        write!(self.output, "{delimiter}")?;
238        Ok(())
239    }
240
241    /// Writes any interstitial whitespace that is appropriate for the current context, including:
242    /// * Whitespace following a container start
243    /// * Whitespace between values
244    /// * Indentation
245    fn write_space_before_value(&mut self) -> IonResult<()> {
246        // If this is the first value in this container...
247        if self.index_within_parent() == 0 {
248            // ...and we're not at the top level...
249            if self.depth() > 0 {
250                // ...then this is the first value inside a container. We'll write the
251                // `space_after_container_start` so it will (e.g.) appear on its own line.
252                write!(
253                    &mut self.output,
254                    "{}",
255                    self.whitespace_config.space_after_container_start
256                )?;
257            }
258        } else {
259            // Otherwise, this is not the first value in this container. Emit the container's
260            // delimiter (for example: in a list, write a `,`) before we write the value itself.
261            self.write_value_delimiter()?;
262
263            let value_spacer = if self.depth() == 0 {
264                &self.whitespace_config.space_between_top_level_values
265            } else {
266                &self.whitespace_config.space_between_nested_values
267            };
268            write!(&mut self.output, "{value_spacer}")?;
269        }
270
271        if !self.whitespace_config.indentation.is_empty() {
272            // Write enough indentation for the current level of depth
273            for _ in 0..self.depth() {
274                write!(&mut self.output, "{}", self.whitespace_config.indentation)?;
275            }
276        }
277        Ok(())
278    }
279
280    /// Returns `true` if the provided `token`'s text is an 'identifier'. That is, the text starts
281    /// with a `$`, `_` or ASCII letter and is followed by a sequence of `$`, `_`, or ASCII letters
282    /// and numbers. Examples:
283    /// * `firstName`
284    /// * `first_name`
285    /// * `name_1`
286    /// * `$name`
287    /// Unlike other symbols, identifiers don't have to be wrapped in quotes.
288    fn token_is_identifier(token: &str) -> bool {
289        if token.is_empty() {
290            return false;
291        }
292        let mut chars = token.chars();
293        let first = chars.next().unwrap();
294        (first == '$' || first == '_' || first.is_ascii_alphabetic())
295            && chars.all(|c| c == '$' || c == '_' || c.is_ascii_alphanumeric())
296    }
297
298    /// Returns `true` if the provided text is an Ion keyword. Keywords like `true` or `null`
299    /// resemble identifiers, but writers must wrap them in quotes when using them as symbol text.
300    fn token_is_keyword(token: &str) -> bool {
301        const KEYWORDS: &[&str] = &["true", "false", "nan", "null"];
302        KEYWORDS.contains(&token)
303    }
304
305    /// Returns `true` if this token's text resembles a symbol ID literal. For example: `'$99'` is a
306    /// symbol with the text `$99`. However, `$99` (without quotes) is a symbol ID that maps to
307    /// different text.
308    fn token_resembles_symbol_id(token: &str) -> bool {
309        if token.is_empty() {
310            return false;
311        }
312        let mut chars = token.chars();
313        let first = chars.next().unwrap();
314        first == '$' && chars.all(|c| c.is_numeric())
315    }
316
317    pub(crate) fn write_symbol_token<A: AsRawSymbolTokenRef>(
318        output: &mut BufWriter<W>,
319        token: A,
320    ) -> IonResult<()> {
321        match token.as_raw_symbol_token_ref() {
322            RawSymbolTokenRef::SymbolId(sid) => write!(output, "${sid}")?,
323            RawSymbolTokenRef::Text(text)
324                if Self::token_is_keyword(text) || Self::token_resembles_symbol_id(text) =>
325            {
326                // Write the symbol text in single quotes
327                write!(output, "'{text}'")?;
328            }
329            RawSymbolTokenRef::Text(text) if Self::token_is_identifier(text) => {
330                // Write the symbol text without quotes
331                write!(output, "{text}")?
332            }
333            RawSymbolTokenRef::Text(text) => {
334                // Write the symbol text using quotes and escaping any characters that require it.
335                write!(output, "\'")?;
336                Self::write_escaped_text_body(output, text)?;
337                write!(output, "\'")?;
338            }
339        };
340        Ok(())
341    }
342
343    // Write the field name and annotations if set
344    fn write_value_metadata(&mut self) -> IonResult<()> {
345        if let Some(field_name) = &self.field_name.take() {
346            Self::write_symbol_token(&mut self.output, field_name)?;
347            write!(
348                self.output,
349                ":{}",
350                self.whitespace_config.space_after_field_name
351            )?;
352        } else if self.is_in_struct() {
353            return illegal_operation("Values inside a struct must have a field name.");
354        }
355
356        if !self.annotations.is_empty() {
357            for annotation in &self.annotations {
358                Self::write_symbol_token(&mut self.output, annotation)?;
359                write!(self.output, "::")?;
360            }
361            self.annotations.clear();
362        }
363        Ok(())
364    }
365
366    // Writes:
367    // * the field name (if any)
368    // * the annotations (if any)
369    // * the value written by the `scalar_writer` closure
370    // * a trailing delimiter (if any)
371    fn write_scalar<F>(&mut self, scalar_writer: F) -> IonResult<()>
372    where
373        F: FnOnce(&mut BufWriter<W>) -> IonResult<()>,
374    {
375        self.write_space_before_value()?;
376        self.write_value_metadata()?;
377        scalar_writer(&mut self.output)?;
378        // We just wrote another value; bump the child count.
379        self.increment_child_count();
380        Ok(())
381    }
382
383    /// Writes the provided BigDecimal value as an Ion decimal.
384    pub fn write_big_decimal(&mut self, value: &BigDecimal) -> IonResult<()> {
385        self.write_scalar(|output| {
386            write!(output, "{}", &value)?;
387            Ok(())
388        })
389    }
390
391    /// Writes the provided DateTime value as an Ion timestamp.
392    #[deprecated(
393        since = "0.6.1",
394        note = "Please use the `write_timestamp` method instead."
395    )]
396    pub fn write_datetime(&mut self, value: &DateTime<FixedOffset>) -> IonResult<()> {
397        self.write_scalar(|output| {
398            write!(output, "{}", value.to_rfc3339())?;
399            Ok(())
400        })
401    }
402
403    pub fn add_annotation<A: AsRawSymbolTokenRef>(&mut self, annotation: A) {
404        // TODO: This function currently allocates a new string for each annotation.
405        //       It will be common for this text to come from the symbol table; we should
406        //       make it possible to pass an Arc<str> or similar when applicable.
407        //       See: https://github.com/amazon-ion/ion-rust/issues/496
408        let token = match annotation.as_raw_symbol_token_ref() {
409            RawSymbolTokenRef::SymbolId(sid) => RawSymbolToken::SymbolId(sid),
410            RawSymbolTokenRef::Text(text) => RawSymbolToken::Text(text.to_string()),
411        };
412        self.annotations.push(token);
413    }
414
415    /// Writes the body (i.e. no start or end delimiters) of a string or symbol with any illegal
416    /// characters escaped.
417    pub(crate) fn write_escaped_text_body<S: AsRef<str>>(
418        output: &mut BufWriter<W>,
419        value: S,
420    ) -> IonResult<()> {
421        let mut start = 0usize;
422        let text = value.as_ref();
423        for (byte_index, character) in text.char_indices() {
424            let escaped = match character {
425                '\n' => r"\n",
426                '\r' => r"\r",
427                '\t' => r"\t",
428                '\\' => r"\\",
429                '/' => r"\/",
430                '"' => r#"\""#,
431                '\'' => r"\'",
432                '?' => r"\?",
433                '\x00' => r"\0", // NUL
434                '\x07' => r"\a", // alert BEL
435                '\x08' => r"\b", // backspace
436                '\x0B' => r"\v", // vertical tab
437                '\x0C' => r"\f", // form feed
438                _ => {
439                    // Other characters can be left as-is
440                    continue;
441                }
442            };
443            // If we reach this point, the current character needed to be escaped.
444            // Write all of the text leading up to this character to output, then the escaped
445            // version of this character.
446            write!(output, "{}{}", &text[start..byte_index], escaped)?;
447            // Update `start` to point to the first byte after the end of this character.
448            start = byte_index + character.len_utf8();
449        }
450        write!(output, "{}", &text[start..])?;
451        Ok(())
452    }
453}
454
455impl<W: Write> IonWriter for RawTextWriter<W> {
456    type Output = W;
457
458    fn ion_version(&self) -> (u8, u8) {
459        (1, 0)
460    }
461
462    fn write_ion_version_marker(&mut self, major: u8, minor: u8) -> IonResult<()> {
463        write!(self.output, "$ion_{major}_{minor}")?;
464        Ok(())
465    }
466
467    fn supports_text_symbol_tokens(&self) -> bool {
468        true
469    }
470
471    /// Sets a list of annotations that will be applied to the next value that is written.
472    fn set_annotations<I, A>(&mut self, annotations: I)
473    where
474        A: AsRawSymbolTokenRef,
475        I: IntoIterator<Item = A>,
476    {
477        self.annotations.clear();
478        for annotation in annotations {
479            self.add_annotation(annotation)
480        }
481    }
482
483    /// Writes an Ion null of the specified type.
484    fn write_null(&mut self, ion_type: IonType) -> IonResult<()> {
485        use IonType::*;
486        self.write_scalar(|output| {
487            let null_text = match ion_type {
488                Null => "null",
489                Bool => "null.bool",
490                Int => "null.int",
491                Float => "null.float",
492                Decimal => "null.decimal",
493                Timestamp => "null.timestamp",
494                Symbol => "null.symbol",
495                String => "null.string",
496                Blob => "null.blob",
497                Clob => "null.clob",
498                List => "null.list",
499                SExp => "null.sexp",
500                Struct => "null.struct",
501            };
502            write!(output, "{null_text}")?;
503            Ok(())
504        })
505    }
506
507    /// Writes the provided bool value as an Ion boolean.
508    fn write_bool(&mut self, value: bool) -> IonResult<()> {
509        self.write_scalar(|output| {
510            let bool_text = match value {
511                true => "true",
512                false => "false",
513            };
514            write!(output, "{bool_text}")?;
515            Ok(())
516        })
517    }
518
519    /// Writes the provided i64 value as an Ion integer.
520    fn write_i64(&mut self, value: i64) -> IonResult<()> {
521        self.write_scalar(|output| {
522            write!(output, "{value}")?;
523            Ok(())
524        })
525    }
526
527    /// Writes an Ion `integer` with the specified value to the output stream.
528    fn write_int(&mut self, value: &Int) -> IonResult<()> {
529        self.write_scalar(|output| {
530            write!(output, "{value}")?;
531            Ok(())
532        })
533    }
534
535    /// Writes the provided f64 value as an Ion float.
536    fn write_f32(&mut self, value: f32) -> IonResult<()> {
537        // The text writer doesn't distinguish between f32 and f64 in its output.
538        self.write_f64(value as f64)
539    }
540
541    /// Writes the provided f64 value as an Ion float.
542    fn write_f64(&mut self, value: f64) -> IonResult<()> {
543        self.write_scalar(|output| {
544            if value.is_nan() {
545                write!(output, "nan")?;
546                return Ok(());
547            }
548
549            if value.is_infinite() {
550                if value.is_sign_positive() {
551                    write!(output, "+inf")?;
552                } else {
553                    write!(output, "-inf")?;
554                }
555                return Ok(());
556            }
557
558            // The {:e} formatter provided by the Display trait writes floats using scientific
559            // notation. It works for all floating point values except -0.0 (it drops the sign).
560            // See: https://github.com/rust-lang/rust/issues/20596
561            if value == 0.0f64 && value.is_sign_negative() {
562                write!(output, "-0e0")?;
563                return Ok(());
564            }
565
566            write!(output, "{value:e}")?;
567            Ok(())
568        })
569    }
570
571    /// Writes the provided Decimal as an Ion decimal.
572    fn write_decimal(&mut self, value: &Decimal) -> IonResult<()> {
573        self.write_scalar(|output| {
574            write!(output, "{value}")?;
575            Ok(())
576        })
577    }
578
579    /// Writes the provided Timestamp as an Ion timestamp.
580    fn write_timestamp(&mut self, value: &Timestamp) -> IonResult<()> {
581        self.write_scalar(|output| {
582            write!(output, "{value}")?;
583            Ok(())
584        })
585    }
586
587    /// Writes the provided &str value as an Ion symbol.
588    fn write_symbol<A: AsRawSymbolTokenRef>(&mut self, value: A) -> IonResult<()> {
589        self.write_scalar(|output| {
590            RawTextWriter::write_symbol_token(output, value)?;
591            Ok(())
592        })
593    }
594
595    /// Writes the provided &str value as an Ion string.
596    fn write_string<S: AsRef<str>>(&mut self, value: S) -> IonResult<()> {
597        self.write_scalar(|output| {
598            write!(output, "\"")?;
599            RawTextWriter::write_escaped_text_body(output, value)?;
600            write!(output, "\"")?;
601            Ok(())
602        })
603    }
604
605    /// Writes the provided byte array slice as an Ion clob.
606    fn write_clob<A: AsRef<[u8]>>(&mut self, value: A) -> IonResult<()> {
607        // clob_value to be written based on defined STRING_ESCAPE_CODES.
608        const NUM_DELIMITER_BYTES: usize = 4; // {{}}
609        const NUM_HEX_BYTES_PER_BYTE: usize = 4; // \xHH
610
611        let value: &[u8] = value.as_ref();
612
613        // Set aside enough memory to hold a clob containing all hex-encoded bytes
614        let mut clob_value =
615            String::with_capacity((value.len() * NUM_HEX_BYTES_PER_BYTE) + NUM_DELIMITER_BYTES);
616
617        for byte in value.iter().copied() {
618            let c = byte as char;
619            let escaped_byte = STRING_ESCAPE_CODES[c as usize];
620            if !escaped_byte.is_empty() {
621                clob_value.push_str(escaped_byte);
622            } else {
623                clob_value.push(c);
624            }
625        }
626        self.write_scalar(|output| {
627            write!(output, "{{{{\"{clob_value}\"}}}}")?;
628            Ok(())
629        })
630    }
631
632    /// Writes the provided byte array slice as an Ion blob.
633    fn write_blob<A: AsRef<[u8]>>(&mut self, value: A) -> IonResult<()> {
634        self.write_scalar(|output| {
635            // Rust format strings escape curly braces by doubling them. The following string is:
636            // * The opening {{ from a text Ion blob, with each brace doubled to escape it.
637            // * A {} pair used by the format string to indicate where the base64-encoded bytes
638            //   should be inserted.
639            // * The closing }} from a text Ion blob, with each brace doubled to escape it.
640            write!(output, "{{{{{}}}}}", base64::encode(value))?;
641            Ok(())
642        })
643    }
644
645    /// Begins a container (List, S-Expression, or Struct). If `ion_type` is not a container type,
646    /// `step_in` will return an Err(IllegalOperation).
647    fn step_in(&mut self, ion_type: IonType) -> IonResult<()> {
648        use IonType::*;
649
650        self.write_space_before_value()?;
651        self.write_value_metadata()?;
652        let container_type = match ion_type {
653            Struct => {
654                write!(self.output, "{{")?;
655                ContainerType::Struct
656            }
657            List => {
658                write!(self.output, "[")?;
659                ContainerType::List
660            }
661            SExp => {
662                write!(self.output, "(")?;
663                ContainerType::SExpression
664            }
665            _ => return illegal_operation(format!("Cannot step into a(n) {ion_type:?}")),
666        };
667        self.containers.push(EncodingLevel {
668            container_type,
669            child_count: 0usize,
670        });
671
672        Ok(())
673    }
674
675    /// Sets the current field name to `name`. If the TextWriter is currently positioned inside
676    /// of a struct, the field name will be written before the next value. Otherwise, it will be
677    /// ignored.
678    fn set_field_name<A: AsRawSymbolTokenRef>(&mut self, name: A) {
679        let token = match name.as_raw_symbol_token_ref() {
680            RawSymbolTokenRef::SymbolId(sid) => RawSymbolToken::SymbolId(sid),
681            RawSymbolTokenRef::Text(text) => RawSymbolToken::Text(text.to_string()),
682        };
683        self.field_name = Some(token);
684    }
685
686    fn parent_type(&self) -> Option<IonType> {
687        match self.parent_level().container_type {
688            ContainerType::TopLevel => None,
689            ContainerType::List => Some(IonType::List),
690            ContainerType::SExpression => Some(IonType::SExp),
691            ContainerType::Struct => Some(IonType::Struct),
692        }
693    }
694
695    fn depth(&self) -> usize {
696        self.containers.len() - 1
697    }
698
699    /// Completes the current container. If the TextWriter is not currently positioned inside a
700    /// container, `step_out` will return an Err(IllegalOperation).
701    fn step_out(&mut self) -> IonResult<()> {
702        use ContainerType::*;
703        let end_delimiter = match self.parent_level().container_type {
704            Struct => "}",
705            List => "]",
706            SExpression => ")",
707            TopLevel => return illegal_operation("cannot step out of the top level"),
708        };
709        // Wait to pop() the encoding level until after we've confirmed it wasn't TopLevel
710        let popped_encoding_level = self.containers.pop().unwrap();
711        if popped_encoding_level.child_count > 0 {
712            // If this isn't an empty container, put the closing delimiter on the next line
713            // with proper indentation.
714            if self
715                .whitespace_config
716                .space_between_nested_values
717                .contains(['\n', '\r'])
718            {
719                writeln!(&mut self.output)?;
720                for _ in 0..self.depth() {
721                    write!(&mut self.output, "{}", self.whitespace_config.indentation)?;
722                }
723            }
724        }
725        write!(self.output, "{end_delimiter}")?;
726        self.increment_child_count();
727        Ok(())
728    }
729
730    fn flush(&mut self) -> IonResult<()> {
731        self.output.flush()?;
732        Ok(())
733    }
734
735    fn output(&self) -> &W {
736        self.output.get_ref()
737    }
738
739    fn output_mut(&mut self) -> &mut W {
740        self.output.get_mut()
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use std::str;
747    use std::str::FromStr;
748
749    use bigdecimal::BigDecimal;
750    use chrono::{FixedOffset, NaiveDate, TimeZone};
751
752    use crate::result::IonResult;
753    use crate::text::raw_text_writer::{RawTextWriter, RawTextWriterBuilder};
754    use crate::types::Timestamp;
755    use crate::writer::IonWriter;
756    use crate::IonType;
757
758    fn writer_test_with_builder<F>(builder: RawTextWriterBuilder, mut commands: F, expected: &str)
759    where
760        F: FnMut(&mut RawTextWriter<&mut Vec<u8>>) -> IonResult<()>,
761    {
762        let mut output = Vec::new();
763        let mut writer = builder
764            .build(&mut output)
765            .expect("could not create RawTextWriter");
766        commands(&mut writer).expect("Invalid TextWriter test commands.");
767        writer.flush().expect("Call to flush() failed.");
768        assert_eq!(
769            str::from_utf8(writer.output().as_slice()).unwrap(),
770            expected
771        );
772    }
773
774    /// Constructs a [RawTextWriter] using [RawTextReaderBuilder::default], passes it to the
775    /// provided `commands` closure, and then verifies that its output matches `expected_default`.
776    /// Then, constructs a [RawTextWriter] using [RawTextReaderBuilder::pretty], passes it to the
777    /// provided `commands` closure, and then verifies that its output matches `expected_pretty`.
778    /// Finally, constructs a [RawTextWriter] using [RawTextReaderBuilder::lines], passes it to the
779    /// provided `commands` closure, and then verifies that its output matches `expected_lines`.
780    fn writer_test<F>(
781        mut commands: F,
782        expected_default: &str,
783        expected_pretty: &str,
784        expected_lines: &str,
785    ) where
786        F: Fn(&mut RawTextWriter<&mut Vec<u8>>) -> IonResult<()>,
787    {
788        writer_test_with_builder(
789            RawTextWriterBuilder::default(),
790            &mut commands,
791            expected_default,
792        );
793        writer_test_with_builder(
794            RawTextWriterBuilder::pretty(),
795            &mut commands,
796            expected_pretty,
797        );
798        writer_test_with_builder(RawTextWriterBuilder::lines(), commands, expected_lines)
799    }
800
801    /// When writing a scalar value, there shouldn't be any difference in output between the
802    /// `default`, `pretty`, and `lines` text writers. This function simply calls `writer_test_with_builder`
803    /// above using the same expected text for all cases.
804    fn write_scalar_test<F>(mut commands: F, expected: &str)
805    where
806        F: Fn(&mut RawTextWriter<&mut Vec<u8>>) -> IonResult<()>,
807    {
808        writer_test_with_builder(RawTextWriterBuilder::default(), &mut commands, expected);
809        writer_test_with_builder(RawTextWriterBuilder::pretty(), &mut commands, expected);
810        writer_test_with_builder(RawTextWriterBuilder::lines(), commands, expected)
811    }
812
813    #[test]
814    fn write_null_null() {
815        write_scalar_test(|w| w.write_null(IonType::Null), "null");
816    }
817
818    #[test]
819    fn write_null_string() {
820        write_scalar_test(|w| w.write_null(IonType::String), "null.string");
821    }
822
823    #[test]
824    fn write_bool_true() {
825        write_scalar_test(|w| w.write_bool(true), "true");
826    }
827
828    #[test]
829    fn write_bool_false() {
830        write_scalar_test(|w| w.write_bool(false), "false");
831    }
832
833    #[test]
834    fn write_i64() {
835        write_scalar_test(|w| w.write_i64(7), "7");
836    }
837
838    #[test]
839    fn write_f32() {
840        write_scalar_test(|w| w.write_f32(700f32), "7e2");
841    }
842
843    #[test]
844    fn write_f64() {
845        write_scalar_test(|w| w.write_f64(700f64), "7e2");
846    }
847
848    #[test]
849    fn write_annotated_i64() {
850        write_scalar_test(
851            |w| {
852                w.set_annotations(["foo", "bar", "baz quux"]);
853                w.write_i64(7)
854            },
855            "foo::bar::'baz quux'::7",
856        );
857    }
858
859    #[test]
860    fn write_decimal() {
861        let decimal_text = "731221.9948";
862        write_scalar_test(
863            |w| w.write_big_decimal(&BigDecimal::from_str(decimal_text).unwrap()),
864            decimal_text,
865        );
866    }
867
868    #[test]
869    fn write_datetime_epoch() {
870        #![allow(deprecated)] // `write_datetime` is deprecated
871        let naive_datetime =
872            NaiveDate::from_ymd(2000_i32, 1_u32, 1_u32).and_hms(0_u32, 0_u32, 0_u32);
873        let offset = FixedOffset::west(0);
874        let datetime = offset.from_utc_datetime(&naive_datetime);
875        write_scalar_test(|w| w.write_datetime(&datetime), "2000-01-01T00:00:00+00:00");
876    }
877
878    #[test]
879    fn write_timestamp_with_year() {
880        let timestamp = Timestamp::with_year(2000)
881            .build()
882            .expect("building timestamp failed");
883        write_scalar_test(|w| w.write_timestamp(&timestamp), "2000T");
884    }
885
886    #[test]
887    fn write_timestamp_with_month() {
888        let timestamp = Timestamp::with_year(2000)
889            .with_month(8)
890            .build()
891            .expect("building timestamp failed");
892        write_scalar_test(|w| w.write_timestamp(&timestamp), "2000-08T");
893    }
894
895    #[test]
896    fn write_timestamp_with_ymd() {
897        let timestamp = Timestamp::with_ymd(2000, 8, 22)
898            .build()
899            .expect("building timestamp failed");
900        write_scalar_test(|w| w.write_timestamp(&timestamp), "2000-08-22T");
901    }
902
903    #[test]
904    fn write_timestamp_with_ymd_hms() {
905        let timestamp = Timestamp::with_ymd(2000, 8, 22)
906            .with_hms(15, 45, 11)
907            .build_at_offset(2 * 60)
908            .expect("building timestamp failed");
909        write_scalar_test(
910            |w| w.write_timestamp(&timestamp),
911            "2000-08-22T15:45:11+02:00",
912        );
913    }
914
915    #[test]
916    fn write_timestamp_with_ymd_hms_millis() {
917        let timestamp = Timestamp::with_ymd_hms_millis(2000, 8, 22, 15, 45, 11, 931)
918            .build_at_offset(-5 * 60)
919            .expect("building timestamp failed");
920        write_scalar_test(
921            |w| w.write_timestamp(&timestamp),
922            "2000-08-22T15:45:11.931-05:00",
923        );
924    }
925
926    #[test]
927    fn write_timestamp_with_ymd_hms_millis_unknown_offset() {
928        let timestamp = Timestamp::with_ymd_hms_millis(2000, 8, 22, 15, 45, 11, 931)
929            .build_at_unknown_offset()
930            .expect("building timestamp failed");
931        write_scalar_test(
932            |w| w.write_timestamp(&timestamp),
933            "2000-08-22T15:45:11.931-00:00",
934        );
935    }
936
937    #[test]
938    fn write_blob() {
939        write_scalar_test(|w| w.write_blob("hello".as_bytes()), "{{aGVsbG8=}}");
940    }
941
942    #[test]
943    fn write_clob() {
944        write_scalar_test(|w| w.write_clob("a\"\'\n".as_bytes()), "{{\"a\\\"'\\n\"}}");
945        write_scalar_test(
946            |w| w.write_clob("❤️".as_bytes()),
947            "{{\"\\xe2\\x9d\\xa4\\xef\\xb8\\x8f\"}}",
948        );
949        write_scalar_test(
950            |w| w.write_clob("hello world".as_bytes()),
951            "{{\"hello world\"}}",
952        );
953    }
954
955    #[test]
956    fn with_space_between_top_level_values() {
957        writer_test_with_builder(
958            RawTextWriterBuilder::default().with_space_between_top_level_values("  "),
959            |w| {
960                w.write_bool(true)?;
961                w.write_bool(false)
962            },
963            "true  false",
964        );
965    }
966
967    #[test]
968    fn with_space_between_nested_values() {
969        writer_test_with_builder(
970            RawTextWriterBuilder::default().with_space_between_nested_values("  "),
971            |w| {
972                w.write_bool(true)?;
973                w.step_in(IonType::List)?;
974                w.write_string("foo")?;
975                w.write_i64(21)?;
976                w.write_symbol("bar")?;
977                w.step_out()
978            },
979            "true [\"foo\",  21,  bar]", // extra spaces between nested values only
980        );
981    }
982
983    #[test]
984    fn with_indentation() {
985        writer_test_with_builder(
986            RawTextWriterBuilder::pretty().with_indentation(" "),
987            |w| {
988                w.step_in(IonType::List)?;
989                w.write_string("foo")?;
990                w.write_i64(21)?;
991                w.write_symbol("bar")?;
992                w.step_out()
993            },
994            "[\n \"foo\",\n 21,\n bar\n]", // single space indentation differs from pretty()
995        );
996    }
997
998    #[test]
999    fn with_space_after_field_name() {
1000        writer_test_with_builder(
1001            RawTextWriterBuilder::default().with_space_after_field_name("   "),
1002            |w| {
1003                w.step_in(IonType::Struct)?;
1004                w.set_field_name("a");
1005                w.write_string("foo")?;
1006                w.step_out()
1007            },
1008            "{a:   \"foo\"}",
1009        );
1010    }
1011
1012    #[test]
1013    fn with_space_after_container_start() {
1014        writer_test_with_builder(
1015            RawTextWriterBuilder::default().with_space_after_container_start("   "),
1016            |w| {
1017                w.step_in(IonType::Struct)?;
1018                w.set_field_name("a");
1019                w.write_string("foo")?;
1020                w.step_out()
1021            },
1022            "{   a: \"foo\"}",
1023        );
1024    }
1025
1026    #[test]
1027    fn write_stream() {
1028        writer_test(
1029            |w| {
1030                w.write_string("foo")?;
1031                w.write_i64(21)?;
1032                w.write_symbol("bar")
1033            },
1034            "\"foo\" 21 bar",
1035            "\"foo\"\n21\nbar",
1036            "\"foo\"\n21\nbar",
1037        );
1038    }
1039
1040    #[test]
1041    fn write_list() {
1042        writer_test(
1043            |w| {
1044                w.step_in(IonType::List)?;
1045                w.write_string("foo")?;
1046                w.write_i64(21)?;
1047                w.write_symbol("bar")?;
1048                w.step_out()
1049            },
1050            "[\"foo\", 21, bar]",
1051            "[\n  \"foo\",\n  21,\n  bar\n]",
1052            "[\"foo\", 21, bar]",
1053        );
1054    }
1055
1056    #[test]
1057    fn write_nested_list() {
1058        writer_test(
1059            |w| {
1060                w.step_in(IonType::List)?;
1061                w.write_string("foo")?;
1062                w.write_i64(21)?;
1063                w.step_in(IonType::List)?;
1064                w.write_symbol("bar")?;
1065                w.step_out()?;
1066                w.step_out()
1067            },
1068            "[\"foo\", 21, [bar]]",
1069            "[\n  \"foo\",\n  21,\n  [\n    bar\n  ]\n]",
1070            "[\"foo\", 21, [bar]]",
1071        );
1072    }
1073
1074    #[test]
1075    fn write_s_expression() {
1076        writer_test(
1077            |w| {
1078                w.step_in(IonType::SExp)?;
1079                w.write_string("foo")?;
1080                w.write_i64(21)?;
1081                w.write_symbol("bar")?;
1082                w.step_out()
1083            },
1084            "(\"foo\" 21 bar)",
1085            "(\n  \"foo\"\n  21\n  bar\n)",
1086            "(\"foo\" 21 bar)",
1087        );
1088    }
1089
1090    #[test]
1091    fn write_struct() {
1092        writer_test(
1093            |w| {
1094                w.step_in(IonType::Struct)?;
1095                w.set_field_name("a");
1096                w.write_string("foo")?;
1097                w.set_field_name("b");
1098                w.write_i64(21)?;
1099                w.set_field_name("c");
1100                w.set_annotations(["quux"]);
1101                w.write_symbol("bar")?;
1102                w.step_out()
1103            },
1104            "{a: \"foo\", b: 21, c: quux::bar}",
1105            "{\n  a: \"foo\",\n  b: 21,\n  c: quux::bar\n}",
1106            "{a: \"foo\", b: 21, c: quux::bar}",
1107        );
1108    }
1109}