Skip to main content

ephemeris_documents/
spec.rs

1//! The wire shape of a document and its dispatch to a payload type.
2
3mod structured;
4mod text;
5
6use facet::Facet;
7
8pub use self::structured::StructuredSpec;
9pub use self::text::TextSpec;
10use crate::error::DocumentError;
11use crate::format::Format;
12use crate::line_ending::LineEndingSpec;
13use crate::markdown::MarkdownSpec;
14use crate::ownership::{DocumentOwnership, ManagedPath};
15use crate::source::Source;
16
17/// A complete desired document or a source-preserving partial document.
18#[derive(Debug, Clone, Facet)]
19#[facet(tag = "type", deny_unknown_fields)]
20#[repr(u8)]
21pub enum DocumentSpec {
22    /// A plain text document, rendered node by node.
23    #[facet(rename = "text")]
24    Text(TextSpec),
25    /// A structured (JSON, JSON5, TOML, or YAML) document.
26    #[facet(rename = "structured")]
27    Structured(StructuredSpec),
28    /// A Markdown document.
29    #[facet(rename = "markdown")]
30    Markdown(MarkdownSpec),
31}
32
33/// What every kind of document declares and does.
34pub(crate) trait Payload {
35    /// The line ending of the written document.
36    fn line_ending(&self) -> LineEndingSpec;
37
38    /// Reconcile the document against its current source.
39    ///
40    /// # Errors
41    /// Returns an error for malformed source, values the document cannot
42    /// represent, or an incompatible merge.
43    fn reconcile(&self, source: &Source<'_>) -> Result<Vec<u8>, DocumentError>;
44
45    /// Check the declaration without reading any source.
46    ///
47    /// # Errors
48    /// Returns an error for invalid ownership paths, for comment nodes that
49    /// cannot be rendered and for invalid Markdown selectors.
50    fn validate(&self) -> Result<(), DocumentError>;
51}
52
53impl DocumentSpec {
54    /// A structured document that sets the string at `key` to `value` and
55    /// leaves the rest of the source alone. It keeps the line ending of the
56    /// source.
57    #[must_use]
58    pub fn set_string(format: Format, key: &ManagedPath, value: &str) -> Self {
59        Self::Structured(StructuredSpec {
60            format,
61            value: key.nest(value.into()),
62            ownership: DocumentOwnership::selected([key.clone()]),
63            line_ending: LineEndingSpec::Preserve,
64        })
65    }
66
67    fn payload(&self) -> &dyn Payload {
68        match self {
69            Self::Text(spec) => spec,
70            Self::Structured(spec) => spec,
71            Self::Markdown(spec) => spec,
72        }
73    }
74
75    /// Whether this document leaves any part of its source user-owned.
76    #[must_use]
77    pub const fn is_partial(&self) -> bool {
78        match self {
79            Self::Text(_) => false,
80            Self::Structured(spec) => spec.is_partial(),
81            Self::Markdown(spec) => spec.is_partial(),
82        }
83    }
84
85    /// Check the document's declaration without reading any source.
86    ///
87    /// # Errors
88    /// Returns an error for invalid ownership paths, for comment nodes that
89    /// cannot be rendered and for invalid Markdown selectors.
90    pub fn validate(&self) -> Result<(), DocumentError> {
91        self.payload().validate()
92    }
93
94    /// Reconcile one desired document against its current source bytes.
95    ///
96    /// # Errors
97    /// Returns an error for invalid AST, unsupported partial ownership,
98    /// malformed source, or a current source whose line endings cannot be
99    /// resolved.
100    pub fn reconcile(&self, current: Option<&[u8]>) -> Result<Vec<u8>, DocumentError> {
101        let payload = self.payload();
102        let source = Source::read(current, payload.line_ending(), self.is_partial())?;
103        payload.reconcile(&source)
104    }
105}
106
107#[cfg(test)]
108mod tests;