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