cbor-edn 0.0.11

Converter and processor for CBOR Diagnostic Notation (EDN)
Documentation
use super::{DelimiterPolicy, Transformable};

/// A builder-style transformation of CBOR or EDN data.
///
/// A transformation encodes changes that can be applied to a [`Transformable`] such as a
/// [`StandaloneItem`][crate::StandaloneItem] or a [`Sequence`][crate::Sequence].
///
/// While methods on those and the [`application`][crate::application] module provide fine
/// granularity and arbitrary operations, many use cases do not require the finest control -- and
/// applying changes manually can lead to suboptimal results if not done in the right sequence.
///
/// Note that not all transformations have an effect for all input and output formats (any of
/// CBOR-to-EDN, EDN-to-CBOR, EDN-to-EDN and CBOR-to-CBOR). For example, operations that influence
/// indentation or comments have no practical effect when the transformation's output is encoded to
/// CBOR, and operations that decode application-oriented literals have no effect when the input is
/// decoded from CBOR.
#[derive(Debug, Copy, Clone)]
pub struct Transformation {
    /// Policy applied to input
    early_delimiter_policy: Option<DelimiterPolicy>,
    /// Policy applied after application-oriented literals have been created
    late_delimiter_policy: Option<DelimiterPolicy>,
    tag_to_aol: bool,
    from_999: bool,
    aol_to_item: bool,
    to_999: bool,
    bignum_tag_to_edn_integer: bool,
    known_structure: Option<KnownStructure>,
    bytestring_heuristics: bool,
}

impl Transformation {
    /// Creates a transformation builder that applies no changes.
    pub const fn new() -> Self {
        Transformation {
            early_delimiter_policy: None,
            late_delimiter_policy: None,
            tag_to_aol: false,
            from_999: false,
            aol_to_item: false,
            to_999: false,
            bignum_tag_to_edn_integer: false,
            known_structure: None,
            bytestring_heuristics: false,
        }
    }

    pub fn apply_to<'a>(&self, item: &mut impl Transformable<'a>) {
        // This should get us to notice whenever there's a new parameter to process
        let &Transformation {
            early_delimiter_policy,
            late_delimiter_policy,
            tag_to_aol,
            from_999,
            aol_to_item,
            to_999,
            bignum_tag_to_edn_integer,
            known_structure,
            bytestring_heuristics,
        } = self;

        if let Some(indent) = early_delimiter_policy {
            item.set_delimiters(indent);
        }

        // Running this early because later to-AoL steps should produce something suitable
        // themselves.
        if bytestring_heuristics {
            crate::string::apply_bytestring_heuristincs(item);
        }

        if from_999 {
            item.visit_tag(&mut crate::application::tag999_to_aol);
        }

        #[allow(clippy::single_match)] // reason: expecting to add more
        match known_structure {
            // 601 is UCCS, but that's just because it's toplevel; we have it as a CBOR item, so
            // locally it is unprotected, but more generally, it is a CCS.
            Some(KnownStructure::UnwrappedTag(601)) => {
                for item in item.toplevel_items() {
                    // FIXME: How do we best propagate a "that's not a map" error?
                    // For what it's worth, do we even expect that there is more than one or no
                    // CCS item, and if so, what does that mean for errors?
                    let _ = item.visit_map_elements(&mut crate::application::comment_ccs);
                }
            }
            Some(KnownStructure::CoseHeaderMap) => {
                for item in item.toplevel_items() {
                    // FIXME: like above
                    let _ = item.visit_map_elements(&mut crate::application::comment_cose_header);
                }
            }
            Some(KnownStructure::UnwrappedTag(_)) | None => {}
        }

        if tag_to_aol {
            item.visit_tag(&mut crate::application::dt_tag_to_aol);
            item.visit_tag(&mut crate::application::ip_tag_to_aol);
            item.visit_tag(&mut crate::application::comment_lang_tag);
        }

        if bignum_tag_to_edn_integer {
            item.visit_tag(&mut crate::application::bignum_tag_to_edn_integer);
        }

        if aol_to_item {
            item.visit_application_literals(&mut crate::application::dt_aol_to_item);
            item.visit_application_literals(&mut crate::application::ip_aol_to_item);
        }

        if to_999 {
            item.visit_application_literals(&mut crate::application::any_aol_to_tag999);
        }

        if let Some(indent) = late_delimiter_policy {
            item.set_delimiters(indent);
        }
    }

    // FIXME: how much do we have to do manually so this can be used also with items?
}

/// # Groups of transformations transformations
impl Transformation {
    /// Applies any transformation that making the EDN well-readable for humans.
    ///
    /// Currently, this applies indentation and processes all known tags to produce corresponding
    /// application-oriented literals (or, in case of tags 2 and 3, to EDN numbers).
    pub const fn pretty(self) -> Self {
        self.indent()
            .tag_to_aol()
            .bignum_tag_to_edn_integer()
            .bytestring_heuristics()
    }

    /// Applies the minimal transformations that are necessary for expressing general EDN as CBOR.
    ///
    /// Currently, this is an alias for [`Self::aol_to_item()`] (and the author lacks the
    /// imagination on whether this would ever need other transformations; the one that comes to
    /// mind is converting big integers from EDN to CBOR, but that currently happens automatically
    /// at serialization time).
    pub const fn for_cbor_serialization(self) -> Self {
        self.aol_to_item()
    }
}

/// # Single transformations
impl Transformation {
    /// Removes all comments and spaces from the EDN.
    pub const fn minify(self) -> Self {
        Transformation {
            early_delimiter_policy: Some(DelimiterPolicy::DiscardAll),
            ..self
        }
    }

    /// Applies some default indentation to the EDN.
    ///
    /// Output will have a final newline if the general structure is multiline.
    pub const fn indent(self) -> Self {
        Transformation {
            late_delimiter_policy: Some(DelimiterPolicy::indented()),
            ..self
        }
    }

    /// Applies some default indentation to the EDN.
    ///
    /// Output will have a final newline unconditionally
    pub const fn indent_with_final_newline(self) -> Self {
        Transformation {
            late_delimiter_policy: Some(DelimiterPolicy::indented_with_final_newline()),
            ..self
        }
    }

    /// Recognizes Tag 999 and turns it into application-oriented literals.
    ///
    /// While this is mostly useful to obtain pretty EDN from an application that, knowing its
    /// domain CBOR, prepared tags for pretty-printing, it can also interact with other options
    /// that recognize application-oriented literals.
    pub const fn from_999(self) -> Self {
        Transformation {
            from_999: true,
            ..self
        }
    }

    /// Produces Tag 999 for all application-oriented literals that are not converted to items in
    /// other steps.
    ///
    /// This is applied last, after any other transformation option that process
    /// application-oriented literals.
    pub const fn to_999(self) -> Self {
        Transformation {
            to_999: true,
            ..self
        }
    }

    /// Recognizes tags to produce application-oriented literals or annotations inside the tag.
    pub const fn tag_to_aol(self) -> Self {
        Transformation {
            tag_to_aol: true,
            ..self
        }
    }

    pub const fn bignum_tag_to_edn_integer(self) -> Self {
        Transformation {
            bignum_tag_to_edn_integer: true,
            ..self
        }
    }

    /// Turns recognized application-oriented literals into their CBOR items.
    pub const fn aol_to_item(self) -> Self {
        Transformation {
            aol_to_item: true,
            ..self
        }
    }

    /// Annotates (by turning into an AOL, adding comments or processing nested items) the
    /// top-level structure .
    pub fn annotate_unwrapped_tag(self, tag_number: u64) -> Self {
        Transformation {
            known_structure: Some(KnownStructure::UnwrappedTag(tag_number)),
            ..self
        }
    }

    pub fn annotate_cose_header_map(self) -> Self {
        Transformation {
            known_structure: Some(KnownStructure::CoseHeaderMap),
            ..self
        }
    }

    /// Decides `'abc'` and `h'616263'` based on its content
    pub const fn bytestring_heuristics(self) -> Self {
        Transformation {
            bytestring_heuristics: true,
            ..self
        }
    }
}

impl Default for Transformation {
    fn default() -> Self {
        Self::new()
    }
}

/// A structure known of an item that can be used for better annotation.
#[derive(Copy, Clone, Debug)]
enum KnownStructure {
    /// The structure follows the content of a tag given by number.
    UnwrappedTag(u64),
    // Not yet:
    //
    // ContentFormat(u16)
    // MediaType(...)
    CoseHeaderMap,
}