Skip to main content

nickel_lang_parser/ast/
record.rs

1use super::{
2    Annotation, Ast, AstAlloc, MergePriority, TraverseAlloc, TraverseControl, TraverseOrder,
3};
4
5use crate::{
6    identifier::{Ident, LocIdent},
7    position::TermPos,
8};
9
10use indexmap::IndexMap;
11
12/// Element of a record field path in a record field definition. For example, in  `{ a."%{"hello-"
13/// ++ "world"}".c = true }`, the path `a."%{b}".c` is composed of three elements: an identifier
14/// `a`, an expression `"hello" ++ "world"`, and another identifier `c`.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FieldPathElem<'ast> {
17    /// A statically known identifier.
18    Ident(LocIdent),
19    /// A dynamic field name written as a quoted expression, e.g. `"%{protocol}" = .. `. Normally,
20    /// the expression must be a [crate::ast::Node::StringChunks], so we could store the
21    /// chunks directly which would be more precise. However, it's useful to keep a general
22    /// [crate::ast::Ast] to store errors when part of the field path failed to parse
23    /// correctly.
24    Expr(Ast<'ast>),
25}
26
27impl<'ast> FieldPathElem<'ast> {
28    /// Build a field path element from an expression. Automatically convert an expression that is
29    /// actually a static identifier to [Self::Ident].
30    pub fn expr(expr: Ast<'ast>) -> Self {
31        if let Some(id) = expr.node.try_str_chunk_as_static_str() {
32            FieldPathElem::Ident(LocIdent::from(id).with_pos(expr.pos))
33        } else {
34            FieldPathElem::Expr(expr)
35        }
36    }
37
38    /// Returns the position of the field path element.
39    pub fn pos(&self) -> TermPos {
40        match self {
41            FieldPathElem::Ident(ident) => ident.pos,
42            FieldPathElem::Expr(expr) => expr.pos,
43        }
44    }
45
46    /// Crate a path composed of a single static identifier.
47    pub fn single_ident_path(
48        alloc: &'ast AstAlloc,
49        ident: LocIdent,
50    ) -> &'ast [FieldPathElem<'ast>] {
51        alloc.alloc_singleton(FieldPathElem::Ident(ident))
52    }
53
54    /// Create a path composed of a single dynamic expression.
55    pub fn single_expr_path(alloc: &'ast AstAlloc, expr: Ast<'ast>) -> &'ast [FieldPathElem<'ast>] {
56        alloc.alloc_singleton(FieldPathElem::Expr(expr))
57    }
58
59    /// Try to interpret this element as a static identifier. Returns `None` if the element
60    /// is an expression with interpolation inside. Dual of [Self::try_as_dyn_expr].
61    ///
62    /// We assume that the parser has already normalized expressions that are actually static
63    /// identifiers. So this method is a simple selector over [Self::Ident].
64    pub fn try_as_ident(&self) -> Option<LocIdent> {
65        if let FieldPathElem::Ident(ident) = self {
66            Some(*ident)
67        } else {
68            None
69        }
70    }
71
72    /// Tries to interpret this element as a dynamic identifier. Returns `None` if the element is a
73    /// static identifier (that is, if [Self::try_as_ident] returns `Some(_)`).
74    pub fn try_as_dyn_expr(&self) -> Option<&Ast<'ast>> {
75        if let FieldPathElem::Expr(expr) = self {
76            Some(expr)
77        } else {
78            None
79        }
80    }
81}
82
83/// A field definition. A field is defined by a dot-separated path of identifier or interpolated
84/// strings, a potential value, and associated metadata.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct FieldDef<'ast> {
87    /// A sequence of field path elements, composing the left hand side (with respect to the `=`)
88    /// of the field definition.
89    ///
90    /// # Invariants
91    ///
92    /// **Important**: The path must be non-empty, or some of `FieldDef` methods will panic.
93    pub path: &'ast [FieldPathElem<'ast>],
94    /// The metadata and the optional value bundled as a field.
95    pub metadata: FieldMetadata<'ast>,
96    pub value: Option<Ast<'ast>>,
97    /// The position of the whole field definition.
98    pub pos: TermPos,
99}
100
101impl FieldDef<'_> {
102    /// Returns the identifier corresponding to this definition if the path is composed of exactly
103    /// one element which is a static identifier. Returns `None` otherwise.
104    pub fn path_as_ident(&self) -> Option<LocIdent> {
105        if let [elem] = self.path {
106            elem.try_as_ident()
107        } else {
108            None
109        }
110    }
111
112    /// Returns the declared field name, that is the last element of the path, as a static
113    /// identifier. Returns `None` if the last element is an expression.
114    pub fn name_as_ident(&self) -> Option<LocIdent> {
115        self.path.last().expect("empty field path").try_as_ident()
116    }
117
118    /// Returns the root identifier of the field path, that is the first element of the path, as a
119    /// static identifier. Returns `None` if the first element is an expression.
120    pub fn root_as_ident(&self) -> Option<LocIdent> {
121        self.path.first().expect("empty field path").try_as_ident()
122    }
123}
124
125/// The metadata attached to record fields.
126#[derive(Debug, PartialEq, Eq, Clone, Default)]
127pub struct FieldMetadata<'ast> {
128    /// The documentation of the field.
129    pub doc: Option<&'ast str>,
130    /// Type and contract annotations.
131    pub annotation: Annotation<'ast>,
132    /// If the field is optional.
133    pub opt: bool,
134    /// If the field should be skipped during serialization.
135    pub not_exported: bool,
136    /// The merge priority.
137    pub priority: MergePriority,
138}
139
140impl FieldMetadata<'_> {
141    pub fn new() -> Self {
142        Default::default()
143    }
144
145    pub fn is_empty(&self) -> bool {
146        self.doc.is_none()
147            && self.annotation.is_empty()
148            && !self.opt
149            && !self.not_exported
150            && matches!(self.priority, MergePriority::Neutral)
151    }
152}
153
154impl<'ast> From<Annotation<'ast>> for FieldMetadata<'ast> {
155    fn from(annotation: Annotation<'ast>) -> Self {
156        FieldMetadata {
157            annotation,
158            ..Default::default()
159        }
160    }
161}
162
163/// An include expression.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct Include<'ast> {
166    /// The included identifier.
167    pub ident: LocIdent,
168    /// The field metadata.
169    pub metadata: FieldMetadata<'ast>,
170}
171
172/// A nickel record literal.
173#[derive(Clone, Debug, Default, PartialEq, Eq)]
174pub struct Record<'ast> {
175    /// `include` expressions.
176    pub includes: &'ast [Include<'ast>],
177    /// Field definitions.
178    pub field_defs: &'ast [FieldDef<'ast>],
179    /// If the record is open, i.e. if it ended with `..`.
180    pub open: bool,
181}
182
183impl<'ast> Record<'ast> {
184    /// A record with no fields and the default set of attributes.
185    pub fn empty() -> Self {
186        Default::default()
187    }
188
189    /// Returns self with the open flag set to true.
190    pub fn open(self) -> Self {
191        Record { open: true, ..self }
192    }
193
194    /// Returns `false` if at least one field in the first layer of the record (that is the first
195    /// element of each field path) is defined dynamically, and `true` otherwise.
196    pub fn has_static_structure(&self) -> bool {
197        self.field_defs
198            .iter()
199            .all(|field| field.path.iter().any(|elem| elem.try_as_ident().is_some()))
200    }
201
202    /// Returns the top-level dynamically defined fields of this record.
203    pub fn toplvl_dyn_fields(&self) -> Vec<&Ast<'ast>> {
204        self.field_defs
205            .iter()
206            .filter_map(|field| field.path.first()?.try_as_dyn_expr())
207            .collect()
208    }
209
210    /// Returns all the pieces that define the field with the given identifier. This requires to
211    /// make a linear search over this record.
212    pub fn defs_of(&self, ident: Ident) -> impl Iterator<Item = &'ast FieldDef<'ast>> {
213        self.field_defs.iter().filter(move |field| {
214            field
215                .path
216                .first()
217                .and_then(FieldPathElem::try_as_ident)
218                .is_some_and(|i| i.ident() == ident)
219        })
220    }
221
222    /// Returns an iterator over all field definitions, grouped by the first identifier of their
223    /// paths (that is, the field which they are defining). Field that aren't statically defined
224    /// (i.e. whose path's first element isn't an ident) are ignored.
225    pub fn group_by_field_id(&self) -> IndexMap<Ident, Vec<&FieldDef<'ast>>> {
226        let mut map = IndexMap::new();
227
228        for (id, field) in self.field_defs.iter().filter_map(|field| {
229            field
230                .path
231                .first()
232                .and_then(FieldPathElem::try_as_ident)
233                .map(|i| (i, field))
234        }) {
235            map.entry(id.ident()).or_insert_with(Vec::new).push(field);
236        }
237
238        map
239    }
240}
241
242impl<'ast> TraverseAlloc<'ast, Ast<'ast>> for FieldDef<'ast> {
243    fn traverse<F, E>(
244        self,
245        alloc: &'ast AstAlloc,
246        f: &mut F,
247        order: TraverseOrder,
248    ) -> Result<Self, E>
249    where
250        F: FnMut(Ast<'ast>) -> Result<Ast<'ast>, E>,
251    {
252        let path: Result<Vec<_>, E> = self
253            .path
254            .iter()
255            .map(|elem| match elem {
256                FieldPathElem::Ident(ident) => Ok(FieldPathElem::Ident(*ident)),
257                FieldPathElem::Expr(expr) => expr
258                    .clone()
259                    .traverse(alloc, f, order)
260                    .map(FieldPathElem::Expr),
261            })
262            .collect();
263
264        let metadata = FieldMetadata {
265            annotation: self.metadata.annotation.traverse(alloc, f, order)?,
266            ..self.metadata
267        };
268
269        let value = self
270            .value
271            .map(|v| v.traverse(alloc, f, order))
272            .transpose()?;
273
274        Ok(FieldDef {
275            path: alloc.alloc_many(path?),
276            metadata,
277            value,
278            pos: self.pos,
279        })
280    }
281
282    fn traverse_ref<S, U>(
283        &'ast self,
284        f: &mut dyn FnMut(&'ast Ast<'ast>, &S) -> TraverseControl<S, U>,
285        scope: &S,
286    ) -> Option<U> {
287        self.path
288            .iter()
289            .find_map(|elem| match elem {
290                FieldPathElem::Ident(_) => None,
291                FieldPathElem::Expr(expr) => expr.traverse_ref(f, scope),
292            })
293            .or_else(|| self.metadata.annotation.traverse_ref(f, scope))
294            .or_else(|| self.value.as_ref().and_then(|v| v.traverse_ref(f, scope)))
295    }
296}