Skip to content

Commit 24d9835

Browse files
Merge pull request #40 from LukasKalbertodt/validation
Add Validation via `#[config(validate)]`
2 parents ed402c4 + 4b6a8fd commit 24d9835

File tree

11 files changed

+1221
-251
lines changed

11 files changed

+1221
-251
lines changed

Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,19 @@ exclude = [".github"]
1919
name = "simple"
2020
required-features = ["toml"]
2121

22+
[[example]]
23+
name = "validate"
24+
required-features = ["toml"]
25+
2226
[[test]]
2327
name = "indirect-serde"
2428
path = "tests/indirect-serde/run.rs"
2529
harness = false
2630

31+
[[test]]
32+
name = "validation"
33+
required-features = ["toml"]
34+
2735

2836
[features]
2937
default = ["toml", "yaml", "json5"]

examples/files/validate.toml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name = "peter"
2+
port = 1234
3+
4+
[watch]
5+
busy_poll = true
6+
poll_period = 300

examples/validate.rs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! This example demonstrates the usage of validators for single fields or whole
2+
//! structs. Try editing `files/validate.toml` to see different errors. Also
3+
//! see the docs.
4+
5+
use std::time::Duration;
6+
7+
use confique::Config;
8+
9+
10+
#[derive(Debug, Config)]
11+
#[allow(dead_code)]
12+
struct Conf {
13+
// Here, the validator is a function returning `Result<(), impl Display>`.
14+
#[config(validate = validate_name)]
15+
name: String,
16+
17+
// For simple cases, validation can be written in this `assert!`-like style.
18+
#[config(env = "PORT", validate(*port >= 1024, "port must not require super-user"))]
19+
port: Option<u16>,
20+
21+
#[config(nested)]
22+
watch: WatchConfig,
23+
}
24+
25+
// You can also add validators for whole structs, which are called later in the
26+
// pipeline, when all layers are already merged. These validators allow you to
27+
// check fields in relationship to one another, e.g. maybe one field only makes
28+
// sense to be set whenever another one has a specific value.
29+
#[derive(Debug, Config)]
30+
#[config(validate = Self::validate)]
31+
struct WatchConfig {
32+
#[config(default = false)]
33+
busy_poll: bool,
34+
35+
#[config(
36+
deserialize_with = deserialize_duration_ms,
37+
validate(*poll_period > Duration::from_millis(10), "cannot poll faster than 10ms"),
38+
)]
39+
poll_period: Option<Duration>,
40+
}
41+
42+
fn validate_name(name: &String) -> Result<(), &'static str> {
43+
if name.is_empty() {
44+
return Err("name must be non-empty");
45+
}
46+
if !name.is_ascii() {
47+
return Err("name must be ASCII");
48+
}
49+
Ok(())
50+
}
51+
52+
impl WatchConfig {
53+
fn validate(&self) -> Result<(), &'static str> {
54+
if !self.busy_poll && self.poll_period.is_some() {
55+
return Err("'poll_period' set, but busy polling is not enabled");
56+
}
57+
58+
Ok(())
59+
}
60+
}
61+
62+
63+
pub(crate) fn deserialize_duration_ms<'de, D>(deserializer: D) -> Result<Duration, D::Error>
64+
where
65+
D: serde::Deserializer<'de>,
66+
{
67+
let ms = <u64 as serde::Deserialize>::deserialize(deserializer)?;
68+
Ok(Duration::from_millis(ms))
69+
}
70+
71+
72+
fn main() {
73+
let r = Conf::builder()
74+
.env()
75+
.file("examples/files/validate.toml")
76+
.load();
77+
78+
match r {
79+
Ok(conf) => println!("{:#?}", conf),
80+
Err(e) => println!("{e:#}"),
81+
}
82+
}

0 commit comments

Comments
 (0)