Schematic

derive(Config)

TODO

This crate was built specifically for moon, and many of the design decisions are based around that project and its needs. Because of that, this crate is quite opinionated and won't change heavily.

Usage

Define a struct and derive the Config trait. This struct represents the final state, after all layers have been merged.

```rust use schematic::Config;

[derive(Config)]

struct AppConfig { #[setting(default = 3000, env = "PORT")] port: usize,

#[setting(default = true)]
secure: bool,

#[setting(default = vec!["localhost".into()])]
allowed_hosts: Vec<String>,

} ```

Then load, parse, and validate the configuration from one or many sources. A source is either a file path, secure URL, or code block.

```rust use schematic::ConfigLoader;

let result = ConfigLoader::::yaml() .code("secure: false")? .file(pathtoconfig)? .url(urltoconfig)? .load()?;

result.config; result.layers; ```

Configuration

TODO

Partials

TODO

Nested

TODO

Contexts

TODO

Metadata

TODO

Serde support

By default the Config macro will apply #[serde(default, deny_unknown_fields, rename_all = "camelCase")] to the partial config. The default and deny_unknown_fields cannot be customized, as they ensure proper parsing and layer merging.

However, the rename_all field can be customized, and we also support the rename field, both via the top-level #[config] attribute.

```rust

[derive(Config)]

[config(rename = "ExampleConfig", renameall = "snakecase")]

struct Example { // ... } ```

The rename field will also update the metadata name.

Settings

Settings are the individual fields/members of a configuration struct, and can be annotated with the optional #[setting] attribute.

Default values

In schematic, there are 2 forms of default values:

This section will talk about the #[setting] attribute, and the supported default, default_str, and default_fn attribute fields.

The default attribute field is used for declaring primitive values, like numbers and booleans. It can also be used for array and tuple literals, as well as function (mainly for from()) and macros calls.

```rust

[derive(Config)]

struct AppConfig { #[setting(default = 3000)] port: usize,

#[setting(default = true)]
secure: bool,

#[setting(default = vec!["localhost".into()])]
allowed_hosts: Vec<String>,

} ```

For string literals, the default_str attribute field should be used instead.

```rust

[derive(Config)]

struct AppConfig { #[setting(default_str = "/")] base: String, } ```

And lastly, if you need more control or need to calculate a complex value, you can use the default_fn attribute field, which requires a path to a function to call.

This function receives the context as the first argument (use () or generics if you don't have context), and can return an optional value. If None is returned, the Default value will be used instead.

```rust fn findunusedport(ctx: &Context) -> Option { let port = do_find(); Some(port) }

[derive(Config)]

struct AppConfig { #[setting(defaultfn = findunused_port)] port: usize, } ```

Environment variables

Settings can also inherit values from environment variables via the env attribute field. When using this, variables take the highest precedence, and are merged as the last layer.

```rust

[derive(Config)]

struct AppConfig { #[setting(default = 3000, env = "PORT")] port: usize, } ```

We also support parsing environment variables into the required type. For example, the variable may be a comma separated list of values, or a JSON string.

The parse_env attribute field can be used, which requires a path to a function to handle the parsing, and receives the variable value as a single argument.

```rust

[derive(Config)]

struct AppConfig { #[setting(env = "ALLOWEDHOSTS", parseenv = schematic::env::splitcomma)] allowedhosts: Vec, } ```

We provide a handful of built-in parsing functions in the env module.

When defining a custom parse function, you should return an error with ConfigError::Message if parsing fails.

```rust use schematic::ConfigError;

pub fn customparse(var: String) -> Result { doparse().maperr(|e| ConfigError::Message(e.tostring())) } ```

Extendable

Configs can extend other configs, generating an accurate layer chain, via the extend attribute field. Extended configs can either be a file path (relative from the current config) or a secure URL. For example:

yaml extends: - "./another/file.yml" - "https://domain.com/some/other/file.yml"

When defining extend, we currently support 3 types of patterns. The first is with a single string, which only allows a single file to be extended.

```rust

[derive(Config)]

struct AppConfig { #[setting(extend, validate = schematic::validate::extends_string)] extends: Option, } ```

The second is with a list of strings, allowing multiple files to be extended. This is the YAML example above.

```rust

[derive(Config)]

struct AppConfig { #[setting(extend, validate = schematic::validate::extends_list)] extends: Option>, } ```

And lastly, supporting both a string or a list, using our built-in enum.

```rust

[derive(Config)]

struct AppConfig { #[setting(extend, validate = schematic::validate::extends_from)] extends: Option, } ```

We suggest making this field optional, so that extending is not required by consumers!

Merge strategies

A common requirement for configuration is to merge multiple sources/layers into a final result. By default schematic will replace the previous value with the next value if the next value is Some, but sometimes you want far more control, like shallow merging or deep merging collections.

This can be achieved with the merge attribute field, which requires a path to a function to call.

```rust

[derive(Config)]

struct AppConfig { #[setting(merge = schematic::merge::appendvec)] allowedhosts: Vec, } ```

We provide a handful of built-in merge functions in the merge module.

When defining a custom merge function, the previous and next values are passed as arguments, and the function must return an optional merged result. If None is provided, neither value will be used.

Here's an example of the merge function above.

```rust pub fn append_vec(mut prev: Vec, next: Vec) -> Option> { prev.extend(next);

Some(prev)

} ```

Validation rules

What kind of configuration crate would this be without built-in validation? As such, we support it as a first-class feature, with built-in validation rules provided by garde.

In schematic, validation does not happen as part of the serde parsing process, and instead happens after the final configuration has been merged. This means we only validate the end result, not partial values (which may be incorrect).

Validation can be applied on a per-setting basis with the validate attribute field, which requires a path to a function to call. Furthermore, some functions are factories which can be called to produce a validator.

```rust

[derive(Config)]

struct AppConfig { #[setting(validate = schematic::validate::alphanumeric)] secret_key: String,

#[setting(validate = schematic::validate::regex("^\.env"))]
env_file: String,

} ```

We provide a handful of built-in validation functions in the validate module.

When defining a custom validate function, the value to check is passed as the first argument, the current struct as the second, and the context as the third. The ValidateError type must be used for failures.

```rust use schematic::ValidateError;

fn validatestring( value: &str, config: &AppConfig, context: &Context ) -> Result<(), ValidateError> { if !docheck(value) { return Err(ValidateError::new("Some failure message")); }

Ok(())

} ```

If validating an item in a vector or collection, you can specifiy the nested path when erroring. This is extremely useful when building error messages.

```rust use schematic::Segment;

ValidateError::withsegments( "Some failure message", // [i].key [Segment::Index(i), Segment::Key(key.tostring())] ) ```

Serde support

The rename and skip attribute fields are currently supported and will apply a #[serde] attribute to the partial setting.

```rust

[derive(Config)]

struct Example { #[setting(rename = "type")] type_of: SomeEnum, } ```

Features

The following Cargo features are available:

Parsing

Validation