Skip to main content

ephemeris_documents/
annotation.rs

1//! Inline annotations embedded in structured document values.
2//!
3//! An object with `_ephemeris: "annotated"` is a marker carrying `value` and
4//! optional `order`, `string`, `comment`, and `dotted` fields. `order` is only
5//! valid on object entries and `dotted` only on objects.
6//! [`Annotated::try_from`] decodes a [`Value`] tree once into typed scalars,
7//! comments, and object entries already sorted by `order`, then by key.
8//!
9//! Detection is shape-based: an unrelated object that matches the marker shape
10//! is consumed as one, and a near-miss with an unknown field is rejected.
11
12mod validate;
13
14use facet::Facet;
15use facet_value::{DestructuredRef, VObject, Value};
16
17use crate::entries::Entries;
18use crate::error::{AnnotationError, DocumentError, MarkerField, TextError, ValueError};
19use crate::header::GENERATED;
20use crate::path::Segment;
21use crate::scalar::Scalar;
22use crate::text::Comment;
23
24const MARKER_KEY: &str = "_ephemeris";
25const MARKER_VALUE: &str = "annotated";
26
27/// String rendering style requested by an annotation.
28#[derive(Debug, Clone, Copy, Facet, PartialEq, Eq)]
29#[repr(u8)]
30pub(crate) enum StringStyle {
31    /// Render the string using the format's default style.
32    #[facet(rename = "plain")]
33    Plain,
34    /// Render the string as a literal block, preserving line breaks. JSON5
35    /// writes single quotes and still escapes line breaks.
36    #[facet(rename = "literal")]
37    Literal,
38}
39
40#[derive(Debug, Facet)]
41#[repr(u8)]
42enum MarkerTag {
43    #[facet(rename = "annotated")]
44    Annotated,
45}
46
47/// The marker fields other than `value`, which is read borrowed.
48#[derive(Debug, Facet)]
49#[facet(deny_unknown_fields)]
50struct Marker {
51    _ephemeris: MarkerTag,
52    #[facet(default)]
53    order: Option<i64>,
54    #[facet(default)]
55    string: Option<StringStyle>,
56    #[facet(default)]
57    comment: Option<String>,
58    #[facet(default)]
59    dotted: Option<bool>,
60}
61
62/// A value decorated with an optional comment.
63#[derive(Debug, Clone)]
64pub(crate) struct Annotated {
65    /// Comment attached to this value by the author.
66    pub(crate) comment: Option<Comment>,
67    /// The annotated value itself.
68    pub(crate) value: AnnotatedValue,
69}
70
71impl Annotated {
72    /// Put the generated marker in front of the comment of this value.
73    pub(crate) fn mark_generated(&mut self) -> Result<(), TextError> {
74        let text = self.comment.as_ref().map_or_else(
75            || GENERATED.to_owned(),
76            |existing| format!("{GENERATED}\n{}", &**existing),
77        );
78        self.comment = Some(Comment::new(text)?);
79        Ok(())
80    }
81
82    /// Whether this value or any value below it has a comment.
83    pub(crate) fn carries_comment(&self) -> bool {
84        self.comment.is_some()
85            || match &self.value {
86                AnnotatedValue::Scalar(_) => false,
87                AnnotatedValue::Array(items) => items.iter().any(Self::carries_comment),
88                AnnotatedValue::Object(entries) => entries.carries_comment(),
89            }
90    }
91}
92
93/// The shape of an annotated value once markers have been decoded away.
94#[derive(Debug, Clone)]
95pub(crate) enum AnnotatedValue {
96    /// A null, boolean, number, or string leaf.
97    Scalar(Scalar),
98    /// An ordered sequence of annotated values.
99    Array(Vec<Annotated>),
100    /// An object with annotated entries, in render order.
101    Object(Entries),
102}
103
104impl TryFrom<&Value> for Annotated {
105    type Error = DocumentError;
106
107    /// Decode a generic structured value into its annotation tree.
108    ///
109    /// # Errors
110    /// Returns an error for malformed annotation markers, fields set more
111    /// than once, a string style on a non string value, non finite numbers,
112    /// and value kinds without a scalar counterpart.
113    fn try_from(value: &Value) -> Result<Self, Self::Error> {
114        Decoded::decode(value)?.into_ranked()?.into_unordered()
115    }
116}
117
118/// A decoded value and the order it requests among its siblings.
119struct Ranked {
120    order: Option<i64>,
121    annotated: Annotated,
122}
123
124impl Ranked {
125    fn into_unordered(self) -> Result<Annotated, DocumentError> {
126        match self.order {
127            Some(_) => Err(DocumentError::annotation(
128                AnnotationError::OrderOutsideObject,
129            )),
130            None => Ok(self.annotated),
131        }
132    }
133}
134
135/// A decoded value whose marker fields have not been applied yet, so that
136/// composed markers can still be merged.
137struct Decoded {
138    comment: Option<Comment>,
139    order: Option<i64>,
140    style: Option<StringStyle>,
141    dotted: Option<bool>,
142    value: AnnotatedValue,
143}
144
145impl Decoded {
146    const fn plain(value: AnnotatedValue) -> Self {
147        Self {
148            comment: None,
149            order: None,
150            style: None,
151            dotted: None,
152            value,
153        }
154    }
155
156    fn decode(value: &Value) -> Result<Self, DocumentError> {
157        #[expect(
158            clippy::wildcard_enum_match_arm,
159            reason = "DestructuredRef is a foreign, growable enum from facet-value"
160        )]
161        let node = match value.destructure_ref() {
162            DestructuredRef::Object(object) if is_marker(object) => return Self::marker(object),
163            DestructuredRef::Object(object) => {
164                let entries = object
165                    .iter()
166                    .map(|(key, entry)| {
167                        let ranked = Self::decode(entry)
168                            .and_then(Self::into_ranked)
169                            .map_err(|error| error.under(Segment::Key(key.as_str().to_owned())))?;
170                        Ok((ranked.order, key.as_str().to_owned(), ranked.annotated))
171                    })
172                    .collect::<Result<_, DocumentError>>()?;
173                AnnotatedValue::Object(Entries::sorted(entries))
174            }
175            DestructuredRef::Array(values) => {
176                let items = values
177                    .iter()
178                    .enumerate()
179                    .map(|(index, item)| {
180                        Self::decode(item)
181                            .and_then(Self::into_ranked)
182                            .and_then(Ranked::into_unordered)
183                            .map_err(|error| error.under(Segment::Index(index)))
184                    })
185                    .collect::<Result<_, DocumentError>>()?;
186                AnnotatedValue::Array(items)
187            }
188            DestructuredRef::Null => AnnotatedValue::Scalar(Scalar::Null),
189            DestructuredRef::Bool(value) => AnnotatedValue::Scalar(Scalar::Bool(value)),
190            DestructuredRef::Number(number) => {
191                AnnotatedValue::Scalar(Scalar::number(number).map_err(DocumentError::value)?)
192            }
193            DestructuredRef::String(text) => AnnotatedValue::Scalar(Scalar::String {
194                text: text.as_str().to_owned(),
195                style: StringStyle::Plain,
196            }),
197            _ => return Err(DocumentError::value(ValueError::UnsupportedKind)),
198        };
199        Ok(Self::plain(node))
200    }
201
202    fn marker(object: &VObject) -> Result<Self, DocumentError> {
203        let mut fields = VObject::new();
204        for (key, field) in object {
205            if key.as_str() != "value" {
206                fields.insert(key.as_str(), field.clone());
207            }
208        }
209        let marker = facet_value::from_value::<Marker>(Value::from(fields)).map_err(|source| {
210            DocumentError::annotation(AnnotationError::Marker {
211                source: Box::new(source),
212            })
213        })?;
214        let inner = object
215            .get("value")
216            .ok_or_else(|| DocumentError::annotation(AnnotationError::MissingValue))?;
217        let mut node = Self::decode(inner)?;
218        MarkerField::Order
219            .set_once(&mut node.order, marker.order)
220            .map_err(DocumentError::annotation)?;
221        MarkerField::String
222            .set_once(&mut node.style, marker.string)
223            .map_err(DocumentError::annotation)?;
224        MarkerField::Dotted
225            .set_once(&mut node.dotted, marker.dotted)
226            .map_err(DocumentError::annotation)?;
227        MarkerField::Comment
228            .set_once(
229                &mut node.comment,
230                marker.comment.map(Comment::new).transpose()?,
231            )
232            .map_err(DocumentError::annotation)?;
233        Ok(node)
234    }
235
236    fn into_ranked(self) -> Result<Ranked, DocumentError> {
237        let value = match self.dotted {
238            Some(dotted) => Self::dotted(dotted, self.value)?,
239            None => self.value,
240        };
241        let value = match (self.style, value) {
242            (Some(style), AnnotatedValue::Scalar(Scalar::String { text, style: _ })) => {
243                AnnotatedValue::Scalar(Scalar::String { text, style })
244            }
245            (
246                Some(_),
247                AnnotatedValue::Scalar(_) | AnnotatedValue::Array(_) | AnnotatedValue::Object(_),
248            ) => {
249                return Err(DocumentError::annotation(AnnotationError::StyleOnNonString));
250            }
251            (None, value) => value,
252        };
253        Ok(Ranked {
254            order: self.order,
255            annotated: Annotated {
256                comment: self.comment,
257                value,
258            },
259        })
260    }
261}
262
263impl Decoded {
264    fn dotted(dotted: bool, value: AnnotatedValue) -> Result<AnnotatedValue, DocumentError> {
265        match value {
266            AnnotatedValue::Object(mut entries) => {
267                entries.set_dotted(dotted);
268                Ok(AnnotatedValue::Object(entries))
269            }
270            AnnotatedValue::Scalar(_) | AnnotatedValue::Array(_) => Err(DocumentError::annotation(
271                AnnotationError::DottedOnNonObject,
272            )),
273        }
274    }
275}
276
277impl MarkerField {
278    fn set_once<T>(self, slot: &mut Option<T>, value: Option<T>) -> Result<(), AnnotationError> {
279        match (slot.is_some(), value) {
280            (true, Some(_)) => Err(AnnotationError::FieldSetTwice { field: self }),
281            (false, Some(value)) => {
282                *slot = Some(value);
283                Ok(())
284            }
285            (_, None) => Ok(()),
286        }
287    }
288}
289
290fn is_marker(object: &VObject) -> bool {
291    matches!(
292        object.get(MARKER_KEY).map(Value::destructure_ref),
293        Some(DestructuredRef::String(marker)) if marker.as_str() == MARKER_VALUE
294    )
295}
296
297#[cfg(test)]
298mod tests;