#schema #directory #directory-tree #parser #symlink #set #node

diskplan-schema

A library component of Diskplan for defining and parsing schemas

1 unstable release

0.1.0 Oct 6, 2023

#34 in #symlink

40 downloads per month
Used in 3 crates (2 directly)

MIT license

63KB
1.5K SLoC

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);

A [DirectorySchema] may contain sub-directories and files:

#
// ...
"
    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
);

It may also contain symlinks to directories and files, whose own schemas will apply to the target:

#
// ...
"
    example_link/ -> /another/disk/example_target/
        :owner admin
        :mode 700

        file_to_create_at_target_end
            :source content/example_file
"
// ...
#
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(_)));
#
#

Variable Substitution

Variables can be used to drive construction, for example:

"
    :let asset_type = character
    :let asset_name = Monkey

    assets/
        $asset_type/
            $asset/
                reference/
"

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:

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:

$first/
$second/

For instance, when operating on the path /test, it yields:

Error: "test" matches multiple dynamic bindings "$first" and "$second" (Any)

A working example might be:

$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:

:def reusable/
    anything_inside/

It is used by adding the :use tag inside any other (same or deeper level) node:

reused_here/
    :use reusable

Multiple :use tags may be used. Attributes are resolved in the following order:

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

Dependencies

~1.5MB
~25K SLoC