Skip to main content

ephemeris_documents/
format.rs

1//! The output format of a structured document, its capabilities and its
2//! dispatch to a backend.
3
4use std::fmt;
5
6use facet::Facet;
7
8use crate::annotation::Annotated;
9use crate::backend::Backend;
10use crate::columns::Columns;
11use crate::error::{ColumnsError, DocumentError, Feature, ValueError};
12use crate::json::{Dialect, Json};
13use crate::ownership::{ManagedPath, Ownership};
14use crate::source::Source;
15use crate::toml::Toml;
16use crate::yaml::{LiteralBlock, Yaml};
17
18/// Output syntax for a structured document.
19#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
20#[facet(tag = "type", deny_unknown_fields)]
21#[repr(u8)]
22pub enum Format {
23    /// JSON.
24    #[facet(rename = "json")]
25    Json,
26    /// JSON5, with unquoted keys, comments, and literal strings.
27    #[facet(rename = "json5")]
28    Json5,
29    /// TOML.
30    #[facet(rename = "toml")]
31    Toml {
32        /// Column limit for rendering a table as an inline table.
33        inline_width: InlineWidth,
34    },
35    /// YAML.
36    #[facet(rename = "yaml")]
37    Yaml,
38}
39
40/// An operation that runs against the backend of a format.
41trait Run {
42    type Output;
43
44    fn run(self, backend: &impl Backend) -> Self::Output;
45}
46
47struct Reconcile<'a> {
48    desired: &'a Annotated,
49    source: &'a Source<'a>,
50    ownership: &'a Ownership,
51}
52
53impl Run for Reconcile<'_> {
54    type Output = Result<Vec<u8>, DocumentError>;
55
56    fn run(self, backend: &impl Backend) -> Self::Output {
57        backend.reconcile(self.desired, self.source, self.ownership)
58    }
59}
60
61struct ReadString<'a> {
62    source: &'a str,
63    path: &'a [String],
64}
65
66impl Run for ReadString<'_> {
67    type Output = Result<Option<String>, DocumentError>;
68
69    fn run(self, backend: &impl Backend) -> Self::Output {
70        backend.read_string(self.source, self.path)
71    }
72}
73
74impl Format {
75    /// The format of a file with this extension, without the dot. TOML needs
76    /// the inline width the caller wants.
77    #[must_use]
78    pub fn from_extension(extension: &str, inline_width: InlineWidth) -> Option<Self> {
79        match extension {
80            "toml" => Some(Self::Toml { inline_width }),
81            "yaml" | "yml" => Some(Self::Yaml),
82            "json" => Some(Self::Json),
83            "json5" => Some(Self::Json5),
84            _ => None,
85        }
86    }
87
88    fn run<R: Run>(self, operation: R) -> R::Output {
89        match self {
90            Self::Toml { inline_width } => operation.run(&Toml::new(inline_width)),
91            Self::Yaml => operation.run(&Yaml),
92            Self::Json => operation.run(&Json::new(Dialect::Json)),
93            Self::Json5 => operation.run(&Json::new(Dialect::Json5)),
94        }
95    }
96
97    /// Reconcile a desired value against the current source.
98    ///
99    /// # Errors
100    /// Returns an error for malformed source, values the format cannot
101    /// represent, or an incompatible merge.
102    pub(crate) fn reconcile(
103        self,
104        desired: &Annotated,
105        source: &Source<'_>,
106        ownership: &Ownership,
107    ) -> Result<Vec<u8>, DocumentError> {
108        self.run(Reconcile {
109            desired,
110            source,
111            ownership,
112        })
113    }
114
115    /// The string at `path` in `source`. A key that is absent on the way
116    /// gives `None`.
117    ///
118    /// # Errors
119    /// Returns an error for malformed source, and
120    /// [`ValueError::NotString`] when the value at `path` is not a string.
121    /// YAML returns the text of any scalar, including numbers and booleans.
122    pub fn read_string(
123        self,
124        source: &str,
125        path: &ManagedPath,
126    ) -> Result<Option<String>, DocumentError> {
127        self.run(ReadString {
128            source,
129            path: path.segments(),
130        })
131    }
132
133    /// Whether the format can express `feature` at all.
134    ///
135    /// A literal string can still fail when the format cannot reproduce its
136    /// text.
137    pub(crate) const fn supports(self, feature: Feature) -> bool {
138        match feature {
139            Feature::Null | Feature::LargeInteger => {
140                !matches!(self, Self::Toml { inline_width: _ })
141            }
142            Feature::InlineComment => matches!(self, Self::Json5 | Self::Yaml),
143            Feature::DottedTable => matches!(self, Self::Toml { inline_width: _ }),
144            Feature::Comment | Feature::LiteralString => !matches!(self, Self::Json),
145        }
146    }
147
148    /// Whether the format can write `text` as a literal string.
149    pub(crate) fn reproduces_literal(self, text: &str) -> bool {
150        match self {
151            Self::Yaml => LiteralBlock::new(text).is_representable(),
152            Self::Json | Self::Json5 | Self::Toml { inline_width: _ } => true,
153        }
154    }
155
156    /// The error for a `feature` this format cannot express.
157    pub(crate) const fn reject(self, feature: Feature) -> ValueError {
158        ValueError::Unsupported {
159            format: self,
160            feature,
161        }
162    }
163
164    /// Check that the format supports `feature`.
165    ///
166    /// # Errors
167    /// Returns [`ValueError::Unsupported`] when it does not.
168    pub(crate) const fn require(self, feature: Feature) -> Result<(), ValueError> {
169        if self.supports(feature) {
170            Ok(())
171        } else {
172            Err(self.reject(feature))
173        }
174    }
175}
176
177impl fmt::Display for Format {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.write_str(match self {
180            Self::Json => "JSON",
181            Self::Json5 => "JSON5",
182            Self::Toml { inline_width: _ } => "TOML",
183            Self::Yaml => "YAML",
184        })
185    }
186}
187
188/// Widest `key = { ... }` line, in columns, that stays an inline table.
189/// Wider tables are promoted to `[table]` sections.
190#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
191#[facet(proxy = usize)]
192pub struct InlineWidth(#[facet(opaque)] Columns);
193
194impl InlineWidth {
195    /// An inline width of `columns`, or `None` for zero.
196    #[must_use]
197    pub const fn new(columns: usize) -> Option<Self> {
198        match Columns::new(columns) {
199            Some(columns) => Some(Self(columns)),
200            None => None,
201        }
202    }
203
204    /// The column count.
205    #[must_use]
206    pub const fn get(self) -> usize {
207        self.0.get()
208    }
209}
210
211impl TryFrom<usize> for InlineWidth {
212    type Error = ColumnsError;
213
214    fn try_from(columns: usize) -> Result<Self, Self::Error> {
215        Columns::try_from(columns).map(Self)
216    }
217}
218
219impl From<&InlineWidth> for usize {
220    fn from(width: &InlineWidth) -> Self {
221        width.get()
222    }
223}
224
225#[cfg(test)]
226mod tests;