Validation rules
Requires the
validateCargo feature, which is enabled by default.
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 the garde crate.
In Schematic, validation does not happen as part of the serde parsing process, and instead happens for each partial configuration to be merged. However, with that said, prefer serde parsing over validation rules for structural adherence (learn more).
Validation can be applied on a per-setting basis with the #[setting(validate)] attribute field,
which requires a reference to a function to call.
#![allow(unused)]
fn main() {
#[derive(Config)]
struct AppConfig {
#[setting(validate = schematic::validate::alphanumeric)]
pub secret_key: String,
#[setting(validate = schematic::validate::regex("^\.env"))]
pub env_file: String,
}
}
Or on a per-variant basis when using an enum.
#![allow(unused)]
fn main() {
#[derive(Config)]
enum Projects {
#[setting(validate = schematic::validate::min_length(1))]
List(Vec<String>),
// ...
}
}
A variant holding several values passes them as a tuple of references, not as a reference to a tuple.
#![allow(unused)]
fn main() {
fn validate_pair<P, C>(value: (&String, &usize), partial: &P, context: &C, finalize: bool) -> ValidateResult {
// ...
}
#[derive(Config)]
enum Entry {
#[setting(validate = validate_pair)]
Pair(String, usize),
// ...
}
}
We provide a handful of built-in validation functions in the
validatemodule. Furthermore, some functions are factories which can be called to produce a validator.
Required settings
Every setting in a partial configuration is optional, so a setting that no source
provides falls back to its default value, or to Default::default() when it has
none. To fail instead, as serde does for a missing field, mark the setting with
#[setting(required)].
#![allow(unused)]
fn main() {
#[derive(Config)]
struct AppConfig {
#[setting(required)]
pub name: String,
#[setting(required, env = "APP_TOKEN")]
pub token: String,
#[setting(required)]
pub port: Option<usize>,
}
}
A required setting has no default value, and is checked by the final validation pass, once every
source and environment variable has been merged. Any one of them may provide it, so each
source is free to omit it. A null counts as missing, while an empty value, like "" or [], does
not.
When the setting is wrapped in Option, the value must be Some, but the field stays an Option
in the final configuration. A setting that isn’t wrapped still has to implement Default, which
AppConfig::default() falls back to.
A required setting can’t declare a default, as it would always have a value. Nor can a
nested config that isn’t wrapped in Option, as one always exists in the final
configuration. Mark the settings within it as required instead, which are then checked even when no
source provided the nested config at all.
Validate handler function
You can also define your own function for validating values, also known as a validator.
When defining a custom validate function, the value to check is passed as the first argument, the current/parent partial as the second, the context as the third, and whether this is the final validation pass.
#![allow(unused)]
fn main() {
fn validate_string(
value: &str,
partial: &PartialAppConfig,
context: &Context
finalize: bool
) -> ValidateResult {
if !do_check(value) {
return Err(ValidateError::new("Some failure message"));
}
Ok(())
}
}
If validation fails, you must return a
ValidateError with a
failure message.
The setting is passed by reference, and deref coerces on its way in, so the first argument can take
a borrowed form of the type instead of the type itself. A String setting reaches a &str
validator, as above, and a Vec<T> setting reaches a &[T] one.
Factories
For composition and reusability concerns, we also support factory functions that can be called to
create a unique validator. This can be seen above with schematic::validate::regex. To create your
own factory, declare a normal function, with any number of arguments, that returns a
Validator.
Using the regex factory as an example, it would look something like this. The closure takes the
same four arguments a plain validator does.
#![allow(unused)]
fn main() {
use schematic::Validator;
fn regex<T, P, C>(pattern: &str) -> Validator<T, P, C> {
let pattern = regex::Regex::new(pattern).unwrap();
Box::new(move |value, _, _, _| {
if !pattern.is_match(value) {
return Err(ValidateError::new("Some failure message"));
}
Ok(())
})
}
}
Path targeting
If validating an item in a list or collection, you can specifiy the nested path when failing. This is extremely useful when building error messages.
#![allow(unused)]
fn main() {
use schematic::PathSegment;
ValidateError::with_segments(
"Some failure message",
// [i].key
[PathSegment::Index(i), PathSegment::Key(key.to_string())]
)
}
Context and partial handling
If you’re not using context, or want to create a validator for any kind of partial, we suggest generic inferrence.
#![allow(unused)]
fn main() {
fn using_generics<P, C>(value: &str, partial: &P, context: &C, finalize: bool) -> ValidateResult {
// ...
}
}
Cargo features
The following Cargo features can be enabled for more functionality:
validate_email- Enables email validation with theschematic::validate::emailfunction.validate_url- Enables URL validation with theschematic::validate::urlandurl_securefunctions.