Skip to main content

qframe/document/
shape.rs

1//! The shape of a document: the keys one table holds, what each key may hold, the tables that
2//! sit inside it and the keys that carry an array of tables.
3
4/// What one key may hold, in the loader's own terms.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub(crate) enum Kind {
7    /// Any text.
8    Text,
9    /// Text from a fixed list.
10    Choice(Vec<String>),
11    /// A whole number.
12    Integer,
13    /// `true` or `false`.
14    Flag,
15}
16
17impl Kind {
18    /// The expectation in words, for diagnostics.
19    pub(crate) fn describe(&self) -> String {
20        match self {
21            Self::Text => "a string".to_owned(),
22            Self::Choice(choices) => format!("one of {}", choices.join(", ")),
23            Self::Integer => "a whole number".to_owned(),
24            Self::Flag => "a boolean".to_owned(),
25        }
26    }
27}
28
29/// What one key of a [`Shape`] may hold, for [`Shape::required`] and [`Shape::optional`].
30///
31/// A plain value rather than one builder per kind (`required_text`, `optional_text`, …): the
32/// kinds stay listed once, and any kind can be required or optional.
33///
34/// ```
35/// use qframe::document::{Shape, ValueKind};
36///
37/// let shape = Shape::new()
38///     .required("name", ValueKind::text())
39///     .required("engine", ValueKind::choice(["podman", "docker"]))
40///     .optional("retries", ValueKind::integer())
41///     .optional("network", ValueKind::flag());
42/// ```
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ValueKind(pub(crate) Kind);
45
46impl ValueKind {
47    /// Any text.
48    #[must_use]
49    pub fn text() -> Self {
50        Self(Kind::Text)
51    }
52
53    /// One text of `choices`, e.g. the names an application knows.
54    #[must_use]
55    pub fn choice(choices: impl IntoIterator<Item = impl Into<String>>) -> Self {
56        Self(Kind::Choice(choices.into_iter().map(Into::into).collect()))
57    }
58
59    /// A whole number, in any base TOML writes.
60    #[must_use]
61    pub fn integer() -> Self {
62        Self(Kind::Integer)
63    }
64
65    /// `true` or `false`.
66    #[must_use]
67    pub fn flag() -> Self {
68        Self(Kind::Flag)
69    }
70}
71
72/// One declared key of a [`Shape`].
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub(crate) struct Key {
75    pub(crate) name: String,
76    pub(crate) kind: Kind,
77    /// Whether a document without the key is incomplete.
78    pub(crate) required: bool,
79}
80
81/// The shape of one table of a document: which keys it holds, which tables sit inside it and
82/// which of its keys carry an array of tables.
83///
84/// A [`Document`](super::Document) is read against the shape of its root table. Keys are single
85/// names, never dotted paths: nesting is declared with [`Shape::table`], which reads both
86/// `[mounts]` with `project` under it and the same key written as `mounts.project`.
87///
88/// ```
89/// use qframe::document::{Document, Shape, ValueKind};
90///
91/// let profile = Shape::new().required("name", ValueKind::text()).optional("added", ValueKind::text());
92/// let shape = Shape::new()
93///     .required("id", ValueKind::text())
94///     .optional("created", ValueKind::text())
95///     .table("mounts", Shape::new().optional("assets", ValueKind::choice(["rw", "ro"])))
96///     .entries("profile", profile);
97///
98/// let text = "id = \"api\"\n\n[mounts]\nassets = \"ro\"\n\n[[profile]]\nname = \"review\"\n";
99/// let document = Document::parse("project.qcode", text, &shape);
100/// assert!(document.is_clean());
101/// assert_eq!(document.root().text("id"), Some("api"));
102/// assert_eq!(document.root().table("mounts").and_then(|mounts| mounts.text("assets")), Some("ro"));
103/// assert_eq!(document.root().entries("profile").len(), 1);
104/// ```
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct Shape {
107    keys: Vec<Key>,
108    /// Tables that sit inside this one, each with its own shape.
109    tables: Vec<(String, Shape)>,
110    /// Keys carrying an array of tables, with the shape of one entry.
111    arrays: Vec<(String, Shape)>,
112}
113
114impl Shape {
115    /// A table that holds nothing yet. Every key it may hold is declared on it.
116    #[must_use]
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// A key the document must hold: a document without it, or with a value of another type, is
122    /// reported as an error. Declaring a name again replaces what it declared before.
123    #[must_use]
124    pub fn required(self, key: &str, kind: ValueKind) -> Self {
125        self.declare(key, kind, true)
126    }
127
128    /// A key the document may hold: it is read when it is there and of the declared type, a
129    /// value of another type is a warning, and a missing key is not a problem at all.
130    #[must_use]
131    pub fn optional(self, key: &str, kind: ValueKind) -> Self {
132        self.declare(key, kind, false)
133    }
134
135    /// A table inside this one, `[key]` with `shape` below it.
136    ///
137    /// The table itself is optional: a document without it is not a problem, and the keys of a
138    /// missing table read as missing. Its required keys are required once the table is there.
139    #[must_use]
140    pub fn table(mut self, key: &str, shape: Shape) -> Self {
141        self.forget(key);
142        self.tables.push((key.to_owned(), shape));
143        self
144    }
145
146    /// An array of tables, `[[key]]` repeated, each entry shaped by `shape`.
147    ///
148    /// The array itself is optional: a document that lists no entry simply has none. Each entry
149    /// is checked on its own, and an entry that is missing a required key is reported where it
150    /// starts, beside the entries that were read.
151    #[must_use]
152    pub fn entries(mut self, key: &str, shape: Shape) -> Self {
153        self.forget(key);
154        self.arrays.push((key.to_owned(), shape));
155        self
156    }
157
158    fn declare(mut self, key: &str, kind: ValueKind, required: bool) -> Self {
159        self.forget(key);
160        self.keys.push(Key { name: key.to_owned(), kind: kind.0, required });
161        self
162    }
163
164    /// Drops whatever `key` declared before, so one name means one thing.
165    fn forget(&mut self, key: &str) {
166        self.keys.retain(|declared| declared.name != key);
167        self.tables.retain(|(name, _)| name != key);
168        self.arrays.retain(|(name, _)| name != key);
169    }
170
171    /// The key declared as `name`, if the shape declares one.
172    pub(crate) fn key(&self, name: &str) -> Option<&Key> {
173        self.keys.iter().find(|key| key.name == name)
174    }
175
176    /// The shape of the table declared as `name`, if the shape holds one.
177    pub(crate) fn inner(&self, name: &str) -> Option<&Shape> {
178        self.tables.iter().find(|(key, _)| key == name).map(|(_, shape)| shape)
179    }
180
181    /// The shape of one entry of the array declared as `name`, if the shape holds one.
182    pub(crate) fn entry(&self, name: &str) -> Option<&Shape> {
183        self.arrays.iter().find(|(key, _)| key == name).map(|(_, shape)| shape)
184    }
185
186    /// The keys a document must hold, in the order they were declared.
187    pub(crate) fn required_keys(&self) -> impl Iterator<Item = &str> {
188        self.keys.iter().filter(|key| key.required).map(|key| key.name.as_str())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn kinds_describe_what_they_expect() {
198        assert_eq!(ValueKind::text().0.describe(), "a string");
199        assert_eq!(ValueKind::choice(["rw", "ro"]).0.describe(), "one of rw, ro");
200        assert_eq!(ValueKind::integer().0.describe(), "a whole number");
201        assert_eq!(ValueKind::flag().0.describe(), "a boolean");
202    }
203
204    #[test]
205    fn one_name_means_one_thing() {
206        let shape = Shape::new()
207            .required("profile", ValueKind::text())
208            .table("profile", Shape::new())
209            .entries("profile", Shape::new().required("name", ValueKind::text()));
210        assert!(shape.key("profile").is_none() && shape.inner("profile").is_none());
211        assert_eq!(shape.entry("profile").map(|entry| entry.required_keys().count()), Some(1));
212
213        let shape = shape.optional("profile", ValueKind::integer());
214        assert!(shape.entry("profile").is_none(), "the array is gone once the name is a key");
215        assert_eq!(shape.key("profile").map(|key| key.required), Some(false));
216        assert_eq!(shape.required_keys().count(), 0);
217    }
218
219    #[test]
220    fn declaring_a_key_again_replaces_it() {
221        let shape = Shape::new().optional("id", ValueKind::text()).required("id", ValueKind::text());
222        assert_eq!(shape.keys.len(), 1);
223        assert_eq!(shape.required_keys().collect::<Vec<_>>(), vec!["id"]);
224        assert_eq!(shape, Shape::new().required("id", ValueKind::text()));
225    }
226}