diff --git a/Cargo.lock b/Cargo.lock index 4f0f9a6..e4629ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "borsh" version = "1.6.1" @@ -240,6 +249,7 @@ dependencies = [ "ipnetwork", "jiff", "js_option", + "libcorn", "ron", "rust_decimal", "secrecy", @@ -281,6 +291,25 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.21.3" @@ -360,6 +389,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -448,6 +487,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -799,6 +848,19 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libcorn" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbe123d43f1351417f3b13e643bfce6c32590ca218226f1dc0c0dc9de2fbcdc" +dependencies = [ + "indexmap 2.13.0", + "pest", + "pest_derive", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "libm" version = "0.2.16" @@ -913,6 +975,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -1336,6 +1441,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1662,6 +1778,18 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/confik/CHANGELOG.md b/confik/CHANGELOG.md index 40e760d..57d9b03 100644 --- a/confik/CHANGELOG.md +++ b/confik/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add Corn source support behind new crate feature `corn-0_10` (off-by-default). + ## 0.15.11 - Add a new helper `MergingUnsetBuilder` and trait `MergingWithUnset`, for aiding in building of custom `Configuration` implementations. See the docs on `MergingWithUnset` for a worked example. diff --git a/confik/Cargo.toml b/confik/Cargo.toml index a70eacb..3c037c1 100644 --- a/confik/Cargo.toml +++ b/confik/Cargo.toml @@ -26,6 +26,7 @@ default = ["env", "toml"] # Source types env = ["dep:envious"] json = ["dep:serde_json"] +corn-0_10 = ["dep:libcorn-0_10"] ron-0_12 = ["dep:ron-0_12"] toml = ["dep:toml"] yaml_serde-0_10 = ["dep:yaml_serde-0_10"] @@ -62,6 +63,7 @@ thiserror = "2" # Source types envious = { version = "0.2", optional = true } +libcorn-0_10 = { package = "libcorn", version = "0.10", optional = true } ron-0_12 = { package = "ron", version = "0.12", optional = true } serde_json = { version = "1", optional = true } toml = { version = "1", optional = true, default-features = false, features = ["parse", "serde"] } diff --git a/confik/src/lib.md b/confik/src/lib.md index 0291430..6ec0d83 100644 --- a/confik/src/lib.md +++ b/confik/src/lib.md @@ -131,9 +131,10 @@ When the `tracing` feature is enabled, reload errors in the signal handler are a A [`Source`] is anything that can produce a partial [`ConfigurationBuilder`]. `confik` ships with the following source types: - [`EnvSource`]: Loads configuration from environment variables using the [`envious`] crate. Requires the `env` feature. (Enabled by default.) -- [`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.) +- [`FileSource`]: Loads configuration from a file, detecting `.toml`, `.json`, `.corn`, `.ron`, `.yaml`, or `.yml` files based on the file extension. Requires the matching `toml`, `json`, `corn-0_10`, `ron-0_12`, or `yaml_serde-0_10` feature. (`toml` is enabled by default.) - [`TomlSource`]: Loads configuration from a TOML string literal. Requires the `toml` feature. (Enabled by default.) - [`JsonSource`]: Loads configuration from a JSON string literal. Requires the `json` feature. +- [`CornSource`]: Loads configuration from a Corn string literal. Requires the `corn-0_10` feature. - [`RonSource`]: Loads configuration from a RON string literal. Requires the `ron-0_12` feature. - [`YamlSource`]: Loads configuration from a YAML string literal. Requires the `yaml_serde-0_10` feature. - [`OffsetSource`]: Loads configuration from an inner source that is provided to it, but applied to a particular offset of the root configuration builder. diff --git a/confik/src/lib.rs b/confik/src/lib.rs index f2ef195..8c9811f 100644 --- a/confik/src/lib.rs +++ b/confik/src/lib.rs @@ -40,6 +40,8 @@ mod third_party; use self::path::Path; #[cfg(feature = "reloading")] pub use self::reloading::{ReloadCallback, ReloadableConfig, ReloadingConfig}; +#[cfg(feature = "corn-0_10")] +pub use self::sources::corn_source::CornSource; #[cfg(feature = "env")] pub use self::sources::env_source::EnvSource; #[cfg(feature = "json")] diff --git a/confik/src/sources/corn_source.rs b/confik/src/sources/corn_source.rs new file mode 100644 index 0000000..309a1e5 --- /dev/null +++ b/confik/src/sources/corn_source.rs @@ -0,0 +1,95 @@ +use std::{borrow::Cow, error::Error, fmt}; + +use crate::{ConfigurationBuilder, Source}; + +/// A [`Source`] containing raw Corn data. +#[derive(Clone)] +pub struct CornSource<'a> { + contents: Cow<'a, str>, + allow_secrets: bool, +} + +impl<'a> CornSource<'a> { + /// Creates a [`Source`] containing raw Corn data. + pub fn new(contents: impl Into>) -> Self { + Self { + contents: contents.into(), + allow_secrets: false, + } + } + + /// Allows this source to contain secrets. + pub fn allow_secrets(mut self) -> Self { + self.allow_secrets = true; + self + } +} + +impl Source for CornSource<'_> { + fn allows_secrets(&self) -> bool { + self.allow_secrets + } + + fn provide(&self) -> Result> { + Ok(libcorn_0_10::from_str(&self.contents)?) + } +} + +impl fmt::Debug for CornSource<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CornSource") + .field("allow_secrets", &self.allow_secrets) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use confik_macros::Configuration; + + use super::*; + + #[derive(Debug, PartialEq, Eq, serde::Deserialize, Configuration)] + struct TestConfig { + value: usize, + } + + #[test] + fn provides_corn_data() { + let source = CornSource::new("{ value = 42 }"); + + let config = + as Source<::Builder>>::provide( + &source, + ) + .unwrap() + .try_build() + .unwrap(); + + assert_eq!(config, TestConfig { value: 42 }); + } + + #[test] + fn propagates_parse_errors() { + let source = CornSource::new("{ value = "); + + let err = + match as Source<::Builder>>::provide( + &source, + ) { + Ok(_) => panic!("Corn parsing should fail"), + Err(err) => err, + }; + + assert!(!err.to_string().is_empty()); + } + + #[test] + fn allow_secrets_enables_secret_loading() { + let source = CornSource::new("{ value = 42 }").allow_secrets(); + + assert!( as Source< + ::Builder, + >>::allows_secrets(&source)); + } +} diff --git a/confik/src/sources/file_source.rs b/confik/src/sources/file_source.rs index 2354be4..bee43cb 100644 --- a/confik/src/sources/file_source.rs +++ b/confik/src/sources/file_source.rs @@ -35,6 +35,10 @@ enum FileErrorKind { #[error(transparent)] Json(#[from] serde_json::Error), + #[cfg(feature = "corn-0_10")] + #[error(transparent)] + Corn(#[from] libcorn_0_10::error::Error), + #[cfg(feature = "ron-0_12")] #[error(transparent)] Ron(#[from] ron_0_12::error::SpannedError), @@ -60,6 +64,7 @@ impl FileSource { /// - `toml` /// - `json` /// - `ron` + /// - `corn` /// - `yaml` /// - `yml` pub fn new(path: impl Into) -> Self { @@ -100,6 +105,16 @@ impl FileSource { } } + Some("corn") => { + cfg_if! { + if #[cfg(feature = "corn-0_10")] { + Ok(libcorn_0_10::from_str(&contents)?) + } else { + Err(FileErrorKind::MissingFeatureForExtension("corn")) + } + } + } + Some("ron") => { cfg_if! { if #[cfg(feature = "ron-0_12")] { @@ -253,6 +268,29 @@ mod tests { dir.close().unwrap(); } + #[cfg(feature = "corn-0_10")] + #[test] + fn corn() { + let dir = tempfile::TempDir::new().unwrap(); + + let corn_path = dir.path().join("config.corn"); + + fs::write(&corn_path, "{ bar = 42 }").unwrap(); + let source = FileSource::new(&corn_path); + let err = source.deserialize::>().unwrap_err(); + assert!( + err.to_string().contains("missing field"), + "unexpected error message: {err}", + ); + + fs::write(&corn_path, "{ foo = 42 }").unwrap(); + let source = FileSource::new(&corn_path); + let config = source.deserialize::>().unwrap(); + assert_eq!(config.unwrap().foo, 42); + + dir.close().unwrap(); + } + #[cfg(feature = "yaml_serde-0_10")] #[test] fn yaml() { diff --git a/confik/src/sources/mod.rs b/confik/src/sources/mod.rs index ccc2509..66847e3 100644 --- a/confik/src/sources/mod.rs +++ b/confik/src/sources/mod.rs @@ -40,6 +40,9 @@ pub(crate) mod toml_source; #[cfg(feature = "json")] pub(crate) mod json_source; +#[cfg(feature = "corn-0_10")] +pub(crate) mod corn_source; + #[cfg(feature = "ron-0_12")] pub(crate) mod ron_source; diff --git a/confik/tests/main.rs b/confik/tests/main.rs index 3522d50..8650ee3 100644 --- a/confik/tests/main.rs +++ b/confik/tests/main.rs @@ -58,6 +58,27 @@ mod json { } } +#[cfg(feature = "corn-0_10")] +mod corn { + use confik::{ConfigBuilder, CornSource}; + + use crate::{Target, TargetEnum}; + + #[test] + fn check_corn() { + assert_eq!( + ConfigBuilder::::default() + .override_with(CornSource::new(r#"{ a = 6 b = "Second" }"#)) + .try_build() + .expect("Corn deserialization should succeed"), + Target { + a: 6, + b: TargetEnum::Second, + } + ); + } +} + #[cfg(feature = "ron-0_12")] mod ron { use confik::{ConfigBuilder, RonSource}; diff --git a/confik/tests/secret/mod.rs b/confik/tests/secret/mod.rs index 374581d..2c5ff6a 100644 --- a/confik/tests/secret/mod.rs +++ b/confik/tests/secret/mod.rs @@ -67,6 +67,27 @@ mod json { } } +#[cfg(feature = "corn-0_10")] +mod corn { + use assert_matches::assert_matches; + use confik::{ConfigBuilder, CornSource, Error}; + + use super::NotSecret; + + #[test] + fn check_corn_is_not_secret() { + let target = ConfigBuilder::::default() + .override_with(CornSource::new("{ public = { public = 1 secret = 2 } }")) + .try_build() + .expect_err("Corn deserialization is not a secret source"); + + assert_matches!( + &target, + Error::UnexpectedSecret(path, _) if path.to_string().contains("public.secret") + ); + } +} + #[cfg(feature = "ron-0_12")] mod ron { use assert_matches::assert_matches; diff --git a/justfile b/justfile index 0a60f69..034d0f7 100644 --- a/justfile +++ b/justfile @@ -35,6 +35,10 @@ downgrade-for-msrv toolchain="": cargo {{ toolchain }} update -p=toml_writer@1.1.0+spec-1.1.0 --precise=1.0.7+spec-1.1.0 # next ver: 1.85 cargo {{ toolchain }} update -p=toml_datetime@1.1.0+spec-1.1.0 --precise=1.0.1+spec-1.1.0 # next ver: 1.85 cargo {{ toolchain }} update -p=yaml_serde --precise=0.10.2 # next ver: 1.82 + cargo {{ toolchain }} update -p=pest_derive --precise=2.8.3 # next ver: 1.83 + cargo {{ toolchain }} update -p=pest_generator --precise=2.8.3 # next ver: 1.83 + cargo {{ toolchain }} update -p=pest_meta --precise=2.8.3 # next ver: 1.83 + cargo {{ toolchain }} update -p=pest --precise=2.8.3 # next ver: 1.83 cargo {{ toolchain }} update -p=indexmap@2 --precise=2.11.4 # next ver: 1.82 # Test workspace using MSRV