diskplan_schema/lib.rs
1//! This crate provides the means to constuct a tree of [SchemaNode]s from text form (see
2//! [parse_schema]).
3//!
4//! The language of the text form uses significant whitespace (four spaces) for indentation,
5//! distinguishes between files and directories by the presence of a `/`, and whether
6//! this is a symlink by presence of an `->` (followed by its target path expression).
7//! That is, each indented node of the directory tree takes one of the following forms:
8//!
9//! | Syntax | Description
10//! |-----------------------|---------------------------
11//! | _str_ | A file
12//! | _str_`/` | A directory
13//! | _str_ `->` _expr_ | A symlink to a file
14//! | _str_/ `->` _expr_ | A symlink to a directory
15//!
16//! Properties of a given node are set using the following tags:
17//!
18//! | Tag | Types | Description
19//! |---------------------------|-----------|---------------------------
20//! |`:owner` _expr_ | All | Sets the owner of this file/directory/symlink target
21//! |`:group` _expr_ | All | Sets the group of this file, directory or symlink target
22//! |`:mode` _octal_ | All | Sets the permissions of this file/directory/symlink target
23//! |`:source` _expr_ | File | Copies content into this file from the path given by _expr_
24//! |`:let` _ident_ `=` _expr_ | Directory | Sets a variable at this level to be used by deeper levels
25//! |`:def` _ident_ | Directory | Defines a sub-schema that can be reused by `:use`
26//! |`:use` _ident_ | Directory | Reuses a sub-schema defined by `:def`
27//!
28//!
29//! # Simple Schema
30//!
31//! The top level of a schema describes a directory, whose [attributes][Attributes] may be set by `:owner`, `:group` and `:mode` tags:
32//! ```
33//! use diskplan_schema::*;
34//!
35//! let schema_root = parse_schema("
36//! :owner person
37//! :group user
38//! :mode 777
39//! ")?;
40//!
41//! assert!(matches!(schema_root.schema, SchemaType::Directory(_)));
42//! assert_eq!(schema_root.attributes.owner.unwrap(), "person");
43//! assert_eq!(schema_root.attributes.group.unwrap(), "user");
44//! assert_eq!(schema_root.attributes.mode.unwrap(), 0o777);
45//! # Ok::<(), anyhow::Error>(())
46//! ```
47//!
48//! A [DirectorySchema] may contain sub-directories and files:
49//! ```
50//! # use diskplan_schema::*;
51//! #
52//! // ...
53//! # let text =
54//! "
55//! subdirectory/
56//! :owner admin
57//! :mode 700
58//!
59//! file_name
60//! :source content/example_file
61//! "
62//! # ;
63//! // ...
64//! assert_eq!(
65//! parse_schema(text)?
66//! .schema
67//! .as_directory()
68//! .expect("Not a directory")
69//! .entries()
70//! .len(),
71//! 2
72//! );
73//! # Ok::<(), anyhow::Error>(())
74//! ```
75//!
76//! It may also contain symlinks to directories and files, whose own schemas will apply to the
77//! target:
78//!
79//! ```
80//! # use diskplan_schema::*;
81//! #
82//! // ...
83//! # let text =
84//! "
85//! example_link/ -> /another/disk/example_target/
86//! :owner admin
87//! :mode 700
88//!
89//! file_to_create_at_target_end
90//! :source content/example_file
91//! "
92//! # ;
93//! // ...
94//! # match parse_schema(text)?.schema {
95//! # SchemaType::Directory(directory) => {
96//! #
97//! let (binding, node) = directory.entries().first().unwrap();
98//! assert!(matches!(
99//! binding,
100//! Binding::Static(ref name) if name == &String::from("example_link")
101//! ));
102//! assert_eq!(
103//! node.symlink.as_ref().unwrap().to_string(),
104//! String::from("/another/disk/example_target/")
105//! );
106//! assert!(matches!(node.schema, SchemaType::Directory(_)));
107//! #
108//! # }
109//! # _ => panic!("Expected directory schema")
110//! # }
111//! #
112//! # Ok::<(), anyhow::Error>(())
113//! ```
114//!
115//! ## Variable Substitution
116//!
117//! Variables can be used to drive construction, for example:
118//! ```
119//! # diskplan_schema::parse_schema(
120//! "
121//! :let asset_type = character
122//! :let asset_name = Monkey
123//!
124//! assets/
125//! $asset_type/
126//! $asset/
127//! reference/
128//! "
129//! # ).unwrap();
130//! ```
131//!
132//! Variables will also pick up on names already on disk (even if a `:let` provides a different
133//! value). For example, if we had `assets/prop/Banana` on disk already, `$asset_type` would match
134//! against and take the value "prop" (as well as "character") and `$asset` would take the value
135//! "Banana" (as well as "Monkey"), producing:
136//! ```text
137//! assets
138//! ├── character
139//! │ └── Monkey
140//! │ └── reference
141//! └── prop
142//! └── Banana
143//! └── reference
144//! ```
145//!
146//! ## Pattern Matching
147//!
148//! Any node of the schema can have a `:match` tag, which, via a Regular Expression, controls the
149//! possible values a variable can take.
150//!
151//! **IMPORTANT:** _No two variables can match the same value_. If they do, an error will occur during
152//! execution, so be careful to ensure there is no overlap between patterns. The use of `:avoid`
153//! can help restrict the pattern matching and ensure proper partitioning.
154//!
155//! Static names (without variables) always take precedence and do not need to be unique with
156//! respect to variable patterns (and vice versa).
157//!
158//! For example, this is legal in the schema but will always error in practice:
159//! ```text
160//! $first/
161//! $second/
162//! ```
163//! For instance, when operating on the path `/test`, it yields:
164//! ```text
165//! Error: "test" matches multiple dynamic bindings "$first" and "$second" (Any)
166//! ```
167//!
168//! A working example might be:
169//! ```text
170//! $first/
171//! :match [A-Z].*
172//! $second/
173//! :match [^A-Z].*
174//! ```
175//!
176//! ## Schema Reuse
177//!
178//! Portions of a schema can be built from reusable definitions.
179//!
180//! A definition is formed using the `:def` keyword, followed by its name and a body like any
181//! other schema node:
182//! ```text
183//! :def reusable/
184//! anything_inside/
185//! ```
186//! It is used by adding the `:use` tag inside any other (same or deeper level) node:
187//! ```text
188//! reused_here/
189//! :use reusable
190//! ```
191//! Multiple `:use` tags may be used. Attributes are resolved in the following order:
192//! ```text
193//! example/
194//! ## Attributes set here win (before or after any :use lines)
195//! :owner root
196//!
197//! ## First :use is next in precedence
198//! :use one
199//!
200//! ## Subsequent :use lines take lower precedence
201//! :use two
202//! ```
203#![warn(missing_docs)]
204
205use std::{collections::HashMap, fmt::Display};
206
207mod attributes;
208pub use attributes::Attributes;
209
210mod expression;
211pub use expression::{Expression, Identifier, Special, Token};
212
213mod text;
214pub use text::{parse_schema, ParseError};
215
216/// A node in an abstract directory hierarchy
217#[derive(Debug, Clone, PartialEq)]
218pub struct SchemaNode<'t> {
219 /// A reference to the line in the text representation where this node was defined
220 pub line: &'t str,
221
222 /// Condition against which to match file/directory names
223 pub match_pattern: Option<Expression<'t>>,
224
225 /// Condition against which file/directory names must not match
226 pub avoid_pattern: Option<Expression<'t>>,
227
228 /// Symlink target - if this produces a symbolic link. Operates on the target end.
229 pub symlink: Option<Expression<'t>>,
230
231 /// Links to other schemas `:use`d by this one (found in parent [`DirectorySchema`] definitions)
232 pub uses: Vec<Identifier<'t>>,
233
234 /// Properties of this file/directory
235 pub attributes: Attributes<'t>,
236
237 /// Properties specific to the underlying (file or directory) type
238 pub schema: SchemaType<'t>,
239}
240
241impl<'t> std::fmt::Display for SchemaNode<'t> {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 write!(f, "Schema node \"{}\"", self.line)?;
244 if let Some(ref match_pattern) = self.match_pattern {
245 write!(f, ", matching \"{match_pattern}\"")?;
246 }
247 if let Some(ref avoid_pattern) = self.avoid_pattern {
248 write!(f, ", avoiding \"{avoid_pattern}\"")?;
249 }
250
251 match &self.schema {
252 SchemaType::Directory(ds) => {
253 let len = ds.entries().len();
254 write!(
255 f,
256 " (directory with {} entr{})",
257 len,
258 if len == 1 { "y" } else { "ies" }
259 )?
260 }
261 SchemaType::File(fs) => write!(f, " (file from source: {})", fs.source())?,
262 }
263 Ok(())
264 }
265}
266
267/// File/directory specific aspects of a node in the tree
268#[derive(Debug, Clone, PartialEq)]
269pub enum SchemaType<'t> {
270 /// Indicates that this node describes a directory
271 Directory(DirectorySchema<'t>),
272 /// Indicates that this node describes a file
273 File(FileSchema<'t>),
274}
275
276impl<'t> SchemaType<'t> {
277 /// Returns the inner [`DirectorySchema`] if this node is a directory node
278 pub fn as_directory(&self) -> Option<&DirectorySchema<'t>> {
279 match self {
280 SchemaType::Directory(directory) => Some(directory),
281 _ => None,
282 }
283 }
284
285 /// Returns the inner [`FileSchema`] if this node is a file node
286 pub fn as_file(&self) -> Option<&FileSchema<'t>> {
287 match self {
288 SchemaType::File(file) => Some(file),
289 _ => None,
290 }
291 }
292}
293
294/// A DirectorySchema is a container of variables, definitions (named schemas) and a directory listing
295#[derive(Debug, Default, Clone, PartialEq)]
296pub struct DirectorySchema<'t> {
297 /// Text replacement variables
298 vars: HashMap<Identifier<'t>, Expression<'t>>,
299
300 /// Definitions of sub-schemas
301 defs: HashMap<Identifier<'t>, SchemaNode<'t>>,
302
303 /// Disk entries to be created within this directory
304 entries: Vec<(Binding<'t>, SchemaNode<'t>)>,
305}
306
307impl<'t> DirectorySchema<'t> {
308 /// Constructs a new description of a directory in the schema
309 pub fn new(
310 vars: HashMap<Identifier<'t>, Expression<'t>>,
311 defs: HashMap<Identifier<'t>, SchemaNode<'t>>,
312 entries: Vec<(Binding<'t>, SchemaNode<'t>)>,
313 ) -> Self {
314 let mut entries = entries;
315 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
316 DirectorySchema {
317 vars,
318 defs,
319 entries,
320 }
321 }
322 /// Provides access to the variables defined in this node
323 pub fn vars(&self) -> &HashMap<Identifier<'t>, Expression<'t>> {
324 &self.vars
325 }
326 /// Returns the expression associated with the given variable, if any was set in the schema
327 pub fn get_var<'a>(&'a self, id: &Identifier<'a>) -> Option<&'a Expression<'t>> {
328 self.vars.get(id)
329 }
330
331 /// Provides access to the sub-schema definitions defined in this node
332 pub fn defs(&self) -> &HashMap<Identifier, SchemaNode> {
333 &self.defs
334 }
335 /// Returns the sub-schema associated with the given definition, if any was set in the schema
336 pub fn get_def<'a>(&'a self, id: &Identifier<'a>) -> Option<&'a SchemaNode<'t>> {
337 self.defs.get(id)
338 }
339
340 /// Provides access to the child nodes of this node, with their bindings
341 pub fn entries(&self) -> &[(Binding<'t>, SchemaNode<'t>)] {
342 &self.entries[..]
343 }
344}
345
346/// How an entry is bound in a schema, either to a static fixed name or to a variable
347#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
348pub enum Binding<'t> {
349 /// A static, fixed name
350 Static(&'t str), // Static is ordered first
351 /// A dynamic name bound to the given variable
352 Dynamic(Identifier<'t>),
353}
354
355impl Display for Binding<'_> {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 match self {
358 Binding::Static(s) => write!(f, "{s}"),
359 Binding::Dynamic(id) => write!(f, "${id}"),
360 }
361 }
362}
363
364/// A description of a file
365#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct FileSchema<'t> {
367 /// Path to the resource to be copied as file content
368 // TODO: Make source enum: Enforce(...), Default(...) latter only creates if missing
369 source: Expression<'t>,
370}
371
372impl<'t> FileSchema<'t> {
373 /// Constructs a new description of a file
374 pub fn new(source: Expression<'t>) -> Self {
375 FileSchema { source }
376 }
377 /// Returns the expression of the path from where the file will inherit its content
378 pub fn source(&self) -> &Expression<'t> {
379 &self.source
380 }
381}
382
383#[cfg(test)]
384mod tests;