Skip to content

Commit eabdc2b

Browse files
committed
feat: .ini support
1 parent 8d97f86 commit eabdc2b

9 files changed

Lines changed: 208 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

confik/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ default = ["env", "toml"]
2727
env = ["dep:envious"]
2828
json = ["dep:serde_json"]
2929
ron-0_12 = ["dep:ron-0_12"]
30+
serde_ini-0_2 = ["dep:serde_ini-0_2"]
3031
toml = ["dep:toml"]
3132
yaml_serde-0_10 = ["dep:yaml_serde-0_10"]
3233

@@ -62,6 +63,7 @@ thiserror = "2"
6263
# Source types
6364
envious = { version = "0.2", optional = true }
6465
ron-0_12 = { package = "ron", version = "0.12", optional = true }
66+
serde_ini-0_2 = { package = "serde_ini", version = "0.2", optional = true }
6567
serde_json = { version = "1", optional = true }
6668
toml = { version = "1", optional = true, default-features = false, features = ["parse", "serde"] }
6769
yaml_serde-0_10 = { package = "yaml_serde", version = "0.10", optional = true }

confik/src/lib.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ When the `tracing` feature is enabled, reload errors in the signal handler will
123123
A [`Source`] is any type that can create [`ConfigurationBuilder`]s. This crate implements the following sources:
124124

125125
- [`EnvSource`]: Loads configuration from environment variables using the [`envious`] crate. Requires the `env` feature. (Enabled by default.)
126-
- [`FileSource`]: Loads configuration from a file, detecting `.toml`, `.json`, `.ron`, `.yaml`, or `.yml` files based on the file extension. Requires the matching `toml`, `json`, `ron-0_12`, or `yaml_serde-0_10` feature. (`toml` is enabled by default.)
126+
- [`FileSource`]: Loads configuration from a file, detecting `.ini`, `.toml`, `.json`, `.ron`, `.yaml`, or `.yml` files based on the file extension. Requires the matching `serde_ini-0_2`, `toml`, `json`, `ron-0_12`, or `yaml_serde-0_10` feature. (`toml` is enabled by default.)
127127
- [`TomlSource`]: Loads configuration from a TOML string literal. Requires the `toml` feature. (Enabled by default.)
128128
- [`JsonSource`]: Loads configuration from a JSON string literal. Requires the `json` feature.
129+
- [`IniSource`]: Loads configuration from an INI string literal. Requires the `serde_ini-0_2` feature.
129130
- [`RonSource`]: Loads configuration from a RON string literal. Requires the `ron-0_12` feature.
130131
- [`YamlSource`]: Loads configuration from a YAML string literal. Requires the `yaml_serde-0_10` feature.
131132
- [`OffsetSource`]: Loads configuration from an inner source that is provided to it, but applied to a particular offset of the root configuration builder.

confik/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ use self::path::Path;
4242
pub use self::reloading::{ReloadCallback, ReloadableConfig, ReloadingConfig};
4343
#[cfg(feature = "env")]
4444
pub use self::sources::env_source::EnvSource;
45+
#[cfg(feature = "serde_ini-0_2")]
46+
pub use self::sources::ini_source::IniSource;
4547
#[cfg(feature = "json")]
4648
pub use self::sources::json_source::JsonSource;
4749
#[cfg(feature = "ron-0_12")]

confik/src/sources/file_source.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ enum FileErrorKind {
3535
#[error(transparent)]
3636
Json(#[from] serde_json::Error),
3737

38+
#[cfg(feature = "serde_ini-0_2")]
39+
#[error(transparent)]
40+
Ini(#[from] serde_ini_0_2::error::Error),
41+
3842
#[cfg(feature = "ron-0_12")]
3943
#[error(transparent)]
4044
Ron(#[from] ron_0_12::error::SpannedError),
@@ -57,6 +61,7 @@ impl FileSource {
5761
/// The deserialization method will be determined by the file extension.
5862
///
5963
/// Supported extensions:
64+
/// - `ini`
6065
/// - `toml`
6166
/// - `json`
6267
/// - `ron`
@@ -100,6 +105,16 @@ impl FileSource {
100105
}
101106
}
102107

108+
Some("ini") => {
109+
cfg_if! {
110+
if #[cfg(feature = "serde_ini-0_2")] {
111+
Ok(serde_ini_0_2::from_str(&contents).map_err(serde_ini_0_2::error::Error::from)?)
112+
} else {
113+
Err(FileErrorKind::MissingFeatureForExtension("ini"))
114+
}
115+
}
116+
}
117+
103118
Some("ron") => {
104119
cfg_if! {
105120
if #[cfg(feature = "ron-0_12")] {
@@ -207,6 +222,29 @@ mod tests {
207222
dir.close().unwrap();
208223
}
209224

225+
#[cfg(feature = "serde_ini-0_2")]
226+
#[test]
227+
fn ini() {
228+
let dir = tempfile::TempDir::new().unwrap();
229+
230+
let ini_path = dir.path().join("config.ini");
231+
232+
fs::write(&ini_path, "").unwrap();
233+
let source = FileSource::new(&ini_path);
234+
let err = source.deserialize::<Option<SimpleConfig>>().unwrap_err();
235+
assert!(
236+
err.to_string().contains("missing field"),
237+
"unexpected error message: {err}",
238+
);
239+
240+
fs::write(&ini_path, "foo = 42\n").unwrap();
241+
let source = FileSource::new(&ini_path);
242+
let config = source.deserialize::<Option<SimpleConfig>>().unwrap();
243+
assert_eq!(config.unwrap().foo, 42);
244+
245+
dir.close().unwrap();
246+
}
247+
210248
#[cfg(feature = "toml")]
211249
#[test]
212250
fn toml() {

confik/src/sources/ini_source.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::{borrow::Cow, error::Error, fmt};
2+
3+
use crate::{ConfigurationBuilder, Source};
4+
5+
/// A [`Source`] containing raw INI data.
6+
#[derive(Clone)]
7+
pub struct IniSource<'a> {
8+
contents: Cow<'a, str>,
9+
allow_secrets: bool,
10+
}
11+
12+
impl<'a> IniSource<'a> {
13+
/// Creates a [`Source`] containing raw INI data.
14+
pub fn new(contents: impl Into<Cow<'a, str>>) -> Self {
15+
Self {
16+
contents: contents.into(),
17+
allow_secrets: false,
18+
}
19+
}
20+
21+
/// Allows this source to contain secrets.
22+
pub fn allow_secrets(mut self) -> Self {
23+
self.allow_secrets = true;
24+
self
25+
}
26+
}
27+
28+
impl<T: ConfigurationBuilder> Source<T> for IniSource<'_> {
29+
fn allows_secrets(&self) -> bool {
30+
self.allow_secrets
31+
}
32+
33+
fn provide(&self) -> Result<T, Box<dyn Error + Sync + Send>> {
34+
Ok(serde_ini_0_2::from_str(&self.contents)?)
35+
}
36+
}
37+
38+
impl fmt::Debug for IniSource<'_> {
39+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40+
f.debug_struct("IniSource")
41+
.field("allow_secrets", &self.allow_secrets)
42+
.finish_non_exhaustive()
43+
}
44+
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use confik_macros::Configuration;
49+
50+
use super::*;
51+
52+
#[derive(Debug, PartialEq, Eq, serde::Deserialize, Configuration)]
53+
struct TestConfig {
54+
value: usize,
55+
}
56+
57+
#[test]
58+
fn provides_ini_data() {
59+
let source = IniSource::new("value = 42\n");
60+
61+
let config =
62+
<IniSource<'_> as Source<<TestConfig as crate::Configuration>::Builder>>::provide(
63+
&source,
64+
)
65+
.unwrap()
66+
.try_build()
67+
.unwrap();
68+
69+
assert_eq!(config, TestConfig { value: 42 });
70+
}
71+
72+
#[test]
73+
fn propagates_parse_errors() {
74+
let source = IniSource::new("value\n");
75+
76+
let err =
77+
match <IniSource<'_> as Source<<TestConfig as crate::Configuration>::Builder>>::provide(
78+
&source,
79+
) {
80+
Ok(_) => panic!("INI parsing should fail"),
81+
Err(err) => err,
82+
};
83+
84+
assert!(!err.to_string().is_empty());
85+
}
86+
87+
#[test]
88+
fn allow_secrets_enables_secret_loading() {
89+
let source = IniSource::new("value = 42\n").allow_secrets();
90+
91+
assert!(<IniSource<'_> as Source<
92+
<TestConfig as crate::Configuration>::Builder,
93+
>>::allows_secrets(&source));
94+
}
95+
}

confik/src/sources/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub(crate) mod json_source;
4343
#[cfg(feature = "ron-0_12")]
4444
pub(crate) mod ron_source;
4545

46+
#[cfg(feature = "serde_ini-0_2")]
47+
pub(crate) mod ini_source;
48+
4649
#[cfg(feature = "env")]
4750
pub(crate) mod env_source;
4851

confik/tests/main.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ mod json {
5858
}
5959
}
6060

61+
#[cfg(feature = "serde_ini-0_2")]
62+
mod ini {
63+
use confik::{ConfigBuilder, IniSource};
64+
65+
use crate::{Target, TargetEnum};
66+
67+
#[test]
68+
fn check_ini() {
69+
assert_eq!(
70+
ConfigBuilder::<Target>::default()
71+
.override_with(IniSource::new("a = 5\nb = Second\n"))
72+
.try_build()
73+
.expect("INI deserialization should succeed"),
74+
Target {
75+
a: 5,
76+
b: TargetEnum::Second,
77+
}
78+
);
79+
}
80+
}
81+
6182
#[cfg(feature = "ron-0_12")]
6283
mod ron {
6384
use confik::{ConfigBuilder, RonSource};

confik/tests/secret/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ mod json {
6767
}
6868
}
6969

70+
#[cfg(feature = "serde_ini-0_2")]
71+
mod ini {
72+
use assert_matches::assert_matches;
73+
use confik::{ConfigBuilder, Error, IniSource};
74+
75+
use super::NotSecret;
76+
77+
#[test]
78+
fn check_ini_is_not_secret() {
79+
let target = ConfigBuilder::<NotSecret>::default()
80+
.override_with(IniSource::new("[public]\npublic = 1\nsecret = 2\n"))
81+
.try_build()
82+
.expect_err("INI deserialization is not a secret source");
83+
84+
assert_matches!(
85+
&target,
86+
Error::UnexpectedSecret(path, _) if path.to_string().contains("public.secret")
87+
);
88+
}
89+
}
90+
7091
#[cfg(feature = "ron-0_12")]
7192
mod ron {
7293
use assert_matches::assert_matches;

0 commit comments

Comments
 (0)