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}