├── .envrc ├── README.md ├── .vscode └── settings.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── ci.yml │ ├── dependabot-reviewer.yml │ └── lint.yml ├── .prettierrc.yml ├── .rustfmt.toml ├── confik-macros ├── tests │ ├── trybuild │ │ ├── fail-uncreatable-type.rs │ │ ├── fail-forward-literal.rs │ │ ├── 20-enum.rs │ │ ├── fail-crate-not-in-scope.rs │ │ ├── fail-config-name-value.rs │ │ ├── fail-default-parse.rs │ │ ├── fail-secret-extra-attr.rs │ │ ├── 01-parse.rs │ │ ├── fail-default-invalid-expr.rs │ │ ├── fail-config-alone.stderr │ │ ├── fail-config-empty-list.stderr │ │ ├── fail-uncreatable-type.stderr │ │ ├── fail-secret-extra-attr.stderr │ │ ├── fail-config-name-value.stderr │ │ ├── fail-default-invalid-expr.stderr │ │ ├── fail-default-not-expression.stderr │ │ ├── fail-default-not-expression.rs │ │ ├── 02-create-builder.rs │ │ ├── 07-phantom-data.rs │ │ ├── 17-comments.rs │ │ ├── 03-simple-impl.rs │ │ ├── 05-simple-build-with-enum.rs │ │ ├── 27-field-access.rs │ │ ├── fail-try-from-not-implemented.rs │ │ ├── 26-named-builder.rs │ │ ├── 31-crate-remap.rs │ │ ├── 08-redefined-prelude-types.rs │ │ ├── 29-named-field-vis.rs │ │ ├── 19-derive.rs │ │ ├── 28-field-vis.rs │ │ ├── 22-dataless-types.rs │ │ ├── fail-crate-not-in-scope.stderr │ │ ├── 18-secret-default.rs │ │ ├── fail-field-from-unknown-type.stderr │ │ ├── 04-simple-build.rs │ │ ├── fail-field-from-unknown-type.rs │ │ ├── 30-skip-field.rs │ │ ├── 09-pub-target.rs │ │ ├── 11-simple-secret-source.rs │ │ ├── fail-forward-literal.stderr │ │ ├── 06-nested-struct.rs │ │ ├── 23-where-clause.rs │ │ ├── fail-try-from-not-implemented.stderr │ │ ├── 10-unnamed_struct_fields.rs │ │ ├── fail-default-parse.stderr │ │ ├── 21-field-from.rs │ │ ├── fail-not-a-type.rs │ │ ├── fail-not-a-type.stderr │ │ ├── 15-default-default.rs │ │ ├── 14-simple-default.rs │ │ ├── 12-complex-secret-source.rs │ │ ├── 16-partial-default.rs │ │ ├── 24-field-try-from.rs │ │ ├── fail-from-and-try-from.stderr │ │ ├── 13-unnamed-secret-source.rs │ │ ├── fail-from-and-try-from.rs │ │ └── 25-pass-enum-untagged.rs │ └── trybuild.rs ├── Cargo.toml ├── src │ └── tests.rs └── README.md ├── .gitignore ├── .codecov.yml ├── .taplo.toml ├── Cargo.toml ├── confik ├── tests │ ├── common │ │ └── mod.rs │ ├── secret_option │ │ └── mod.rs │ ├── defaulting_containers │ │ └── mod.rs │ ├── forward │ │ └── mod.rs │ ├── unkeyed_containers │ │ └── mod.rs │ ├── singly_nested_tests │ │ └── mod.rs │ ├── array │ │ └── mod.rs │ ├── main.rs │ ├── keyed_containers │ │ └── mod.rs │ ├── complex_enums │ │ └── mod.rs │ ├── third_party.rs │ ├── option_builder │ │ └── mod.rs │ └── secret │ │ └── mod.rs ├── src │ ├── path.rs │ ├── sources │ │ ├── json_source.rs │ │ ├── toml_source.rs │ │ ├── mod.rs │ │ ├── env_source.rs │ │ ├── file_source.rs │ │ └── offset_source.rs │ ├── errors.rs │ ├── secrets.rs │ ├── common.rs │ ├── builder.rs │ ├── lib.rs │ ├── third_party.rs │ ├── std_impls.rs │ ├── helpers.rs │ └── lib.md ├── examples │ ├── bigdecimal.rs │ ├── secret_string.rs │ ├── ahash.rs │ ├── simple.rs │ ├── defaulting.rs │ └── derives.rs ├── README.md ├── Cargo.toml └── CHANGELOG.md ├── .cspell.yml ├── flake.nix ├── CONTRIBUTING.md ├── flake.lock └── justfile /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | confik/README.md -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": "all" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [robjtede] 4 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: "*.md" 3 | options: 4 | printWidth: 9999 5 | proseWrap: never 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-uncreatable-type.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration, Debug)] 2 | enum C {} 3 | 4 | fn main() {} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | /target/ 3 | 4 | # direnv 5 | /.direnv/ 6 | 7 | # code coverage 8 | /codecov.json 9 | /lcov.info 10 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-forward-literal.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | #[confik(forward("hello world"))] 3 | struct A; 4 | 5 | fn main() {} 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/20-enum.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | enum Config { 3 | String(String), 4 | Num(u64), 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-crate-not-in-scope.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | #[confik(crate = not_in_scope)] 3 | struct Config {} 4 | 5 | fn main() {} 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-config-name-value.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | struct Config { 3 | #[confik = "foo"] 4 | _param: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-parse.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | struct Config { 3 | #[confik(default = 1 + 2)] 4 | _param: usize, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-secret-extra-attr.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | struct Config { 3 | #[confik(secret = "foo")] 4 | _param: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/01-parse.rs: -------------------------------------------------------------------------------- 1 | //! Check that the derive exists and compiles 2 | 3 | #[derive(confik::Configuration)] 4 | struct _Config { 5 | _param: String, 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-invalid-expr.rs: -------------------------------------------------------------------------------- 1 | #[derive(confik::Configuration)] 2 | struct Config { 3 | #[confik(default = Hello World)] 4 | _param: String, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-config-alone.stderr: -------------------------------------------------------------------------------- 1 | error: `#[config]` is not a valid standalone attribute 2 | --> tests/trybuild/fail-config-alone.rs:3:7 3 | | 4 | 3 | #[config] 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-config-empty-list.stderr: -------------------------------------------------------------------------------- 1 | error: #[confik(...)] attribute must be a non-empty list 2 | --> tests/trybuild/fail-config-empty-list.rs:3:7 3 | | 4 | 3 | #[confik()] 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-uncreatable-type.stderr: -------------------------------------------------------------------------------- 1 | error: Cannot create a builder for a type that cannot be instantiated: C 2 | --> tests/trybuild/fail-uncreatable-type.rs:2:6 3 | | 4 | 2 | enum C {} 5 | | ^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-secret-extra-attr.stderr: -------------------------------------------------------------------------------- 1 | error: Unexpected type `string` 2 | --> tests/trybuild/fail-secret-extra-attr.rs:3:23 3 | | 4 | 3 | #[confik(secret = "foo")] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-config-name-value.stderr: -------------------------------------------------------------------------------- 1 | error: Name-value arguments are not supported. Use #[confik(...)] 2 | --> tests/trybuild/fail-config-name-value.rs:3:7 3 | | 4 | 3 | #[confik = "foo"] 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-invalid-expr.stderr: -------------------------------------------------------------------------------- 1 | error: expected `,` 2 | --> tests/trybuild/fail-default-invalid-expr.rs:3:30 3 | | 4 | 3 | #[confik(default = Hello World)] 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-not-expression.stderr: -------------------------------------------------------------------------------- 1 | error: expected an expression 2 | --> tests/trybuild/fail-default-not-expression.rs:5:24 3 | | 4 | 5 | #[confik(default = +++4_u32)] 5 | | ^ 6 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-not-expression.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[derive(Debug, Configuration, PartialEq, Eq)] 4 | struct Config { 5 | #[confik(default = +++4_u32)] 6 | param: u32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/02-create-builder.rs: -------------------------------------------------------------------------------- 1 | //! Check that the builder is created and can be retrieved. 2 | 3 | #[derive(confik::Configuration)] 4 | struct Config { 5 | _param: String, 6 | } 7 | 8 | fn main() { 9 | let _builder = ::builder(); 10 | } 11 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | threshold: 100% # make CI green 8 | patch: 9 | default: 10 | threshold: 100% # make CI green 11 | 12 | ignore: 13 | - "**/tests" 14 | - "**/benches" 15 | - "**/examples" 16 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/07-phantom-data.rs: -------------------------------------------------------------------------------- 1 | //! Check that the builder is created and can be retrieved. 2 | 3 | use std::marker::PhantomData; 4 | 5 | use confik::Configuration; 6 | 7 | #[derive(Configuration)] 8 | struct Config { 9 | _param: PhantomData, 10 | } 11 | 12 | fn main() { 13 | let _builder = Config::builder(); 14 | } 15 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/17-comments.rs: -------------------------------------------------------------------------------- 1 | //! Check that comments don't interfere with parsing 2 | 3 | #[derive(confik::Configuration)] 4 | struct _Config { 5 | /// Outer doc comments! 6 | // Outer non-doc comments! 7 | /** Out long doc comments */ 8 | /* Outer long non-doc comments! */ 9 | _param: String, 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/03-simple-impl.rs: -------------------------------------------------------------------------------- 1 | //! Simplest check for traits implemented on the builder 2 | use confik::Configuration; 3 | 4 | #[derive(Configuration, Debug)] 5 | struct Config { 6 | _param: String, 7 | } 8 | 9 | fn main() { 10 | let _builder = Config::builder() 11 | .try_build() 12 | .expect_err("Somehow built with no data?"); 13 | } 14 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/05-simple-build-with-enum.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple struct can be built. 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, Configuration, PartialEq)] 6 | enum BasicEnum { 7 | A, 8 | B(), 9 | C{}, 10 | } 11 | 12 | #[derive(Configuration, Debug, PartialEq)] 13 | struct Config { 14 | param: BasicEnum, 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["target/*"] 2 | include = ["**/*.toml"] 3 | 4 | [formatting] 5 | column_width = 100 6 | 7 | [[rule]] 8 | include = ["**/Cargo.toml"] 9 | keys = ["dependencies", "*-dependencies"] 10 | 11 | [rule.formatting] 12 | reorder_keys = true 13 | 14 | [[rule]] 15 | include = ["**/Cargo.toml"] 16 | keys = ["dependencies.*", "*-dependencies.*"] 17 | 18 | [rule.formatting] 19 | reorder_keys = false 20 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/27-field-access.rs: -------------------------------------------------------------------------------- 1 | //! Check that we can reference builder fields 2 | use confik::Configuration; 3 | 4 | #[derive(Configuration, Debug, PartialEq)] 5 | struct Config { 6 | #[confik(default)] 7 | param: String, 8 | } 9 | 10 | type Builder = ::Builder; 11 | 12 | fn main() { 13 | let _ = Builder { 14 | param: Default::default(), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-try-from-not-implemented.rs: -------------------------------------------------------------------------------- 1 | //! Check the error returned when `TryFrom`/`TryInto` is not implemented for the given types. 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, Configuration)] 6 | struct Config { 7 | #[confik(try_from = String)] 8 | param: Foo, 9 | } 10 | 11 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 12 | struct Foo(String); 13 | 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/26-named-builder.rs: -------------------------------------------------------------------------------- 1 | //! Check that we can name and reference a builder. 2 | use confik::ConfigurationBuilder; 3 | 4 | #[derive(confik::Configuration, Debug, PartialEq)] 5 | #[confik(name = C)] 6 | struct Config { 7 | #[confik(default)] 8 | param: String, 9 | } 10 | 11 | fn main() { 12 | let Config { .. } = C::default() 13 | .try_build() 14 | .expect("Default builder should succeed"); 15 | } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["./confik", "./confik-macros"] 4 | 5 | [workspace.package] 6 | authors = ["Rob Ede "] 7 | keywords = ["parser", "serde", "utility", "config"] 8 | categories = ["config"] 9 | repository = "https://github.com/x52dev/confik" 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | rust-version = "1.70" 13 | 14 | [patch.crates-io] 15 | confik = { path = "confik" } 16 | confik-macros = { path = "confik-macros" } 17 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/31-crate-remap.rs: -------------------------------------------------------------------------------- 1 | //! Check that we import from a re-mapped confik crate root. 2 | 3 | use ::confik as confik_remapped; 4 | 5 | use confik_remapped::Configuration as _; 6 | 7 | #[derive(Debug, PartialEq, confik_remapped::Configuration)] 8 | #[confik(crate = confik_remapped)] 9 | struct Config {} 10 | 11 | fn main() { 12 | let config = Config::builder() 13 | .try_build() 14 | .expect("Failed to build when configured"); 15 | 16 | assert_eq!(Config {}, config); 17 | } 18 | -------------------------------------------------------------------------------- /confik/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use confik::{common::DatabaseConnectionConfig, Configuration, TomlSource}; 2 | 3 | #[test] 4 | fn database_config() { 5 | let toml = r#" 6 | database = "postgres" 7 | username = "user" 8 | password = "password" 9 | path = "abc" 10 | "#; 11 | let config = DatabaseConnectionConfig::builder() 12 | .override_with(TomlSource::new(toml).allow_secrets()) 13 | .try_build() 14 | .unwrap(); 15 | assert_eq!(config.to_string(), "postgres://user:password@abc"); 16 | } 17 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/08-redefined-prelude-types.rs: -------------------------------------------------------------------------------- 1 | //! Check that the builder is created and can be retrieved. 2 | use confik::Configuration; 3 | 4 | #[allow(dead_code)] 5 | type Option = (); 6 | #[allow(dead_code)] 7 | type Some = (); 8 | #[allow(dead_code)] 9 | type None = (); 10 | #[allow(dead_code)] 11 | type Result = (); 12 | #[allow(dead_code)] 13 | type Box = (); 14 | 15 | #[derive(Configuration)] 16 | struct Config { 17 | _param: String, 18 | } 19 | 20 | fn main() { 21 | let _builder = Config::builder(); 22 | } 23 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/29-named-field-vis.rs: -------------------------------------------------------------------------------- 1 | //! Check that we can reference builder fields in a different module, when they're public, using the builder's name 2 | 3 | pub mod config { 4 | use confik::Configuration; 5 | 6 | #[derive(Configuration, Debug, PartialEq)] 7 | #[confik(name = Builder)] 8 | pub struct Config { 9 | #[confik(default)] 10 | pub param: String, 11 | } 12 | } 13 | 14 | fn main() { 15 | let _ = config::Builder { 16 | param: Default::default(), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/19-derive.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeSet, HashSet}; 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Configuration)] 6 | #[confik(forward(derive(::std::hash::Hash, std::cmp::Ord, PartialOrd, Eq, PartialEq, Clone)))] 7 | struct Target { 8 | item: usize, 9 | } 10 | 11 | fn main() { 12 | let builder = ::Builder::default(); 13 | let mut set = HashSet::new(); 14 | set.insert(builder.clone()); 15 | let mut set = BTreeSet::new(); 16 | set.insert(builder); 17 | } 18 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/28-field-vis.rs: -------------------------------------------------------------------------------- 1 | //! Check that we can reference builder fields in a different module, when they're public 2 | 3 | pub mod config { 4 | use confik::Configuration; 5 | 6 | #[derive(Configuration, Debug, PartialEq)] 7 | pub struct Config { 8 | #[confik(default)] 9 | pub param: String, 10 | } 11 | 12 | pub type Builder = ::Builder; 13 | } 14 | 15 | fn main() { 16 | let _ = config::Builder { 17 | param: Default::default(), 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/22-dataless-types.rs: -------------------------------------------------------------------------------- 1 | //! Check that structs with no data can be built 2 | use confik::Configuration; 3 | 4 | #[derive(Configuration, Debug)] 5 | struct A; 6 | 7 | #[derive(Configuration, Debug)] 8 | struct B {} 9 | 10 | #[derive(Configuration, Debug)] 11 | struct C(); 12 | 13 | fn main() { 14 | let _builder = A::builder().try_build().expect("No data required"); 15 | let _builder = B::builder().try_build().expect("No data required"); 16 | let _builder = C::builder().try_build().expect("No data required"); 17 | } 18 | -------------------------------------------------------------------------------- /.cspell.yml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | words: 3 | - ahash 4 | - bigdecimal 5 | - bytesize 6 | - chrono 7 | - clippy 8 | - codecov 9 | - confik 10 | - dataless 11 | - deserializations 12 | - docsrs 13 | - doctests 14 | - formatdoc 15 | - humantime 16 | - impls 17 | - indoc 18 | - ipnetwork 19 | - msrv 20 | - nextest 21 | - nixpkgs 22 | - pkgs 23 | - powerset 24 | - rustc 25 | - rustdoc 26 | - rustup 27 | - rustversion 28 | - serde 29 | - struct 30 | - taplo 31 | - tempfile 32 | - thiserror 33 | - trybuild 34 | - usize 35 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-crate-not-in-scope.stderr: -------------------------------------------------------------------------------- 1 | error[E0433]: failed to resolve: use of undeclared crate or module `not_in_scope` 2 | --> tests/trybuild/fail-crate-not-in-scope.rs:2:18 3 | | 4 | 2 | #[confik(crate = not_in_scope)] 5 | | ^^^^^^^^^^^^ use of undeclared crate or module `not_in_scope` 6 | 7 | error: cannot find attribute `serde` in this scope 8 | --> tests/trybuild/fail-crate-not-in-scope.rs:3:8 9 | | 10 | 3 | struct Config {} 11 | | ^^^^^^ 12 | | 13 | = note: `serde` is in scope, but it is a crate, not an attribute 14 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/18-secret-default.rs: -------------------------------------------------------------------------------- 1 | //! Secret fields can have defaults specified on the same inner attribute. 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, PartialEq, Eq, Configuration)] 6 | struct Config { 7 | #[confik(secret, default = "foo")] 8 | param: String, 9 | } 10 | 11 | fn main() { 12 | let config = Config::builder() 13 | .try_build() 14 | .expect("Failed to build secret with default"); 15 | 16 | assert_eq!( 17 | Config { 18 | param: "foo".to_string() 19 | }, 20 | config 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-field-from-unknown-type.stderr: -------------------------------------------------------------------------------- 1 | error[E0412]: cannot find type `A` in this scope 2 | --> tests/trybuild/fail-field-from-unknown-type.rs:6:21 3 | | 4 | 6 | #[confik(from = A)] 5 | | ^ not found in this scope 6 | | 7 | help: you might be missing a type parameter 8 | | 9 | 5 | struct Config { 10 | | +++ 11 | 12 | error[E0412]: cannot find type `A` in this scope 13 | --> tests/trybuild/fail-field-from-unknown-type.rs:6:21 14 | | 15 | 6 | #[confik(from = A)] 16 | | ^ not found in this scope 17 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/04-simple-build.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple struct can be built. 2 | 3 | use confik::{ConfigBuilder, TomlSource}; 4 | 5 | #[derive(confik::Configuration, Debug, PartialEq)] 6 | struct Config { 7 | param: String, 8 | } 9 | 10 | fn main() { 11 | let config = ConfigBuilder::default() 12 | .override_with(TomlSource::new(r#"param = "Hello World""#)) 13 | .try_build() 14 | .expect("Failed to build"); 15 | assert_eq!( 16 | Config { 17 | param: "Hello World".to_string() 18 | }, 19 | config 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-field-from-unknown-type.rs: -------------------------------------------------------------------------------- 1 | //! Check that the `from` attribute works 2 | use confik::{Configuration, TomlSource}; 3 | 4 | #[derive(Debug, Configuration, PartialEq, Eq)] 5 | struct Config { 6 | #[confik(from = A)] 7 | param: String, 8 | } 9 | 10 | fn main() { 11 | let config = Config::builder() 12 | .override_with(TomlSource::new("param = 5")) 13 | .try_build() 14 | .expect("Failed to build with no required data"); 15 | assert_eq!( 16 | Config { 17 | param: String::from("Hello world") 18 | }, 19 | config 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /confik/src/path.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::{Display, Formatter}, 4 | }; 5 | 6 | #[derive(Debug, Default)] 7 | pub(crate) struct Path(pub(crate) Vec>); 8 | 9 | impl Path { 10 | pub fn new() -> Self { 11 | Self::default() 12 | } 13 | } 14 | 15 | impl Display for Path { 16 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 17 | for (i, segment) in self.0.iter().rev().enumerate() { 18 | if i > 0 { 19 | f.write_str(".")?; 20 | } 21 | f.write_str(segment)?; 22 | } 23 | Ok(()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/30-skip-field.rs: -------------------------------------------------------------------------------- 1 | //! Check that we can skip a field with a default 2 | 3 | use confik::ConfigBuilder; 4 | 5 | #[derive(Debug, Default, PartialEq, Eq)] 6 | struct NoConfigImpl; 7 | 8 | #[derive(confik::Configuration, Debug, PartialEq, Eq)] 9 | struct Config { 10 | #[confik(default, skip)] 11 | param: NoConfigImpl, 12 | } 13 | 14 | fn main() { 15 | let config = ConfigBuilder::default() 16 | .try_build() 17 | .expect("Failed to build when configured"); 18 | assert_eq!( 19 | Config { 20 | param: NoConfigImpl 21 | }, 22 | config 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/09-pub-target.rs: -------------------------------------------------------------------------------- 1 | //! Check nested structs. 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, serde::Deserialize, Configuration, PartialEq, Eq)] 6 | pub enum BasicEnum { 7 | A, 8 | B, 9 | } 10 | 11 | #[derive(Configuration, Debug, PartialEq, Eq)] 12 | pub struct ConfigLeaf { 13 | param1: BasicEnum, 14 | param2: BasicEnum, 15 | } 16 | 17 | #[derive(Configuration, Debug, PartialEq, Eq)] 18 | pub struct ConfigNode { 19 | leaf1: ConfigLeaf, 20 | leaf2: ConfigLeaf, 21 | } 22 | 23 | pub(crate) struct _ConfigRoot { 24 | node1: ConfigLeaf, 25 | node2: ConfigLeaf, 26 | } 27 | 28 | fn main() {} 29 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/11-simple-secret-source.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple struct can be built. 2 | 3 | use confik::{ConfigBuilder, Error, TomlSource}; 4 | 5 | #[derive(confik::Configuration, Debug, PartialEq)] 6 | struct Config { 7 | #[confik(secret)] 8 | param: String, 9 | } 10 | 11 | fn main() { 12 | let error = ConfigBuilder::::default() 13 | .override_with(TomlSource::new(r#"param = "Hello World""#)) 14 | .try_build() 15 | .expect_err("Can't build secret from Toml"); 16 | assert_matches::assert_matches!(error, Error::UnexpectedSecret(path, _) if path.to_string().contains("`param`")); 17 | } 18 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-forward-literal.stderr: -------------------------------------------------------------------------------- 1 | error: expected identifier, found `"hello world"` 2 | --> tests/trybuild/fail-forward-literal.rs:2:18 3 | | 4 | 2 | #[confik(forward("hello world"))] 5 | | ^^^^^^^^^^^^^ expected identifier 6 | 7 | error: proc-macro derive produced unparsable tokens 8 | --> tests/trybuild/fail-forward-literal.rs:1:10 9 | | 10 | 1 | #[derive(confik::Configuration)] 11 | | ^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | error[E0412]: cannot find type `AConfigBuilder` in this scope 14 | --> tests/trybuild/fail-forward-literal.rs:3:8 15 | | 16 | 3 | struct A; 17 | | ^ not found in this scope 18 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/06-nested-struct.rs: -------------------------------------------------------------------------------- 1 | //! Check nested structs. 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, serde::Deserialize, Configuration, PartialEq)] 6 | enum BasicEnum { 7 | A, 8 | B, 9 | } 10 | 11 | #[derive(Configuration, Debug, PartialEq)] 12 | struct ConfigLeaf { 13 | param1: BasicEnum, 14 | param2: BasicEnum, 15 | } 16 | 17 | #[derive(Configuration, Debug, PartialEq)] 18 | struct ConfigNode { 19 | leaf1: ConfigLeaf, 20 | leaf2: ConfigLeaf, 21 | } 22 | 23 | #[derive(Configuration, Debug, PartialEq)] 24 | struct ConfigRoot { 25 | node1: ConfigNode, 26 | node2: ConfigNode, 27 | } 28 | 29 | fn main() {} 30 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/23-where-clause.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | use serde::de::DeserializeOwned; 3 | 4 | trait MyTrait { 5 | type Config: Configuration; 6 | } 7 | 8 | #[derive(Configuration)] 9 | struct EmptyConfig; 10 | 11 | impl MyTrait for () { 12 | type Config = EmptyConfig; 13 | } 14 | 15 | #[derive(Configuration)] 16 | #[confik(forward(serde(bound = "C: MyTrait + DeserializeOwned")))] 17 | struct Config 18 | where 19 | C: MyTrait + Default + DeserializeOwned, 20 | { 21 | sub_config: ::Config, 22 | } 23 | 24 | fn main() { 25 | Config::<()>::builder() 26 | .try_build() 27 | .expect("No configuration needed"); 28 | } 29 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-try-from-not-implemented.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `Foo: From` is not satisfied 2 | --> tests/trybuild/fail-try-from-not-implemented.rs:5:17 3 | | 4 | 5 | #[derive(Debug, Configuration)] 5 | | ^^^^^^^^^^^^^ the trait `From` is not implemented for `Foo` 6 | | 7 | = note: required for `std::string::String` to implement `Into` 8 | = note: required for `Foo` to implement `TryFrom` 9 | = note: required for `std::string::String` to implement `std::convert::TryInto` 10 | = note: this error originates in the derive macro `Configuration` (in Nightly builds, run with -Z macro-backtrace for more info) 11 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/10-unnamed_struct_fields.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple struct with unnamed fields can be built. 2 | 3 | use confik::Configuration; 4 | use confik::{ConfigBuilder, TomlSource}; 5 | 6 | #[derive(Configuration, Debug, PartialEq)] 7 | struct Data(String); 8 | 9 | #[derive(Configuration, Debug, PartialEq)] 10 | struct Config { 11 | param: Data, 12 | } 13 | 14 | fn main() { 15 | let config = ConfigBuilder::default() 16 | .override_with(TomlSource::new(r#"param = "Hello World""#)) 17 | .try_build() 18 | .expect("Failed to build"); 19 | assert_eq!( 20 | Config { 21 | param: Data("Hello World".to_string()) 22 | }, 23 | config 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-default-parse.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `usize: From` is not satisfied 2 | --> tests/trybuild/fail-default-parse.rs:3:24 3 | | 4 | 3 | #[confik(default = 1 + 2)] 5 | | ^---- 6 | | | 7 | | the trait `From` is not implemented for `usize` 8 | | this tail expression is of type `i32` 9 | | 10 | = help: the following other types implement trait `From`: 11 | > 12 | > 13 | > 14 | > 15 | > 16 | = note: required for `i32` to implement `Into` 17 | -------------------------------------------------------------------------------- /confik-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "confik-macros" 3 | version = "0.15.1" 4 | description = "Macros for confik" 5 | authors.workspace = true 6 | keywords.workspace = true 7 | categories.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | darling = "0.21.3" 18 | proc-macro2 = "1" 19 | quote = "1" 20 | syn = { version = "2", features = ["extra-traits"] } 21 | 22 | [dev-dependencies] 23 | assert_matches = "1.5" 24 | confik = "0.15" 25 | indoc = "2" 26 | rustversion-msrv = "0.100" 27 | serde = { version = "1", features = ["derive"] } 28 | serde-bool = "0.1" 29 | toml = "0.9" 30 | trybuild = { version = "1", features = ["diff"] } 31 | -------------------------------------------------------------------------------- /confik/examples/bigdecimal.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use bigdecimal::BigDecimal; 4 | use confik::{Configuration, TomlSource}; 5 | use indoc::formatdoc; 6 | 7 | #[derive(Configuration, Debug)] 8 | struct Config { 9 | big_decimal: BigDecimal, 10 | } 11 | 12 | fn main() { 13 | let big_decimal = "1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573"; 14 | let toml = formatdoc! {r#" 15 | big_decimal = "{big_decimal}" 16 | "#}; 17 | 18 | let config = Config::builder() 19 | .override_with(TomlSource::new(toml)) 20 | .try_build() 21 | .expect("Failed to parse config"); 22 | 23 | assert_eq!( 24 | config.big_decimal, 25 | BigDecimal::from_str(big_decimal).unwrap() 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/21-field-from.rs: -------------------------------------------------------------------------------- 1 | //! Check that the `from` attribute works 2 | 3 | use confik::{Configuration, TomlSource}; 4 | 5 | #[derive(Debug, Configuration, PartialEq, Eq)] 6 | struct Config { 7 | #[confik(from = A)] 8 | param: String, 9 | } 10 | 11 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 12 | struct A(usize); 13 | 14 | impl From for String { 15 | fn from(_: A) -> Self { 16 | String::from("Hello world") 17 | } 18 | } 19 | 20 | fn main() { 21 | let config = Config::builder() 22 | .override_with(TomlSource::new("param = 5")) 23 | .try_build() 24 | .expect("Failed to build with no required data"); 25 | assert_eq!( 26 | Config { 27 | param: String::from("Hello world") 28 | }, 29 | config 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-not-a-type.rs: -------------------------------------------------------------------------------- 1 | //! Check that the `from` attribute works 2 | use confik::{Configuration, TomlSource}; 3 | 4 | #[derive(Debug, Configuration, PartialEq, Eq)] 5 | struct Config { 6 | #[confik(from = { A })] 7 | param: String, 8 | } 9 | 10 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 11 | struct A(usize); 12 | 13 | impl From for String { 14 | fn from(_: A) -> Self { 15 | String::from("Hello world") 16 | } 17 | } 18 | 19 | fn main() { 20 | let config = Config::builder() 21 | .override_with(TomlSource::new("param = 5")) 22 | .try_build() 23 | .expect("Failed to build with no required data"); 24 | assert_eq!( 25 | Config { 26 | param: String::from("Hello world") 27 | }, 28 | config 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /confik-macros/src/tests.rs: -------------------------------------------------------------------------------- 1 | use syn::parse_str; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn secret_attribute_parsing() { 7 | let input = r#" 8 | #[derive(Configuration)] 9 | struct Config { 10 | #[confik(secret)] 11 | field: String, 12 | } 13 | "#; 14 | 15 | let parsed = parse_str(input).expect("Failed to parse input as rust code"); 16 | let implementer = RootImplementer::from_derive_input(&parsed) 17 | .expect("Failed to read derive input into `RootImplementer`"); 18 | assert!( 19 | implementer 20 | .data 21 | .as_ref() 22 | .take_struct() 23 | .expect("Didn't parse as struct") 24 | .fields[0] 25 | .secret 26 | .is_present(), 27 | "Failed to read secret, state: {implementer:?}" 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | }; 6 | 7 | outputs = inputs@{ flake-parts, ... }: 8 | flake-parts.lib.mkFlake { inherit inputs; } { 9 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 10 | perSystem = { pkgs, config, inputs', system, lib, ... }: { 11 | formatter = pkgs.nixpkgs-fmt; 12 | 13 | devShells.default = pkgs.mkShell { 14 | packages = [ 15 | config.formatter 16 | pkgs.nodePackages.prettier 17 | pkgs.taplo 18 | pkgs.just 19 | pkgs.cargo-hack 20 | ] ++ lib.optional pkgs.stdenv.isDarwin [ 21 | pkgs.pkgsBuildHost.libiconv 22 | ]; 23 | }; 24 | }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-not-a-type.stderr: -------------------------------------------------------------------------------- 1 | error: Unable to parse type from: { A } 2 | --> tests/trybuild/fail-not-a-type.rs:6:21 3 | | 4 | 6 | #[confik(from = { A })] 5 | | ^^^^^ 6 | 7 | error[E0599]: no function or associated item named `builder` found for struct `Config` in the current scope 8 | --> tests/trybuild/fail-not-a-type.rs:20:26 9 | | 10 | 5 | struct Config { 11 | | ------------- function or associated item `builder` not found for this struct 12 | ... 13 | 20 | let config = Config::builder() 14 | | ^^^^^^^ function or associated item not found in `Config` 15 | | 16 | = help: items from traits can only be used if the trait is implemented and in scope 17 | = note: the following trait defines an item `builder`, perhaps you need to implement it: 18 | candidate #1: `Configuration` 19 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/15-default-default.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple default can be used 2 | 3 | use confik::{ConfigBuilder, TomlSource}; 4 | 5 | #[derive(confik::Configuration, Debug, PartialEq)] 6 | struct Config { 7 | #[confik(default)] 8 | param: String, 9 | } 10 | 11 | fn main() { 12 | let config = ConfigBuilder::default() 13 | .override_with(TomlSource::new(r#"param = "Hello World""#)) 14 | .try_build() 15 | .expect("Failed to build when configured"); 16 | assert_eq!( 17 | Config { 18 | param: "Hello World".to_string() 19 | }, 20 | config 21 | ); 22 | 23 | let config = ConfigBuilder::default() 24 | .try_build() 25 | .expect("Failed to build with defaults"); 26 | assert_eq!( 27 | Config { 28 | param: "".to_string() 29 | }, 30 | config 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /confik/examples/secret_string.rs: -------------------------------------------------------------------------------- 1 | use confik::{Configuration, TomlSource}; 2 | use indoc::indoc; 3 | use secrecy::{ExposeSecret as _, SecretString}; 4 | 5 | #[derive(Configuration, Debug)] 6 | struct Config { 7 | secret_field: SecretString, 8 | } 9 | 10 | fn main() { 11 | let toml = indoc! {r#" 12 | secret_field = "ProtectedSecret" 13 | "#}; 14 | 15 | let config = Config::builder() 16 | .override_with(TomlSource::new(toml).allow_secrets()) 17 | .try_build() 18 | .expect("Failed to parse config"); 19 | 20 | assert_eq!( 21 | format!("{config:?}"), 22 | "Config { secret_field: Secret([REDACTED alloc::string::String]) }", 23 | ); 24 | assert_eq!( 25 | format!("{:?}", config.secret_field), 26 | "Secret([REDACTED alloc::string::String])", 27 | ); 28 | assert_eq!(config.secret_field.expose_secret(), "ProtectedSecret"); 29 | } 30 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/14-simple-default.rs: -------------------------------------------------------------------------------- 1 | //! Check that a really simple default can be used 2 | 3 | use confik::{ConfigBuilder, TomlSource}; 4 | 5 | #[derive(confik::Configuration, Debug, PartialEq)] 6 | struct Config { 7 | #[confik(default = "hello world")] 8 | param: String, 9 | } 10 | 11 | fn main() { 12 | let config = ConfigBuilder::default() 13 | .override_with(TomlSource::new(r#"param = "Hello World""#)) 14 | .try_build() 15 | .expect("Failed to build when configured"); 16 | assert_eq!( 17 | Config { 18 | param: "Hello World".to_string() 19 | }, 20 | config 21 | ); 22 | 23 | let config = ConfigBuilder::default() 24 | .try_build() 25 | .expect("Failed to build with defaults"); 26 | assert_eq!( 27 | Config { 28 | param: "hello world".to_string() 29 | }, 30 | config 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /confik/examples/ahash.rs: -------------------------------------------------------------------------------- 1 | use ahash::{AHashMap, AHashSet}; 2 | use confik::{Configuration, TomlSource}; 3 | use indoc::formatdoc; 4 | 5 | #[derive(Configuration, Debug)] 6 | struct Config { 7 | hashset: AHashSet, 8 | hashmap: AHashMap, 9 | } 10 | 11 | fn main() { 12 | let toml = formatdoc! {r#" 13 | hashset = [1, 2, 3] 14 | [hashmap] 15 | first = 10 16 | second = 20 17 | "#}; 18 | 19 | let config = Config::builder() 20 | .override_with(TomlSource::new(toml)) 21 | .try_build() 22 | .expect("Failed to parse config"); 23 | 24 | assert_eq!(3, config.hashset.len()); 25 | assert!(config.hashset.contains(&1)); 26 | assert!(config.hashset.contains(&2)); 27 | assert!(config.hashset.contains(&3)); 28 | 29 | assert_eq!(2, config.hashmap.len()); 30 | assert_eq!(Some(&10), config.hashmap.get("first")); 31 | assert_eq!(Some(&20), config.hashmap.get("second")); 32 | } 33 | -------------------------------------------------------------------------------- /confik/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use confik::{Configuration, TomlSource}; 2 | use indoc::indoc; 3 | 4 | #[derive(Configuration, Debug, PartialEq, Eq)] 5 | struct Data { 6 | elements: Vec, 7 | } 8 | 9 | #[derive(Configuration, Debug, PartialEq, Eq)] 10 | struct Config { 11 | field1: usize, 12 | field2: String, 13 | data: Data, 14 | } 15 | 16 | fn main() { 17 | let toml = indoc! {r#" 18 | field1 = 5 19 | field2 = "Hello World" 20 | 21 | [data] 22 | elements = [1, 2, 3, 4] 23 | "#}; 24 | 25 | let config = Config::builder() 26 | .override_with(TomlSource::new(toml)) 27 | .try_build() 28 | .expect("Failed to parse config"); 29 | 30 | assert_eq!( 31 | config, 32 | Config { 33 | field1: 5, 34 | field2: String::from("Hello World"), 35 | data: Data { 36 | elements: vec![1, 2, 3, 4], 37 | }, 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /confik/examples/defaulting.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[derive(Configuration, Debug, PartialEq, Eq)] 4 | struct Data { 5 | #[confik(default = default_elements())] 6 | elements: Vec, 7 | } 8 | 9 | fn default_elements() -> Vec { 10 | vec![4, 3, 2, 1] 11 | } 12 | 13 | const FIELD2_DEFAULT: &str = "Hello World"; 14 | 15 | #[derive(Configuration, Debug, PartialEq, Eq)] 16 | struct Config { 17 | #[confik(default = 5_usize)] 18 | field1: usize, 19 | #[confik(default = FIELD2_DEFAULT)] 20 | field2: String, 21 | data: Data, 22 | } 23 | 24 | fn main() { 25 | let config = Config::builder() 26 | .try_build() 27 | .expect("Failed to parse config"); 28 | 29 | assert_eq!( 30 | config, 31 | Config { 32 | field1: 5, 33 | field2: String::from("Hello World"), 34 | data: Data { 35 | elements: vec![4, 3, 2, 1], 36 | }, 37 | } 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/12-complex-secret-source.rs: -------------------------------------------------------------------------------- 1 | use confik::{ConfigBuilder, Configuration, Error, TomlSource}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq)] 5 | struct Num(usize); 6 | 7 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq)] 8 | struct PartiallySecret { 9 | public: Num, 10 | #[confik(secret)] 11 | secret: Num, 12 | } 13 | 14 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq)] 15 | struct NotSecret { 16 | public: PartiallySecret, 17 | } 18 | 19 | fn main() { 20 | let target = ConfigBuilder::::default() 21 | .override_with(TomlSource::new("[public]\npublic = 1\nsecret = 2")) 22 | .try_build() 23 | .expect_err("Can't build secret from Toml"); 24 | 25 | assert_matches::assert_matches!( 26 | &target, 27 | Error::UnexpectedSecret(path, _) if path.to_string().contains("`public.secret`") 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | coverage: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v5 19 | 20 | - name: Install Rust 21 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 22 | with: 23 | components: llvm-tools-preview 24 | 25 | - name: Install just & cargo-llvm-cov 26 | uses: taiki-e/install-action@v2.62.60 27 | with: 28 | tool: just,cargo-llvm-cov 29 | 30 | - name: Generate code coverage 31 | run: just test-coverage-codecov 32 | 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v5.5.1 35 | with: 36 | files: codecov.json 37 | fail_ci_if_error: true 38 | env: 39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 40 | -------------------------------------------------------------------------------- /confik/examples/derives.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, hash::Hash}; 2 | 3 | use confik::{Configuration, TomlSource}; 4 | use indoc::indoc; 5 | 6 | #[derive(Debug, Configuration, PartialEq, Eq)] 7 | struct Config { 8 | set: HashSet, 9 | } 10 | 11 | #[derive(Debug, Configuration, Hash, Eq, PartialEq)] 12 | #[confik(forward(derive(Hash, Eq, PartialEq)))] 13 | struct Value { 14 | inner: String, 15 | } 16 | 17 | fn main() { 18 | let toml = indoc! {r#" 19 | set = [{inner = "hello"}, {inner = "world"}] 20 | "#}; 21 | 22 | let config = Config::builder() 23 | .override_with(TomlSource::new(toml)) 24 | .try_build() 25 | .expect("Failed to parse config"); 26 | assert_eq!( 27 | config, 28 | Config { 29 | set: HashSet::from([ 30 | Value { 31 | inner: "hello".into() 32 | }, 33 | Value { 34 | inner: "world".into() 35 | } 36 | ]), 37 | } 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/16-partial-default.rs: -------------------------------------------------------------------------------- 1 | use confik::{ConfigBuilder, Configuration, TomlSource}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 5 | struct Num(usize); 6 | 7 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 8 | struct PartiallyPresent { 9 | a: Num, 10 | b: Num, 11 | } 12 | 13 | const DEFAULT_NUM: usize = 3; 14 | 15 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 16 | struct Present { 17 | #[confik(default = def(DEFAULT_NUM))] 18 | partial: PartiallyPresent, 19 | } 20 | 21 | fn def(num: usize) -> PartiallyPresent { 22 | PartiallyPresent { 23 | a: Num(num), 24 | b: Num(num), 25 | } 26 | } 27 | 28 | fn main() { 29 | ConfigBuilder::::default() 30 | .override_with(TomlSource::new("[partial]\na = 10\n")) 31 | .try_build() 32 | .expect_err("Partial configuration with defaults only at higher level should not build"); 33 | } 34 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/24-field-try-from.rs: -------------------------------------------------------------------------------- 1 | //! Check that the `from` attribute works 2 | use confik::{Configuration, TomlSource}; 3 | 4 | #[derive(Debug, Configuration, PartialEq, Eq)] 5 | struct Config { 6 | #[confik(try_from = A)] 7 | param: String, 8 | } 9 | 10 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 11 | struct A(usize); 12 | 13 | #[derive(Debug)] 14 | struct E; 15 | 16 | impl std::fmt::Display for E { 17 | fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | Ok(()) 19 | } 20 | } 21 | 22 | impl std::error::Error for E {} 23 | 24 | impl TryFrom for String { 25 | type Error = E; 26 | 27 | fn try_from(_: A) -> Result { 28 | Ok(String::from("Hello world")) 29 | } 30 | } 31 | 32 | fn main() { 33 | let config = Config::builder() 34 | .override_with(TomlSource::new("param = 5")) 35 | .try_build() 36 | .expect("Failed to build with no required data"); 37 | assert_eq!( 38 | Config { 39 | param: String::from("Hello world") 40 | }, 41 | config 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-from-and-try-from.stderr: -------------------------------------------------------------------------------- 1 | error: Cannot support both `try_from` and `from` confik attributes 2 | --> tests/trybuild/fail-from-and-try-from.rs:6:25 3 | | 4 | 6 | #[confik(try_from = A, from = B)] 5 | | ^ 6 | 7 | error: Cannot support both `try_from` and `from` confik attributes 8 | --> tests/trybuild/fail-from-and-try-from.rs:6:35 9 | | 10 | 6 | #[confik(try_from = A, from = B)] 11 | | ^ 12 | 13 | error[E0599]: no function or associated item named `builder` found for struct `Config` in the current scope 14 | --> tests/trybuild/fail-from-and-try-from.rs:42:26 15 | | 16 | 5 | struct Config { 17 | | ------------- function or associated item `builder` not found for this struct 18 | ... 19 | 42 | let config = Config::builder() 20 | | ^^^^^^^ function or associated item not found in `Config` 21 | | 22 | = help: items from traits can only be used if the trait is implemented and in scope 23 | = note: the following trait defines an item `builder`, perhaps you need to implement it: 24 | candidate #1: `Configuration` 25 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/13-unnamed-secret-source.rs: -------------------------------------------------------------------------------- 1 | use confik::{ConfigBuilder, Configuration, Error, TomlSource}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 5 | struct Num(usize); 6 | 7 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 8 | struct PartiallySecret(Num, #[confik(secret)] Num); 9 | 10 | #[derive(Debug, Default, Deserialize, Configuration, PartialEq, Eq, Serialize)] 11 | struct NotSecret { 12 | public: PartiallySecret, 13 | } 14 | 15 | fn main() { 16 | assert_eq!( 17 | toml::to_string(&NotSecret { 18 | public: PartiallySecret(Num(1), Num(2)), 19 | }) 20 | .expect("Sanity check serialisation"), 21 | "public = [1, 2]\n" 22 | ); 23 | 24 | let target = ConfigBuilder::::default() 25 | .override_with(TomlSource::new("public = [10, 20]\n")) 26 | .try_build() 27 | .expect_err("Forbid building secret from Toml"); 28 | 29 | assert_matches::assert_matches!( 30 | &target, 31 | Error::UnexpectedSecret(path, _) if path.to_string().contains("`public.1`") 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `config` 2 | 3 | Pointers on how to contribute, aimed at simple features. Before starting, it is recommended to install [cargo-hack], due to the number of feature combinations in this crate. 4 | 5 | ## Running tests 6 | 7 | See [test.yml](./.github/workflows/ci.yml). 8 | 9 | ## New 3rd Party Crate Support 10 | 11 | ### Terminal/Leaf items 12 | 13 | In the likely event that the type you want to support is an end type (i.e. we don't need to handle the types internal to it), then this will be similar to most existing [third_party] items. Either the type is fully parsed or it is not. In this case, the builder can be a simple `Option`, with an implementation that looks like: 14 | 15 | ```rust 16 | use t::T; 17 | 18 | use crate::Configuration; 19 | 20 | impl Configuration for T { 21 | type Builder = Option; 22 | } 23 | ``` 24 | 25 | ### Container Types 26 | 27 | I recommend basing this off of the containers in [std_impls]. A simple example for each can be seen in `BTreeSet` and `BTreeMap`, depending on whether the container is indexed with a key on deserialization. 28 | 29 | [cargo-hack]: https://github.com/taiki-e/cargo-hack 30 | [third_party]: ./src/third_party.rs 31 | [std_impls]: ./src/std_impls.rs 32 | -------------------------------------------------------------------------------- /confik/src/sources/json_source.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, error::Error, fmt}; 2 | 3 | use crate::{ConfigurationBuilder, Source}; 4 | 5 | /// A [`Source`] containing raw JSON data. 6 | #[derive(Clone)] 7 | pub struct JsonSource<'a> { 8 | contents: Cow<'a, str>, 9 | allow_secrets: bool, 10 | } 11 | 12 | impl<'a> JsonSource<'a> { 13 | /// Creates a [`Source`] containing raw JSON data. 14 | pub fn new(contents: impl Into>) -> 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 Source for JsonSource<'_> { 29 | fn allows_secrets(&self) -> bool { 30 | self.allow_secrets 31 | } 32 | 33 | fn provide(&self) -> Result> { 34 | Ok(serde_json::from_str(&self.contents)?) 35 | } 36 | } 37 | 38 | impl fmt::Debug for JsonSource<'_> { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | f.debug_struct("JsonSource") 41 | .field("allow_secrets", &self.allow_secrets) 42 | .finish_non_exhaustive() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /confik/tests/secret_option/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "toml")] 2 | mod toml { 3 | use confik::{Configuration, SecretOption, TomlSource}; 4 | use serde::Deserialize; 5 | 6 | #[derive(Debug, PartialEq, Eq, Deserialize)] 7 | #[serde(transparent)] 8 | struct SecretString(String); 9 | 10 | impl Configuration for SecretString { 11 | type Builder = SecretOption; 12 | } 13 | 14 | #[derive(Debug, PartialEq, Eq, Configuration)] 15 | struct Config { 16 | data: SecretString, 17 | } 18 | 19 | #[test] 20 | fn secrets_are_secret() { 21 | let toml = r#"data = "Hello World""#; 22 | 23 | Config::builder() 24 | .override_with(TomlSource::new(toml)) 25 | .try_build() 26 | .expect_err("Source does not allow secrets"); 27 | } 28 | 29 | #[test] 30 | fn secrets_sources_allow_secrets() { 31 | let toml = r#"data = "Hello World""#; 32 | 33 | let config = Config::builder() 34 | .override_with(TomlSource::new(toml).allow_secrets()) 35 | .try_build() 36 | .expect("Secret sources allow secrets"); 37 | 38 | assert_eq!( 39 | config, 40 | Config { 41 | data: SecretString("Hello World".to_string()) 42 | } 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /confik/src/sources/toml_source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | error::Error, 4 | fmt::{Debug, Formatter}, 5 | }; 6 | 7 | use crate::{ConfigurationBuilder, Source}; 8 | 9 | /// A [`Source`] containing raw TOML data. 10 | #[derive(Clone)] 11 | pub struct TomlSource<'a> { 12 | contents: Cow<'a, str>, 13 | allow_secrets: bool, 14 | } 15 | 16 | impl<'a> TomlSource<'a> { 17 | /// A [`Source`] containing raw TOML data. 18 | pub fn new(contents: impl Into>) -> Self { 19 | Self { 20 | contents: contents.into(), 21 | allow_secrets: false, 22 | } 23 | } 24 | 25 | /// Allows this source to contain secrets. 26 | pub fn allow_secrets(mut self) -> Self { 27 | self.allow_secrets = true; 28 | self 29 | } 30 | } 31 | 32 | impl Source for TomlSource<'_> { 33 | fn allows_secrets(&self) -> bool { 34 | self.allow_secrets 35 | } 36 | 37 | fn provide(&self) -> Result> { 38 | Ok(toml::from_str(&self.contents)?) 39 | } 40 | } 41 | 42 | impl Debug for TomlSource<'_> { 43 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 44 | f.debug_struct("TomlSource") 45 | .field("allow_secrets", &self.allow_secrets) 46 | .finish_non_exhaustive() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/fail-from-and-try-from.rs: -------------------------------------------------------------------------------- 1 | //! Check that the `from` attribute works 2 | use confik::{Configuration, TomlSource}; 3 | 4 | #[derive(Debug, Configuration, PartialEq, Eq)] 5 | struct Config { 6 | #[confik(try_from = A, from = B)] 7 | param: String, 8 | } 9 | 10 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 11 | struct A(usize); 12 | 13 | #[derive(Debug, Default, serde::Deserialize, confik::Configuration)] 14 | struct B(usize); 15 | 16 | #[derive(Debug)] 17 | struct E; 18 | 19 | impl std::fmt::Display for E { 20 | fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | Ok(()) 22 | } 23 | } 24 | 25 | impl std::error::Error for E {} 26 | 27 | impl TryFrom for String { 28 | type Error = E; 29 | 30 | fn try_from(_: A) -> Result { 31 | Ok(String::from("Hello world")) 32 | } 33 | } 34 | 35 | impl From for String { 36 | fn from(_: B) -> Self { 37 | String::from("Hello world") 38 | } 39 | } 40 | 41 | fn main() { 42 | let config = Config::builder() 43 | .override_with(TomlSource::new("param = 5")) 44 | .try_build() 45 | .expect("Failed to build with no required data"); 46 | assert_eq!( 47 | Config { 48 | param: String::from("Hello world") 49 | }, 50 | config 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | merge_group: 7 | types: [checks_requested] 8 | pull_request: 9 | branches: [main] 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | read_msrv: 20 | name: Read MSRV 21 | uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 22 | 23 | test: 24 | needs: read_msrv 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | toolchain: 30 | - { name: msrv, version: "${{ needs.read_msrv.outputs.msrv }}" } 31 | - { name: stable, version: stable } 32 | 33 | runs-on: ubuntu-latest 34 | 35 | name: Test / ${{ matrix.toolchain.name }} 36 | 37 | steps: 38 | - uses: actions/checkout@v5 39 | 40 | - name: Install Rust (${{ matrix.toolchain.name }}) 41 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 42 | with: 43 | toolchain: ${{ matrix.toolchain.version }} 44 | 45 | - name: Install just, nextest 46 | uses: taiki-e/install-action@v2.62.60 47 | with: 48 | tool: just,nextest 49 | 50 | - name: workaround MSRV issues 51 | if: matrix.toolchain.name == 'msrv' 52 | run: just downgrade-for-msrv 53 | 54 | - name: Test 55 | run: just test-no-coverage 56 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1743550720, 9 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1750969886, 24 | "narHash": "sha256-zW/OFnotiz/ndPFdebpo3X0CrbVNf22n4DjN2vxlb58=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "a676066377a2fe7457369dd37c31fd2263b662f4", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-25.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1743296961, 40 | "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", 41 | "owner": "nix-community", 42 | "repo": "nixpkgs.lib", 43 | "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nix-community", 48 | "repo": "nixpkgs.lib", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "flake-parts": "flake-parts", 55 | "nixpkgs": "nixpkgs" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /confik-macros/README.md: -------------------------------------------------------------------------------- 1 | # `confik` 2 | 3 | 4 | 5 | [![crates.io](https://img.shields.io/crates/v/confik?label=latest)](https://crates.io/crates/confik) 6 | [![Documentation](https://docs.rs/confik/badge.svg?version=0.9.0)](https://docs.rs/confik/0.9.0) 7 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/confik.svg) 8 |
9 | ![Version](https://img.shields.io/badge/rustc-1.65+-ab6000.svg) 10 | [![dependency status](https://deps.rs/crate/confik/0.9.0/status.svg)](https://deps.rs/crate/confik/0.9.0) 11 | [![Download](https://img.shields.io/crates/d/confik.svg)](https://crates.io/crates/confik) 12 | 13 | 14 | 15 | This crate provides a macro for creating configuration/settings structures and functions to read them from files and the environment. 16 | 17 | ## Example 18 | 19 | Assume that `config.toml` contains 20 | 21 | ```toml 22 | host = "google.com" 23 | username = "root" 24 | ``` 25 | 26 | and the environment contains 27 | 28 | ```sh 29 | PASSWORD=hunter2 30 | ``` 31 | 32 | Then: 33 | 34 | ```rust 35 | use confik::{Configuration, EnvSource, FileSource}; 36 | 37 | #[derive(Debug, PartialEq, Configuration)] 38 | struct Config { 39 | host: String, 40 | username: String, 41 | 42 | #[confik(secret)] 43 | password: String, 44 | } 45 | 46 | fn main() { 47 | let config = Config::builder() 48 | .override_with(FileSource::new("config.toml")) 49 | .override_with(EnvSource::new().allow_secrets()) 50 | .try_build() 51 | .unwrap(); 52 | 53 | assert_eq!( 54 | config, 55 | Config { 56 | host: "google.com".to_string(), 57 | username: "root".to_string(), 58 | password: "hunter2".to_string(), 59 | } 60 | ); 61 | } 62 | ``` 63 | 64 | ## License 65 | 66 | This project is licensed under either of 67 | 68 | - Apache License, Version 2.0 69 | - MIT License 70 | 71 | at your option. 72 | -------------------------------------------------------------------------------- /confik/tests/defaulting_containers/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; 2 | 3 | use confik::Configuration; 4 | 5 | #[derive(Debug, Configuration, PartialEq, Eq, Default)] 6 | struct Config { 7 | #[confik(default = 0)] 8 | option: Option, 9 | #[confik(default = [1])] 10 | vec: Vec, 11 | #[confik(default = [2])] 12 | hashset: HashSet, 13 | #[confik(default = [3])] 14 | btreeset: BTreeSet, 15 | #[confik(default = [(4, 5)])] 16 | btreemap: BTreeMap, 17 | #[confik(default = [(6, 7)])] 18 | hashmap: HashMap, 19 | #[confik(default = [8])] 20 | array: [usize; 1], 21 | } 22 | 23 | #[test] 24 | fn containers_can_default() { 25 | let config = Config::builder().try_build().unwrap(); 26 | assert_eq!( 27 | config, 28 | Config { 29 | option: Some(0), 30 | vec: vec![1], 31 | hashset: [2].into(), 32 | btreeset: [3].into(), 33 | btreemap: [(4, 5)].into(), 34 | hashmap: [(6, 7)].into(), 35 | array: [8] 36 | } 37 | ); 38 | } 39 | 40 | #[cfg(feature = "json")] 41 | mod explicit_config { 42 | use confik::JsonSource; 43 | 44 | use super::*; 45 | 46 | #[test] 47 | fn containers_ignore_default_with_explicit_empty() { 48 | // Array can't be empty 49 | let json = r#"{ 50 | "option": null, 51 | "vec": [], 52 | "hashset": [], 53 | "btreeset": [], 54 | "btreemap": {}, 55 | "hashmap": {}, 56 | "array": [0] 57 | }"#; 58 | 59 | // Note, `#[derive(Default)]` doesn't use `confik`'s defaults. 60 | let config = Config::builder() 61 | .override_with(JsonSource::new(json)) 62 | .try_build() 63 | .unwrap(); 64 | assert_eq!(config, Config::default()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild/25-pass-enum-untagged.rs: -------------------------------------------------------------------------------- 1 | //! Checks "untagged enum" behavior / fall-through when deserializing. 2 | 3 | use confik::{Configuration, TomlSource}; 4 | 5 | #[derive(Debug, serde::Deserialize)] 6 | #[serde(untagged)] 7 | enum S3Auth { 8 | K8sServiceAccount { 9 | use_k8s_service_account: serde_bool::True, 10 | }, 11 | 12 | ApiKey { 13 | access_key: String, 14 | access_secret: String, 15 | }, 16 | } 17 | 18 | impl Configuration for S3Auth { 19 | type Builder = Option; 20 | } 21 | 22 | #[derive(Debug, Configuration)] 23 | struct Config { 24 | s3_auth: S3Auth, 25 | } 26 | 27 | fn main() { 28 | let config = indoc::formatdoc! {" 29 | [s3_auth] 30 | use_k8s_service_account = false 31 | "}; 32 | Config::builder() 33 | .override_with(TomlSource::new(config)) 34 | .try_build() 35 | .expect_err("Successfully built with incorrect boolean inner value"); 36 | 37 | let config = indoc::formatdoc! {" 38 | [s3_auth] 39 | use_k8s_service_account = true 40 | "}; 41 | let config = Config::builder() 42 | .override_with(TomlSource::new(config)) 43 | .try_build() 44 | .expect("Failed to build"); 45 | assert_matches::assert_matches!(config.s3_auth, S3Auth::K8sServiceAccount { .. }); 46 | 47 | let config = indoc::formatdoc! {" 48 | [s3_auth] 49 | access_key = \"foo\" 50 | access_secret = \"bar\" 51 | "}; 52 | let config = Config::builder() 53 | .override_with(TomlSource::new(config)) 54 | .try_build() 55 | .expect("Failed to build"); 56 | assert_matches::assert_matches!(config.s3_auth, S3Auth::ApiKey { .. }); 57 | } 58 | 59 | // /// Only successfully deserializes from a `true` value boolean. 60 | // struct True; 61 | 62 | // impl serde::Deserialize for True {} 63 | 64 | // impl config::Configuration for True { 65 | // type Builder = Option; 66 | // } 67 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-reviewer.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Reviewer 2 | 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | review-dependabot-pr: 11 | name: Approve PR 12 | runs-on: ubuntu-latest 13 | if: ${{ github.actor == 'dependabot[bot]' }} 14 | env: 15 | PR_URL: ${{ github.event.pull_request.html_url }} 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - name: FetchDependabot metadata 19 | id: dependabot-metadata 20 | uses: dependabot/fetch-metadata@v2.4.0 21 | 22 | - name: Enable auto-merge for Dependabot PRs 23 | run: gh pr merge --auto --squash "$PR_URL" 24 | 25 | - name: Approve patch and minor updates 26 | if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch'|| steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor'}} 27 | run: | 28 | gh pr review "$PR_URL" --approve --body "I'm **approving** this pull request because **it only includes patch or minor updates**." 29 | 30 | - name: Approve major updates of dev dependencies 31 | if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' && steps.dependabot-metadata.outputs.dependency-type == 'direct:development'}} 32 | run: | 33 | gh pr review "$PR_URL" --approve --body "I'm **approving** this pull request because **it only includes major updates of dev dependencies**." 34 | 35 | - name: Comment on major updates of normal dependencies 36 | if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-major' && steps.dependabot-metadata.outputs.dependency-type == 'direct:production'}} 37 | run: | 38 | gh pr comment "$PR_URL" --body "I'm **not approving** this PR because **it includes major updates of normal dependencies**." 39 | gh pr edit "$PR_URL" --add-label "requires-manual-qa" 40 | -------------------------------------------------------------------------------- /confik/README.md: -------------------------------------------------------------------------------- 1 | # `confik` 2 | 3 | 4 | 5 | [![crates.io](https://img.shields.io/crates/v/confik?label=latest)](https://crates.io/crates/confik) 6 | [![Documentation](https://docs.rs/confik/badge.svg?version=0.15.1)](https://docs.rs/confik/0.15.1) 7 | [![dependency status](https://deps.rs/crate/confik/0.15.1/status.svg)](https://deps.rs/crate/confik/0.15.1) 8 | ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/confik.svg) 9 |
10 | [![CI](https://github.com/x52dev/confik/actions/workflows/ci.yml/badge.svg)](https://github.com/x52dev/confik/actions/workflows/ci.yml) 11 | [![codecov](https://codecov.io/gh/x52dev/confik/branch/main/graph/badge.svg)](https://codecov.io/gh/x52dev/confik) 12 | ![Version](https://img.shields.io/badge/rustc-1.65+-ab6000.svg) 13 | [![Download](https://img.shields.io/crates/d/confik.svg)](https://crates.io/crates/confik) 14 | 15 | 16 | 17 | This crate provides a macro for creating configuration/settings structures and functions to read them from files and the environment. 18 | 19 | ## Example 20 | 21 | Assume that `config.toml` contains 22 | 23 | ```toml 24 | host = "google.com" 25 | username = "root" 26 | ``` 27 | 28 | and the environment contains 29 | 30 | ```sh 31 | PASSWORD=hunter2 32 | ``` 33 | 34 | Then: 35 | 36 | ```rust 37 | use confik::{Configuration, EnvSource, FileSource}; 38 | 39 | #[derive(Debug, PartialEq, Configuration)] 40 | struct Config { 41 | host: String, 42 | username: String, 43 | 44 | #[confik(secret)] 45 | password: String, 46 | } 47 | 48 | fn main() { 49 | let config = Config::builder() 50 | .override_with(FileSource::new("config.toml")) 51 | .override_with(EnvSource::new().allow_secrets()) 52 | .try_build() 53 | .unwrap(); 54 | 55 | assert_eq!( 56 | config, 57 | Config { 58 | host: "google.com".to_string(), 59 | username: "root".to_string(), 60 | password: "hunter2".to_string(), 61 | } 62 | ); 63 | } 64 | ``` 65 | 66 | ## License 67 | 68 | This project is licensed under either of 69 | 70 | - Apache License, Version 2.0 71 | - MIT License 72 | 73 | at your option. 74 | -------------------------------------------------------------------------------- /confik/src/sources/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt::Debug}; 2 | 3 | use crate::ConfigurationBuilder; 4 | 5 | /// A source of configuration data. 6 | pub trait Source: Debug { 7 | /// Whether this source is allowed to contain secret data. 8 | /// 9 | /// Implementations should be conservative and return `false` by default, allowing users to 10 | /// opt-in to storing secrets in this source. 11 | fn allows_secrets(&self) -> bool { 12 | false 13 | } 14 | 15 | /// Attempts to provide a partial configuration object from this source. 16 | fn provide(&self) -> Result>; 17 | } 18 | 19 | #[derive(Debug)] 20 | pub(crate) struct DefaultSource; 21 | 22 | impl Source for DefaultSource 23 | where 24 | T: ConfigurationBuilder, 25 | { 26 | fn allows_secrets(&self) -> bool { 27 | true 28 | } 29 | 30 | fn provide(&self) -> Result> { 31 | Ok(T::default()) 32 | } 33 | } 34 | 35 | pub(crate) mod file_source; 36 | 37 | #[cfg(feature = "toml")] 38 | pub(crate) mod toml_source; 39 | 40 | #[cfg(feature = "json")] 41 | pub(crate) mod json_source; 42 | 43 | #[cfg(feature = "env")] 44 | pub(crate) mod env_source; 45 | 46 | pub(crate) mod offset_source; 47 | 48 | #[cfg(test)] 49 | pub mod test { 50 | use std::fmt; 51 | 52 | use crate::{ConfigurationBuilder, Source}; 53 | 54 | #[derive(Clone)] 55 | pub(crate) struct TestSource { 56 | pub(crate) data: T, 57 | pub(crate) allow_secrets: bool, 58 | } 59 | 60 | impl fmt::Debug for TestSource { 61 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | f.debug_struct("TestSource") 63 | .field("allow_secrets", &self.allow_secrets) 64 | .finish_non_exhaustive() 65 | } 66 | } 67 | 68 | impl Source for TestSource 69 | where 70 | T: ConfigurationBuilder + Clone, 71 | { 72 | fn provide(&self) -> Result> { 73 | Ok(self.data.clone()) 74 | } 75 | 76 | fn allows_secrets(&self) -> bool { 77 | self.allow_secrets 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | clippy: 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | contents: read 20 | checks: write 21 | 22 | steps: 23 | - uses: actions/checkout@v5 24 | 25 | - name: Install Rust 26 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 27 | with: 28 | components: clippy 29 | 30 | - name: Install just & cargo-hack 31 | uses: taiki-e/install-action@v2.62.60 32 | with: 33 | tool: just,cargo-hack 34 | 35 | - name: Clippy 36 | run: just clippy 37 | 38 | rustfmt: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v5 42 | 43 | - name: Install Rust (nightly) 44 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 45 | with: 46 | toolchain: nightly 47 | components: rustfmt 48 | 49 | - run: cargo fmt -- --check 50 | 51 | docs: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v5 55 | 56 | - name: Install Rust 57 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 58 | with: 59 | components: rust-docs 60 | 61 | - name: Check for broken intra-doc links 62 | env: 63 | RUSTDOCFLAGS: -D warnings 64 | run: cargo doc --workspace --no-deps --all-features 65 | 66 | public-api-diff: 67 | if: false 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v5 71 | with: 72 | ref: ${{ github.base_ref }} 73 | 74 | - uses: actions/checkout@v5 75 | 76 | - name: Install Rust (${{ vars.RUST_VERSION_API_DIFF }}) 77 | uses: actions-rust-lang/setup-rust-toolchain@v1.15.2 78 | with: 79 | toolchain: ${{ vars.RUST_VERSION_API_DIFF }} 80 | 81 | - name: Install cargo-public-api 82 | uses: taiki-e/cache-cargo-install-action@v2.3.1 83 | with: 84 | tool: cargo-public-api 85 | 86 | - name: Generate API diff 87 | run: | 88 | cargo public-api --manifest-path ./confik/Cargo.toml diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} 89 | -------------------------------------------------------------------------------- /confik/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! User-facing error types. 2 | //! 3 | //! Although in theory [`UnexpectedSecret`] and [`MissingValue`] are also user facing, they are 4 | //! entirely handled by the `derive` internals, so is counted as internal. 5 | 6 | use std::{borrow::Cow, error::Error as StdError}; 7 | 8 | use thiserror::Error; 9 | 10 | use crate::{FailedTryInto, MissingValue, UnexpectedSecret}; 11 | 12 | /// Possible error values. 13 | #[derive(Debug, Error)] 14 | #[non_exhaustive] 15 | pub enum Error { 16 | /// The value contained in the `path` was not found when attempting to build 17 | /// the [`Configuration`](crate::Configuration) in 18 | /// [`ConfigurationBuilder::try_build`](crate::ConfigurationBuilder::try_build). 19 | #[error(transparent)] 20 | MissingValue(#[from] MissingValue), 21 | 22 | /// A wrapper around the error from one of the sources. 23 | #[error("Source {1} returned an error")] 24 | Source(#[source] Box, String), 25 | 26 | /// The value contained in the `path` was marked as a [`SecretBuilder`](crate::SecretBuilder) 27 | /// but was parsed from a [`Source`](crate::Source) that was not marked as a secret 28 | /// (see [`Source::allows_secrets`](crate::Source::allows_secrets)). 29 | #[error("Found a secret in source {1} that does not permit secrets")] 30 | UnexpectedSecret(#[source] UnexpectedSecret, String), 31 | 32 | /// The value contained in the `path` was attempted to be converted and that conversion failed. 33 | #[error(transparent)] 34 | TryInto(#[from] FailedTryInto), 35 | } 36 | 37 | impl Error { 38 | /// Used in chaining [`MissingValue`] errors during [`crate::Configuration::try_build`]. 39 | #[doc(hidden)] 40 | #[must_use] 41 | pub fn prepend(self, path_segment: impl Into>) -> Self { 42 | match self { 43 | Self::MissingValue(err) => Self::MissingValue(err.prepend(path_segment)), 44 | Self::TryInto(err) => Self::TryInto(err.prepend(path_segment)), 45 | // This branch will probably never be hit but exists so that the function works the way 46 | // a caller would expect if there is a use case for it in future. 47 | Self::UnexpectedSecret(err, source) => { 48 | Self::UnexpectedSecret(err.prepend(path_segment), source) 49 | } 50 | Self::Source(err, source) => Self::Source(err, source), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /confik/tests/forward/mod.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[allow(dead_code)] // unused in no-default-features cases 4 | #[derive(Configuration, Debug, PartialEq, Eq)] 5 | #[confik(forward(serde(rename_all = "UPPERCASE")))] 6 | struct Container { 7 | field: usize, 8 | } 9 | 10 | #[allow(dead_code)] // unused in no-default-features cases 11 | #[derive(Configuration, Debug, PartialEq, Eq)] 12 | struct Inner { 13 | #[confik(forward(serde(rename = "outer")))] 14 | inner: usize, 15 | } 16 | 17 | #[allow(dead_code)] // unused in no-default-features cases 18 | #[derive(Configuration, Debug, PartialEq, Eq)] 19 | struct Field { 20 | #[confik(forward(serde(rename = "other_name")))] 21 | field1: usize, 22 | #[confik(forward(serde(flatten)))] 23 | field2: Inner, 24 | } 25 | 26 | #[allow(dead_code)] // unused in no-default-features cases 27 | #[derive(Debug, PartialEq, Eq, Configuration)] 28 | enum Clothes { 29 | Hat, 30 | // Put some data in to force use of a custom builder 31 | Scarf(usize), 32 | #[confik(forward(serde(alias = "Gloves", alias = "SomethingElse")))] 33 | Other, 34 | } 35 | 36 | #[allow(dead_code)] // unused in no-default-features cases 37 | #[derive(Configuration)] 38 | struct Cupboard { 39 | items: Vec, 40 | } 41 | 42 | #[cfg(feature = "toml")] 43 | mod toml { 44 | use confik::{Configuration, TomlSource}; 45 | 46 | use super::{Clothes, Container, Cupboard, Field, Inner}; 47 | 48 | #[test] 49 | fn container() { 50 | let target = Container::builder() 51 | .override_with(TomlSource::new("FIELD = 1")) 52 | .try_build() 53 | .expect("Failed to build"); 54 | assert_eq!(target, Container { field: 1 }); 55 | } 56 | 57 | #[test] 58 | fn field() { 59 | let target = Field::builder() 60 | .override_with(TomlSource::new("other_name = 1\nouter = 2")) 61 | .try_build() 62 | .expect("Failed to build"); 63 | assert_eq!( 64 | target, 65 | Field { 66 | field1: 1, 67 | field2: Inner { inner: 2 } 68 | } 69 | ); 70 | } 71 | 72 | #[test] 73 | fn variant() { 74 | let target = Cupboard::builder() 75 | .override_with(TomlSource::new( 76 | r#"items = ["Hat", "Gloves", "SomethingElse"]"#, 77 | )) 78 | .try_build() 79 | .expect("Failed to build"); 80 | assert_eq!( 81 | target.items.as_slice(), 82 | [Clothes::Hat, Clothes::Other, Clothes::Other] 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /confik/tests/unkeyed_containers/mod.rs: -------------------------------------------------------------------------------- 1 | macro_rules! create_tests_for { 2 | ($container:ty) => { 3 | use confik::Configuration; 4 | 5 | #[allow(dead_code)] // unused in no-default-features cases 6 | #[derive(Debug, Configuration, PartialEq, Eq, Hash, Ord, PartialOrd)] 7 | #[confik(forward(derive(Hash, PartialEq, Eq, Ord, PartialOrd)))] 8 | struct TwoVals { 9 | first: usize, 10 | second: usize, 11 | } 12 | 13 | #[allow(dead_code)] // unused in no-default-features cases 14 | #[derive(Debug, Configuration, PartialEq, Eq)] 15 | struct Target { 16 | val: $container, 17 | } 18 | 19 | #[cfg(feature = "toml")] 20 | mod toml { 21 | use confik::{Configuration, TomlSource}; 22 | 23 | use super::{Target, TwoVals}; 24 | 25 | #[test] 26 | fn simple() { 27 | let target = Target::builder() 28 | .override_with(TomlSource::new("val = [{first = 0, second = 1}]")) 29 | .try_build() 30 | .expect("Failed to build container from simple source"); 31 | 32 | assert_eq!( 33 | target.val.iter().collect::>(), 34 | [&TwoVals { 35 | first: 0, 36 | second: 1 37 | }], 38 | "Target not equal to 0, 1: {:?}", 39 | target 40 | ); 41 | } 42 | 43 | #[cfg(feature = "json")] 44 | mod json { 45 | use confik::{Configuration, Error, JsonSource, TomlSource}; 46 | 47 | use super::Target; 48 | 49 | #[test] 50 | fn incomplete() { 51 | let err = Target::builder() 52 | .override_with(TomlSource::new("val = [{ second = 1 }]")) 53 | .override_with(JsonSource::new( 54 | r#"{ "val": [{ "first": 0, "second": null }]"#, 55 | )) 56 | .try_build() 57 | .expect_err("Managed to combine different items across a list?"); 58 | 59 | assert_matches::assert_matches!(&err, Error::Source(_, _)); 60 | } 61 | } 62 | } 63 | }; 64 | } 65 | 66 | mod hashset { 67 | use std::collections::HashSet; 68 | 69 | create_tests_for! { HashSet } 70 | } 71 | 72 | mod btreeset { 73 | use std::collections::BTreeSet; 74 | 75 | create_tests_for! { BTreeSet } 76 | } 77 | 78 | mod vec { 79 | create_tests_for! { Vec } 80 | } 81 | -------------------------------------------------------------------------------- /confik-macros/tests/trybuild.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn macro_pass() { 3 | let t = trybuild::TestCases::new(); 4 | 5 | t.pass("tests/trybuild/01-parse.rs"); 6 | t.pass("tests/trybuild/02-create-builder.rs"); 7 | t.pass("tests/trybuild/03-simple-impl.rs"); 8 | t.pass("tests/trybuild/04-simple-build.rs"); 9 | t.pass("tests/trybuild/05-simple-build-with-enum.rs"); 10 | t.pass("tests/trybuild/06-nested-struct.rs"); 11 | t.pass("tests/trybuild/07-phantom-data.rs"); 12 | t.pass("tests/trybuild/08-redefined-prelude-types.rs"); 13 | t.pass("tests/trybuild/09-pub-target.rs"); 14 | t.pass("tests/trybuild/10-unnamed_struct_fields.rs"); 15 | t.pass("tests/trybuild/11-simple-secret-source.rs"); 16 | t.pass("tests/trybuild/12-complex-secret-source.rs"); 17 | t.pass("tests/trybuild/13-unnamed-secret-source.rs"); 18 | t.pass("tests/trybuild/14-simple-default.rs"); 19 | t.pass("tests/trybuild/15-default-default.rs"); 20 | t.pass("tests/trybuild/16-partial-default.rs"); 21 | t.pass("tests/trybuild/17-comments.rs"); 22 | t.pass("tests/trybuild/18-secret-default.rs"); 23 | t.pass("tests/trybuild/19-derive.rs"); 24 | t.pass("tests/trybuild/20-enum.rs"); 25 | t.pass("tests/trybuild/21-field-from.rs"); 26 | t.pass("tests/trybuild/22-dataless-types.rs"); 27 | t.pass("tests/trybuild/23-where-clause.rs"); 28 | t.pass("tests/trybuild/24-field-try-from.rs"); 29 | t.pass("tests/trybuild/25-pass-enum-untagged.rs"); 30 | t.pass("tests/trybuild/26-named-builder.rs"); 31 | t.pass("tests/trybuild/27-field-access.rs"); 32 | t.pass("tests/trybuild/28-field-vis.rs"); 33 | t.pass("tests/trybuild/29-named-field-vis.rs"); 34 | t.pass("tests/trybuild/30-skip-field.rs"); 35 | t.pass("tests/trybuild/31-crate-remap.rs"); 36 | } 37 | 38 | // only run on MSRV to avoid changes to compiler output causing CI failures 39 | #[rustversion_msrv::msrv] 40 | #[test] 41 | fn macro_fail() { 42 | let t = trybuild::TestCases::new(); 43 | 44 | t.compile_fail("tests/trybuild/fail-default-parse.rs"); 45 | t.compile_fail("tests/trybuild/fail-default-invalid-expr.rs"); 46 | t.compile_fail("tests/trybuild/fail-config-name-value.rs"); 47 | t.compile_fail("tests/trybuild/fail-secret-extra-attr.rs"); 48 | t.compile_fail("tests/trybuild/fail-forward-literal.rs"); 49 | t.compile_fail("tests/trybuild/fail-field-from-unknown-type.rs"); 50 | t.compile_fail("tests/trybuild/fail-uncreatable-type.rs"); 51 | t.compile_fail("tests/trybuild/fail-not-a-type.rs"); 52 | t.compile_fail("tests/trybuild/fail-default-not-expression.rs"); 53 | t.compile_fail("tests/trybuild/fail-from-and-try-from.rs"); 54 | t.compile_fail("tests/trybuild/fail-try-from-not-implemented.rs"); 55 | t.compile_fail("tests/trybuild/fail-crate-not-in-scope.rs"); 56 | } 57 | -------------------------------------------------------------------------------- /confik/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "confik" 3 | version = "0.15.1" 4 | description = "A library for reading application configuration split across multiple sources" 5 | authors.workspace = true 6 | keywords.workspace = true 7 | categories.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | edition.workspace = true 11 | rust-version.workspace = true 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [package.metadata.cargo-machete] 18 | ignored = [ 19 | "humantime_serde", # used in serde macro attributes 20 | "serde_with", # used in doctests 21 | ] 22 | 23 | [features] 24 | default = ["env", "toml"] 25 | 26 | # Source types 27 | env = ["dep:envious"] 28 | json = ["dep:serde_json"] 29 | toml = ["dep:toml"] 30 | 31 | # Destination types 32 | ahash = ["dep:ahash"] 33 | bigdecimal = ["dep:bigdecimal"] 34 | bytesize = ["dep:bytesize"] 35 | camino = ["dep:camino"] 36 | chrono = ["dep:chrono"] 37 | common = [] 38 | ipnetwork = ["dep:ipnetwork"] 39 | js_option = ["dep:js_option"] 40 | rust_decimal = ["dep:rust_decimal"] 41 | secrecy = ["dep:secrecy"] 42 | url = ["dep:url"] 43 | uuid = ["dep:uuid"] 44 | 45 | [dependencies] 46 | confik-macros = "=0.15.1" 47 | 48 | cfg-if = "1" 49 | serde = { version = "1", default-features = false, features = ["std", "derive"] } 50 | thiserror = "2" 51 | 52 | envious = { version = "0.2", optional = true } 53 | serde_json = { version = "1", optional = true } 54 | toml = { version = "0.9", optional = true, default-features = false, features = ["parse", "serde"] } 55 | 56 | ahash = { version = "0.8", optional = true, features = ["serde"] } 57 | bigdecimal = { version = "0.4", optional = true, features = ["serde"] } 58 | bytesize = { version = "2", optional = true, features = ["serde"] } 59 | camino = { version = "1", optional = true, features = ["serde1"] } 60 | chrono = { version = "0.4.42", optional = true, default-features = false, features = ["serde"] } 61 | ipnetwork = { version = "0.21", optional = true, features = ["serde"] } 62 | js_option = { version = "0.2", optional = true, features = ["serde"] } 63 | rust_decimal = { version = "1", optional = true, features = ["serde"] } 64 | secrecy = { version = "0.10", optional = true, features = ["serde"] } 65 | url = { version = "2", optional = true, features = ["serde"] } 66 | uuid = { version = "1", optional = true, features = ["serde"] } 67 | 68 | [dev-dependencies] 69 | assert_matches = "1.5" 70 | humantime-serde = "1" 71 | indoc = "2" 72 | serde_with = "3" 73 | temp-env = "0.3" 74 | tempfile = "3" 75 | 76 | [[example]] 77 | name = "simple" 78 | required-features = ["toml"] 79 | 80 | [[example]] 81 | name = "derives" 82 | required-features = ["toml", "camino"] 83 | 84 | [[example]] 85 | name = "secret_string" 86 | required-features = ["toml", "secrecy"] 87 | 88 | [[example]] 89 | name = "bigdecimal" 90 | required-features = ["toml", "bigdecimal"] 91 | 92 | [[example]] 93 | name = "ahash" 94 | required-features = ["toml", "ahash"] 95 | -------------------------------------------------------------------------------- /confik/src/sources/env_source.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use crate::{ConfigurationBuilder, Source}; 4 | 5 | /// A [`Source`] referring to environment variables. 6 | /// 7 | /// Uses the [envious](https://docs.rs/envious) crate for interpreting env vars. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// use confik::{ConfigBuilder, Configuration, EnvSource}; 13 | /// 14 | /// #[derive(Configuration)] 15 | /// struct Config { 16 | /// port: u16, 17 | /// } 18 | /// 19 | /// std::env::set_var("PORT", "1234"); 20 | /// 21 | /// let config = ConfigBuilder::::default() 22 | /// .override_with(EnvSource::new()) 23 | /// .try_build() 24 | /// .unwrap(); 25 | /// 26 | /// assert_eq!(config.port, 1234); 27 | /// ``` 28 | #[derive(Debug, Clone)] 29 | pub struct EnvSource<'a> { 30 | config: envious::Config<'a>, 31 | allow_secrets: bool, 32 | } 33 | 34 | impl Default for EnvSource<'_> { 35 | fn default() -> Self { 36 | Self::new() 37 | } 38 | } 39 | 40 | impl<'a> EnvSource<'a> { 41 | /// Creates a new [`Source`] referring to environment variables. 42 | pub fn new() -> Self { 43 | Self { 44 | config: envious::Config::new(), 45 | allow_secrets: false, 46 | } 47 | } 48 | 49 | /// Sets the envious prefix. 50 | /// 51 | /// See [`envious::Config::with_prefix()`]. 52 | pub fn with_prefix(mut self, prefix: &'a str) -> Self { 53 | self.config.with_prefix(prefix); 54 | self 55 | } 56 | 57 | /// Sets the envious separator. 58 | /// 59 | /// See [`envious::Config::with_separator()`]. 60 | pub fn with_separator(mut self, separator: &'a str) -> Self { 61 | self.config.with_separator(separator); 62 | self 63 | } 64 | 65 | /// Sets the envious config. 66 | pub fn with_config(mut self, config: envious::Config<'a>) -> Self { 67 | self.config = config; 68 | self 69 | } 70 | 71 | /// Allows this source to contain secrets. 72 | pub fn allow_secrets(mut self) -> Self { 73 | self.allow_secrets = true; 74 | self 75 | } 76 | } 77 | 78 | impl Source for EnvSource<'_> { 79 | fn allows_secrets(&self) -> bool { 80 | self.allow_secrets 81 | } 82 | 83 | fn provide(&self) -> Result> { 84 | Ok(self.config.build_from_env()?) 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | #[test] 93 | fn separator() { 94 | let mut config = envious::Config::new(); 95 | config.with_separator("++"); 96 | config.with_prefix("CFG--"); 97 | let config_debug = format!("{config:?}"); 98 | 99 | let source = EnvSource::default() 100 | .with_prefix("CFG--") 101 | .with_separator("++"); 102 | let source_debug = format!("{source:?}"); 103 | 104 | assert!(source_debug.contains(&config_debug)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | _list: 2 | @just --list 3 | 4 | # Lint workspace with Clippy 5 | clippy: 6 | cargo clippy --workspace --no-default-features 7 | cargo clippy --workspace --all-features 8 | cargo hack --feature-powerset --depth=3 clippy --workspace 9 | 10 | msrv := ``` 11 | cargo metadata --format-version=1 \ 12 | | jq -r 'first(.packages[] | select(.source == null and .rust_version)) | .rust_version' \ 13 | | sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/' 14 | ``` 15 | msrv_rustup := "+" + msrv 16 | 17 | # Downgrade dev-dependencies necessary to run MSRV checks/tests. 18 | [private] 19 | downgrade-for-msrv: 20 | cargo update -p=serde_with --precise=3.12.0 # next ver: 1.74.0 21 | cargo update -p=idna_adapter --precise=1.2.0 # next ver: 1.82.0 22 | cargo update -p=litemap --precise=0.7.3 # next ver: 1.71.1 23 | cargo update -p=zerofrom --precise=0.1.4 # next ver: 1.71.1 24 | cargo update -p=yoke --precise=0.7.4 # next ver: 1.71.1 25 | 26 | # Test workspace using MSRV 27 | test-msrv: downgrade-for-msrv (test-no-coverage msrv_rustup) 28 | 29 | # Test workspace without generating coverage files 30 | [private] 31 | test-no-coverage toolchain="": 32 | cargo {{ toolchain }} test --lib --tests --package=confik-macros 33 | cargo {{ toolchain }} nextest run --package=confik --no-default-features 34 | cargo {{ toolchain }} nextest run --package=confik --all-features 35 | cargo {{ toolchain }} test --doc --workspace --all-features 36 | RUSTDOCFLAGS="-D warnings" cargo {{ toolchain }} doc --workspace --no-deps --all-features 37 | 38 | # Test workspace and generate coverage files 39 | test toolchain="": (test-no-coverage toolchain) 40 | @just test-coverage-codecov {{ toolchain }} 41 | @just test-coverage-lcov {{ toolchain }} 42 | 43 | # Test workspace and generate Codecov coverage file 44 | test-coverage-codecov toolchain="": 45 | cargo {{ toolchain }} llvm-cov --workspace --all-features --codecov --output-path codecov.json 46 | 47 | # Test workspace and generate LCOV coverage file 48 | test-coverage-lcov toolchain="": 49 | cargo {{ toolchain }} llvm-cov --workspace --all-features --lcov --output-path lcov.info 50 | 51 | # Document workspace 52 | doc: 53 | RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features 54 | 55 | # Document workspace and watch for changes 56 | doc-watch: 57 | RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features --open 58 | cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --all-features 59 | 60 | # Check project 61 | check: 62 | just --unstable --fmt --check 63 | nixpkgs-fmt --check . 64 | fd --type=file --hidden --extension=md --extension=yml --exec-batch prettier --check 65 | fd --hidden --extension=toml --exec-batch taplo format --check 66 | fd --hidden --extension=toml --exec-batch taplo lint 67 | cargo +nightly fmt -- --check 68 | cargo clippy --workspace --all-features 69 | 70 | # Format project 71 | fmt: 72 | just --unstable --fmt 73 | nixpkgs-fmt . 74 | fd --type=file --hidden --extension=md --extension=yml --exec-batch prettier --write 75 | fd --hidden --extension=toml --exec-batch taplo format 76 | cargo +nightly fmt 77 | -------------------------------------------------------------------------------- /confik/tests/singly_nested_tests/mod.rs: -------------------------------------------------------------------------------- 1 | use confik_macros::Configuration; 2 | 3 | #[allow(dead_code)] // unused in no-default-features cases 4 | #[derive(Debug, PartialEq, Eq, Configuration)] 5 | struct NestedTargetRoot { 6 | inner: NestedTargetLeaf, 7 | } 8 | 9 | #[allow(dead_code)] // unused in no-default-features cases 10 | #[derive(Debug, PartialEq, Eq, Configuration)] 11 | struct NestedTargetLeaf { 12 | data: usize, 13 | } 14 | 15 | #[cfg(feature = "json")] 16 | mod json { 17 | use confik::{ConfigBuilder, JsonSource}; 18 | 19 | use super::{NestedTargetLeaf, NestedTargetRoot}; 20 | 21 | #[test] 22 | fn check_nested_json() { 23 | assert_eq!( 24 | ConfigBuilder::::default() 25 | .override_with(JsonSource::new(r#"{"inner": {"data": 1}}"#)) 26 | .try_build() 27 | .expect("JSON deserialization should succeed"), 28 | NestedTargetRoot { 29 | inner: NestedTargetLeaf { data: 1 } 30 | } 31 | ); 32 | } 33 | } 34 | 35 | #[cfg(feature = "toml")] 36 | mod toml { 37 | use assert_matches::assert_matches; 38 | use confik::{ConfigBuilder, Error, TomlSource}; 39 | 40 | use super::{NestedTargetLeaf, NestedTargetRoot}; 41 | 42 | #[test] 43 | fn check_nested_toml() { 44 | assert_eq!( 45 | ConfigBuilder::::default() 46 | .override_with(TomlSource::new("[inner]\ndata = 2")) 47 | .try_build() 48 | .expect("Toml deserialization should succeed"), 49 | NestedTargetRoot { 50 | inner: NestedTargetLeaf { data: 2 } 51 | } 52 | ); 53 | } 54 | 55 | #[test] 56 | fn check_missing_path() { 57 | assert_matches!( 58 | ConfigBuilder::::default() 59 | .override_with(TomlSource::new("[inner]")) 60 | .try_build() 61 | .expect_err("Missing data"), 62 | Error::MissingValue(path) if path.to_string().contains("`inner.data`") 63 | ); 64 | } 65 | 66 | #[cfg(feature = "json")] 67 | mod json { 68 | use confik::{ConfigBuilder, JsonSource, TomlSource}; 69 | 70 | use super::{NestedTargetLeaf, NestedTargetRoot}; 71 | 72 | #[test] 73 | fn check_nested_multi_source() { 74 | assert_eq!( 75 | ConfigBuilder::::default() 76 | .override_with(JsonSource::new(r#"{}"#)) 77 | .override_with(TomlSource::new("[inner]\ndata = 2")) 78 | .try_build() 79 | .expect("JSON + Toml deserialization should succeed"), 80 | NestedTargetRoot { 81 | inner: NestedTargetLeaf { data: 2 } 82 | } 83 | ); 84 | assert_eq!( 85 | ConfigBuilder::::default() 86 | .override_with(TomlSource::new("")) 87 | .override_with(JsonSource::new(r#"{"inner": {"data": 1}}"#)) 88 | .try_build() 89 | .expect("JSON + Toml deserialization should succeed"), 90 | NestedTargetRoot { 91 | inner: NestedTargetLeaf { data: 1 } 92 | } 93 | ); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /confik/src/secrets.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{de::DeserializeOwned, Deserialize}; 4 | use thiserror::Error; 5 | 6 | use crate::{path::Path, Configuration, ConfigurationBuilder, Error, MissingValue}; 7 | 8 | /// Captures the path of a secret found in a non-secret source. 9 | #[derive(Debug, Default, Error)] 10 | #[error("Found secret at path `{0}`")] 11 | pub struct UnexpectedSecret(Path); 12 | 13 | impl UnexpectedSecret { 14 | /// Prepends a path segment as we return back up the call-stack. 15 | #[must_use] 16 | pub fn prepend(mut self, path_segment: impl Into>) -> Self { 17 | self.0 .0.push(path_segment.into()); 18 | self 19 | } 20 | } 21 | 22 | /// Wrapper type for carrying secrets, auto-applied to builders when using the `#[config(secret)]` 23 | /// attribute. 24 | /// 25 | /// This type causes non-secret sources (see [`Source::allows_secrets`](crate::Source::allows_secrets)) 26 | /// to error when they parse a value of this type, ensuring that only sources which allow secrets 27 | /// contain them. 28 | /// 29 | /// This is the only source of errors for [`ConfigurationBuilder::contains_non_secret_data`]. 30 | #[derive(Debug, Default, Deserialize)] 31 | #[serde(bound = "T: DeserializeOwned")] 32 | pub struct SecretBuilder(T); 33 | 34 | impl SecretBuilder { 35 | #[must_use] 36 | pub fn merge(self, other: Self) -> Self { 37 | Self(self.0.merge(other.0)) 38 | } 39 | 40 | pub fn try_build(self) -> Result { 41 | self.0.try_build() 42 | } 43 | 44 | pub fn contains_non_secret_data(&self) -> Result { 45 | // Stop at the earliest secret, so even if we contain further `SecretBuilder`s, which have 46 | // returned an `Err`, reset the path. 47 | if self.0.contains_non_secret_data().unwrap_or(true) { 48 | Err(UnexpectedSecret::default()) 49 | } else { 50 | Ok(false) 51 | } 52 | } 53 | } 54 | 55 | /// Builder for trivial types that always contain secrets, regardless of the presence of 56 | /// `#[confik(secret)]` annotations. 57 | /// 58 | /// This cannot be used for any case where an `Option` cannot be used as a builder, and will 59 | /// not descend into the structure. 60 | #[derive(Debug, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)] 61 | #[serde(transparent)] 62 | pub struct SecretOption(Option); 63 | 64 | impl Default for SecretOption { 65 | fn default() -> Self { 66 | Self(None) 67 | } 68 | } 69 | 70 | impl ConfigurationBuilder for SecretOption 71 | where 72 | T: serde::de::DeserializeOwned + Configuration, 73 | { 74 | type Target = T; 75 | 76 | fn merge(self, other: Self) -> Self { 77 | Self(self.0.or(other.0)) 78 | } 79 | 80 | fn try_build(self) -> Result { 81 | self.0 82 | .ok_or_else(|| Error::MissingValue(MissingValue::default())) 83 | } 84 | 85 | /// Should not have an `Option` wrapping a secret as ` as ConfigurationBuilder` is 86 | /// used for terminal types, therefore the `SecretBuilder` wrapping would be external to it. 87 | fn contains_non_secret_data(&self) -> Result { 88 | match self.0 { 89 | Some(_) => Err(UnexpectedSecret::default()), 90 | None => Ok(false), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /confik/tests/array/mod.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[allow(dead_code)] // unused in no-default-features cases 4 | #[derive(Debug, Configuration, Eq, PartialEq)] 5 | struct NonCopy(usize); 6 | 7 | #[allow(dead_code)] // unused in no-default-features cases 8 | #[derive(Debug, Configuration, Eq, PartialEq)] 9 | struct Target { 10 | val: [NonCopy; 10], 11 | } 12 | 13 | #[cfg(feature = "toml")] 14 | mod toml { 15 | use confik::{Configuration, TomlSource}; 16 | 17 | use super::{NonCopy, Target}; 18 | 19 | #[test] 20 | fn success() { 21 | let target = Target::builder() 22 | .override_with(TomlSource::new("val = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]")) 23 | .try_build() 24 | .expect("Failed to build array"); 25 | assert_eq!( 26 | target, 27 | Target { 28 | val: [ 29 | NonCopy(0), 30 | NonCopy(1), 31 | NonCopy(2), 32 | NonCopy(3), 33 | NonCopy(4), 34 | NonCopy(5), 35 | NonCopy(6), 36 | NonCopy(7), 37 | NonCopy(8), 38 | NonCopy(9) 39 | ] 40 | } 41 | ); 42 | } 43 | } 44 | 45 | #[cfg(feature = "json")] 46 | mod json { 47 | use assert_matches::assert_matches; 48 | use confik::{Configuration, Error, JsonSource}; 49 | 50 | use super::{NonCopy, Target}; 51 | 52 | #[test] 53 | fn too_short() { 54 | Target::builder() 55 | .override_with(JsonSource::new("{\"val\": [0, 1, 2, 3, 4, 5, 6, 7, 8]}")) 56 | .try_build() 57 | .expect_err("Built array too short"); 58 | } 59 | 60 | #[test] 61 | fn too_short_with_null() { 62 | let err = Target::builder() 63 | .override_with(JsonSource::new( 64 | "{\"val\": [0, 1, 2, 3, 4, 5, 6, 7, 8, null]}", 65 | )) 66 | .try_build() 67 | .expect_err("Built array too short"); 68 | assert_matches!(err, Error::MissingValue(path) if path.to_string().contains("val.9.0")); 69 | } 70 | 71 | #[test] 72 | fn too_long() { 73 | Target::builder() 74 | .override_with(JsonSource::new( 75 | "{\"val\": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}", 76 | )) 77 | .try_build() 78 | .expect_err("Built array too long"); 79 | } 80 | 81 | #[test] 82 | fn merge() { 83 | let target = Target::builder() 84 | .override_with(JsonSource::new( 85 | "{\"val\": [0, null, 2, null, 4, null, 6, null, 8, null]}", 86 | )) 87 | .override_with(JsonSource::new( 88 | "{\"val\": [null, 1, null, 3, null, 5, null, 7, null, 9]}", 89 | )) 90 | .try_build() 91 | .expect("Merged array failure"); 92 | assert_eq!( 93 | target, 94 | Target { 95 | val: [ 96 | NonCopy(0), 97 | NonCopy(1), 98 | NonCopy(2), 99 | NonCopy(3), 100 | NonCopy(4), 101 | NonCopy(5), 102 | NonCopy(6), 103 | NonCopy(7), 104 | NonCopy(8), 105 | NonCopy(9) 106 | ] 107 | } 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /confik/src/common.rs: -------------------------------------------------------------------------------- 1 | //! Useful configuration types that services will likely otherwise re-implement. 2 | 3 | use std::{fmt, str}; 4 | 5 | use crate::{Configuration, MissingValue}; 6 | 7 | /// The database type, used to determine the connection string format 8 | #[derive(Debug, Clone, PartialEq, Eq, Configuration)] 9 | #[confik(forward(serde(rename_all = "lowercase")))] 10 | enum DatabaseKind { 11 | Mysql, 12 | Postgres, 13 | } 14 | 15 | impl str::FromStr for DatabaseKind { 16 | type Err = MissingValue; 17 | 18 | fn from_str(input: &str) -> Result { 19 | match () { 20 | _ if input.eq_ignore_ascii_case("mysql") => Ok(Self::Mysql), 21 | _ if input.eq_ignore_ascii_case("postgres") => Ok(Self::Postgres), 22 | _ => Err(Self::Err::default()), 23 | } 24 | } 25 | } 26 | 27 | impl fmt::Display for DatabaseKind { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Self::Mysql => f.write_str("mysql"), 31 | Self::Postgres => f.write_str("postgres"), 32 | } 33 | } 34 | } 35 | 36 | /// Database connection configuration, with a secret `password`. 37 | /// 38 | /// The [`Display`] impl provides the full connection string, whereas [`Debug`] is as normal, but 39 | /// with the `password` field value replaced by `[redacted]`. 40 | /// 41 | /// See [`SecretBuilder`](crate::SecretBuilder) for details on secrets. NOTE: The [`Debug`] hiding 42 | /// of the field is manually implemented for this type, and is not automatically handled by 43 | /// `#[config(secret)]`. 44 | /// 45 | /// [`Display`]: #impl-Display-for-DatabaseConnectionConfig 46 | /// [`Debug`]: #impl-Debug-for-DatabaseConnectionConfig 47 | #[derive(Clone, Configuration)] 48 | pub struct DatabaseConnectionConfig { 49 | database: DatabaseKind, 50 | 51 | username: String, 52 | 53 | #[confik(secret)] 54 | password: String, 55 | 56 | path: String, 57 | } 58 | 59 | impl fmt::Debug for DatabaseConnectionConfig { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | f.debug_struct("DatabaseConnectionConfig") 62 | .field("database", &self.database) 63 | .field("username", &self.username) 64 | .field("password", &"[redacted]") 65 | .field("path", &self.path) 66 | .finish() 67 | } 68 | } 69 | 70 | impl fmt::Display for DatabaseConnectionConfig { 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | write!( 73 | f, 74 | "{}://{}:{}@{}", 75 | self.database, self.username, self.password, self.path 76 | ) 77 | } 78 | } 79 | 80 | impl str::FromStr for DatabaseConnectionConfig { 81 | type Err = MissingValue; 82 | 83 | fn from_str(input: &str) -> Result { 84 | let Some((database, input)) = input.split_once("://") else { 85 | return Err(Self::Err::default().prepend("database")); 86 | }; 87 | 88 | let database = database 89 | .parse() 90 | .map_err(|err: MissingValue| err.prepend("database".to_string()))?; 91 | 92 | let Some((username, input)) = input.split_once(':') else { 93 | return Err(Self::Err::default().prepend("username".to_string())); 94 | }; 95 | 96 | let Some((password, path)) = input.split_once('@') else { 97 | return Err(Self::Err::default().prepend("path".to_string())); 98 | }; 99 | 100 | Ok(Self { 101 | database, 102 | username: username.to_owned(), 103 | password: password.to_owned(), 104 | path: path.to_owned(), 105 | }) 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | 113 | #[test] 114 | fn parse_connection_string() { 115 | let db_config = "mysql://root:foo@localhost:3307" 116 | .parse::() 117 | .unwrap(); 118 | assert_eq!(db_config.database, DatabaseKind::Mysql); 119 | assert_eq!(db_config.username, "root"); 120 | assert_eq!(db_config.password, "foo"); 121 | assert_eq!(db_config.path, "localhost:3307"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /confik/src/builder.rs: -------------------------------------------------------------------------------- 1 | //! A general builder struct to allow for adding sources gradually, instead of requiring 2 | //! producing your own list for [`build_from_sources`]. 3 | //! 4 | //! The sources are consumed in the order they are provided, with priority given to the first 5 | //! source. E.g., If sources `Source::File("config.toml")` and `Source::File("defaults.toml")` are 6 | //! provided any values specified in `config.toml` take precedence over `defaults.toml`. 7 | //! 8 | //! A builder will generally be created by calling [`ConfigBuilder::default`], sources will be added 9 | //! with [`ConfigBuilder::override_with`] which overrides existing source with the new source, and 10 | //! then your configuration built with [`ConfigBuilder::try_build`]. 11 | 12 | use std::{marker::PhantomData, mem}; 13 | 14 | use confik::sources::DefaultSource; 15 | 16 | use crate::{build_from_sources, sources::Source, Configuration, Error}; 17 | 18 | /// Used to accumulate ordered sources from which its `Target` is to be built. 19 | /// 20 | /// An instance of this can be created via [`Configuration::builder`] or 21 | /// [`ConfigBuilder::::default`]. 22 | /// 23 | /// # Examples 24 | /// 25 | /// Using [`Configuration::builder`]: 26 | /// 27 | /// ``` 28 | /// # #[cfg(feature = "toml")] 29 | /// # { 30 | /// use confik::{Configuration, TomlSource}; 31 | /// 32 | /// #[derive(Debug, PartialEq, Configuration)] 33 | /// struct MyConfigType { 34 | /// param: String, 35 | /// } 36 | /// 37 | /// let config = MyConfigType::builder() 38 | /// .override_with(TomlSource::new(r#"param = "Hello World""#)) 39 | /// .try_build() 40 | /// .expect("Failed to build"); 41 | /// 42 | /// assert_eq!(config.param, "Hello World"); 43 | /// # } 44 | /// ``` 45 | /// 46 | /// Using [`ConfigBuilder::::default`]: 47 | /// 48 | /// ``` 49 | /// # #[cfg(feature = "toml")] 50 | /// # { 51 | /// use confik::{ConfigBuilder, Configuration, TomlSource}; 52 | /// 53 | /// #[derive(Debug, PartialEq, Configuration)] 54 | /// struct MyConfigType { 55 | /// param: String, 56 | /// } 57 | /// 58 | /// let config = ConfigBuilder::::default() 59 | /// .override_with(TomlSource::new(r#"param = "Hello World""#)) 60 | /// .try_build() 61 | /// .expect("Failed to build"); 62 | /// 63 | /// assert_eq!(config.param, "Hello World"); 64 | /// # } 65 | /// ``` 66 | pub struct ConfigBuilder<'a, Target: Configuration> { 67 | sources: Vec + 'a>>, 68 | 69 | /// Use the generic parameter 70 | _phantom: PhantomData Target>, 71 | } 72 | 73 | impl<'a, Target: Configuration> ConfigBuilder<'a, Target> { 74 | /// Add a single [`Source`] to the list of sources. 75 | /// 76 | /// The source is added at the end of the list, overriding existing sources. 77 | /// 78 | /// ``` 79 | /// # #[cfg(feature = "toml")] 80 | /// # { 81 | /// use confik::{Configuration, TomlSource}; 82 | /// #[derive(Debug, PartialEq, Configuration)] 83 | /// struct MyConfigType { 84 | /// param: String, 85 | /// } 86 | /// 87 | /// let config = MyConfigType::builder() 88 | /// .override_with(TomlSource::new(r#"param = "Hello World""#)) 89 | /// .override_with(TomlSource::new(r#"param = "Hello Universe""#)) 90 | /// .try_build() 91 | /// .expect("Failed to build"); 92 | /// 93 | /// assert_eq!(config.param, "Hello Universe"); 94 | /// # } 95 | /// ``` 96 | pub fn override_with(&mut self, source: impl Source + 'a) -> &mut Self { 97 | self.sources.push(Box::new(source)); 98 | self 99 | } 100 | 101 | /// Attempt to build from the provided sources. 102 | /// 103 | /// # Errors 104 | /// 105 | /// Returns an error if a required value is missing, a secret value was provided in a non-secret 106 | /// source, or an error is returned from a source (e.g., invalid TOML). See [`Error`] for more 107 | /// details. 108 | pub fn try_build(&mut self) -> Result { 109 | if self.sources.is_empty() { 110 | build_from_sources([Box::new(DefaultSource) as Box>]) 111 | } else { 112 | build_from_sources(mem::take(&mut self.sources).into_iter().rev()) 113 | } 114 | } 115 | } 116 | 117 | impl Default for ConfigBuilder<'_, Target> { 118 | fn default() -> Self { 119 | Self { 120 | sources: Vec::new(), 121 | _phantom: PhantomData, 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /confik/tests/main.rs: -------------------------------------------------------------------------------- 1 | mod array; 2 | #[cfg(all(feature = "common", feature = "toml"))] 3 | mod common; 4 | mod complex_enums; 5 | mod defaulting_containers; 6 | mod forward; 7 | mod keyed_containers; 8 | mod option_builder; 9 | mod secret; 10 | mod secret_option; 11 | mod singly_nested_tests; 12 | mod third_party; 13 | mod unkeyed_containers; 14 | 15 | use assert_matches::assert_matches; 16 | use confik::{ConfigBuilder, Configuration, Error}; 17 | use serde::Deserialize; 18 | 19 | #[derive(Debug, PartialEq, Eq, Deserialize, Configuration)] 20 | enum TargetEnum { 21 | First, 22 | Second, 23 | } 24 | 25 | #[derive(Debug, PartialEq, Eq, Deserialize, Configuration)] 26 | struct Target { 27 | a: usize, 28 | b: TargetEnum, 29 | } 30 | 31 | #[test] 32 | fn check_no_source_fails() { 33 | assert_matches!( 34 | ConfigBuilder::::default().try_build(), 35 | Err(Error::MissingValue(path)) if path.to_string().contains('a') 36 | ); 37 | } 38 | 39 | #[cfg(feature = "json")] 40 | mod json { 41 | use confik::{ConfigBuilder, JsonSource}; 42 | 43 | use crate::{Target, TargetEnum}; 44 | 45 | #[test] 46 | fn check_json() { 47 | assert_eq!( 48 | ConfigBuilder::::default() 49 | .override_with(JsonSource::new(r#"{"a": 1, "b": "First"}"#)) 50 | .try_build() 51 | .expect("JSON deserialization should succeed"), 52 | Target { 53 | a: 1, 54 | b: TargetEnum::First, 55 | } 56 | ); 57 | } 58 | } 59 | 60 | #[cfg(feature = "toml")] 61 | mod toml { 62 | use std::time::Duration; 63 | 64 | use confik::{ConfigBuilder, TomlSource}; 65 | use confik_macros::Configuration; 66 | 67 | use crate::{Target, TargetEnum}; 68 | 69 | #[test] 70 | fn check_toml() { 71 | assert_eq!( 72 | ConfigBuilder::::default() 73 | .override_with(TomlSource::new("a = 2\nb = \"Second\"")) 74 | .try_build() 75 | .expect("Toml deserialization should succeed"), 76 | Target { 77 | a: 2, 78 | b: TargetEnum::Second, 79 | } 80 | ); 81 | } 82 | 83 | #[test] 84 | fn from_humantime() { 85 | #[derive(Debug, PartialEq, Eq, Configuration)] 86 | struct Config { 87 | #[confik(forward(serde(with = "humantime_serde")))] 88 | timeout: Duration, 89 | } 90 | 91 | let config = ConfigBuilder::::default() 92 | .override_with(TomlSource::new("timeout = \"1h 42m\"")) 93 | .try_build() 94 | .unwrap(); 95 | 96 | assert_eq!( 97 | config, 98 | Config { 99 | timeout: Duration::from_secs(6_120) 100 | } 101 | ); 102 | } 103 | 104 | #[cfg(feature = "json")] 105 | mod json { 106 | use confik::{ConfigBuilder, JsonSource, TomlSource}; 107 | 108 | use crate::{Target, TargetEnum}; 109 | 110 | #[test] 111 | fn check_multi_source() { 112 | assert_eq!( 113 | ConfigBuilder::::default() 114 | .override_with(JsonSource::new(r#"{"b": "First"}"#)) 115 | .override_with(TomlSource::new("a = 2")) 116 | .try_build() 117 | .expect("JSON + Toml deserialization should succeed"), 118 | Target { 119 | a: 2, 120 | b: TargetEnum::First, 121 | } 122 | ); 123 | } 124 | 125 | #[test] 126 | fn check_source_order() { 127 | assert_eq!( 128 | ConfigBuilder::::default() 129 | .override_with(TomlSource::new("a = 2\nb = \"Second\"")) 130 | .override_with(JsonSource::new(r#"{"a": 1, "b": "First"}"#)) 131 | .try_build() 132 | .expect("JSON + Toml deserialization should succeed"), 133 | Target { 134 | a: 1, 135 | b: TargetEnum::First, 136 | } 137 | ); 138 | assert_eq!( 139 | ConfigBuilder::::default() 140 | .override_with(JsonSource::new(r#"{"a": 1, "b": "First"}"#)) 141 | .override_with(TomlSource::new("a = 2\nb = \"Second\"")) 142 | .try_build() 143 | .expect("Toml + JSON deserialization should succeed"), 144 | Target { 145 | a: 2, 146 | b: TargetEnum::Second, 147 | } 148 | ); 149 | } 150 | 151 | #[test] 152 | fn check_error_propagation() { 153 | assert!(ConfigBuilder::::default() 154 | .override_with(TomlSource::new("a = 2\nb = \"Second\"")) 155 | .override_with(JsonSource::new(r#"{"a": 1, "#)) 156 | .try_build() 157 | .is_err()); 158 | assert!(ConfigBuilder::::default() 159 | .override_with(JsonSource::new(r#"{"a": 1, "#)) 160 | .override_with(TomlSource::new("a = 2\nb = \"Second\"")) 161 | .try_build() 162 | .is_err()); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /confik/tests/keyed_containers/mod.rs: -------------------------------------------------------------------------------- 1 | macro_rules! create_tests_for { 2 | ($container:ty) => { 3 | use confik::Configuration; 4 | 5 | #[allow(dead_code)] // unused in no-default-features cases 6 | #[derive(Debug, Configuration, PartialEq, Eq, Hash, Ord, PartialOrd)] 7 | #[confik(forward(derive(Hash, PartialEq, Eq, Ord, PartialOrd)))] 8 | struct TwoVals { 9 | first: usize, 10 | second: usize, 11 | } 12 | 13 | #[allow(dead_code)] // unused in no-default-features cases 14 | #[derive(Debug, Configuration, PartialEq, Eq)] 15 | struct Target { 16 | val: $container, 17 | } 18 | 19 | #[cfg(feature = "toml")] 20 | mod toml { 21 | use confik::{Configuration, TomlSource}; 22 | 23 | use super::*; 24 | 25 | #[test] 26 | fn simple() { 27 | let target = Target::builder() 28 | .override_with(TomlSource::new("[val]\nkey = { first = 0, second = 1 }")) 29 | .try_build() 30 | .expect("Failed to build container from simple source"); 31 | 32 | assert_eq!( 33 | target.val.iter().collect::>(), 34 | [( 35 | &"key".to_string(), 36 | &TwoVals { 37 | first: 0, 38 | second: 1, 39 | } 40 | )], 41 | "Target not equal to 0, 1: {:?}", 42 | target 43 | ); 44 | } 45 | 46 | #[test] 47 | fn multiple_one_source() { 48 | let target = Target::builder() 49 | .override_with(TomlSource::new( 50 | "[val]\nkey1 = { first = 0, second = 1 }\nkey2 = { first = 2, second = 3 }", 51 | )) 52 | .try_build() 53 | .expect("Failed to build container from simple source"); 54 | 55 | let mut result = target.val.iter().collect::>(); 56 | result.sort_unstable_by_key(|(key, _)| key.clone()); 57 | 58 | assert_eq!( 59 | result, 60 | [ 61 | ( 62 | &"key1".to_string(), 63 | &TwoVals { 64 | first: 0, 65 | second: 1, 66 | } 67 | ), 68 | ( 69 | &"key2".to_string(), 70 | &TwoVals { 71 | first: 2, 72 | second: 3, 73 | } 74 | ) 75 | ], 76 | "Target not equal to 0, 1: {:?}", 77 | target 78 | ); 79 | } 80 | 81 | #[test] 82 | fn multiple_multiple_source() { 83 | let target = Target::builder() 84 | .override_with(TomlSource::new("[val]\nkey1 = { first = 0, second = 1 }")) 85 | .override_with(TomlSource::new("[val]\nkey2 = { first = 2, second = 3 }")) 86 | .try_build() 87 | .expect("Failed to build container from simple source"); 88 | 89 | let mut result = target.val.iter().collect::>(); 90 | result.sort_unstable_by_key(|(key, _)| key.clone()); 91 | 92 | assert_eq!( 93 | result, 94 | [ 95 | ( 96 | &"key1".to_string(), 97 | &TwoVals { 98 | first: 0, 99 | second: 1, 100 | } 101 | ), 102 | ( 103 | &"key2".to_string(), 104 | &TwoVals { 105 | first: 2, 106 | second: 3, 107 | } 108 | ) 109 | ], 110 | "Target not equal to 0, 1: {:?}", 111 | target 112 | ); 113 | } 114 | } 115 | 116 | #[cfg(feature = "json")] 117 | #[cfg(feature = "toml")] 118 | mod json { 119 | use confik::{Configuration, JsonSource, TomlSource}; 120 | 121 | use super::*; 122 | 123 | #[test] 124 | fn incomplete() { 125 | let target = Target::builder() 126 | .override_with(TomlSource::new("[val]\nkey = { second = 1 }")) 127 | .override_with(JsonSource::new( 128 | r#"{ "val": { "key": { "first": 0, "second": null }}}"#, 129 | )) 130 | .try_build() 131 | .expect("Should be able to build a map from multiple sources"); 132 | 133 | assert_eq!( 134 | target.val.iter().collect::>(), 135 | [( 136 | &"key".to_string(), 137 | &TwoVals { 138 | first: 0, 139 | second: 1, 140 | } 141 | )], 142 | "Target not equal to 0, 1: {:?}", 143 | target 144 | ); 145 | } 146 | } 147 | }; 148 | } 149 | 150 | mod hashmap { 151 | use std::collections::HashMap; 152 | 153 | create_tests_for! { HashMap } 154 | } 155 | 156 | mod btreemap { 157 | use std::collections::BTreeMap; 158 | 159 | create_tests_for! { BTreeMap } 160 | } 161 | -------------------------------------------------------------------------------- /confik/tests/complex_enums/mod.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[allow(dead_code)] // unused in no-default-features cases 4 | #[derive(Configuration, Debug, PartialEq, Eq)] 5 | enum Target { 6 | Simple, 7 | Tuple(usize, usize), 8 | Field { field1: usize, field2: usize }, 9 | } 10 | 11 | #[allow(dead_code)] // unused in no-default-features cases 12 | #[derive(Configuration, Debug, PartialEq, Eq)] 13 | struct RootTarget { 14 | target: Target, 15 | } 16 | 17 | #[cfg(feature = "toml")] 18 | mod toml { 19 | use assert_matches::assert_matches; 20 | use confik::{ConfigBuilder, Error, TomlSource}; 21 | 22 | use super::{RootTarget, Target}; 23 | 24 | #[test] 25 | fn undefined() { 26 | let err = ConfigBuilder::::default() 27 | .override_with(TomlSource::new("")) 28 | .try_build() 29 | .expect_err("Somehow built with no data"); 30 | assert_matches!( 31 | err, 32 | Error::MissingValue(path) if path.to_string().contains("`target`") 33 | ); 34 | } 35 | 36 | #[test] 37 | fn simple_variant() { 38 | let target = ConfigBuilder::::default() 39 | .override_with(TomlSource::new("target = \"Simple\"")) 40 | .try_build() 41 | .expect("Failed to build Simple"); 42 | assert_eq!( 43 | target, 44 | RootTarget { 45 | target: Target::Simple 46 | } 47 | ); 48 | } 49 | 50 | #[test] 51 | fn tuple_variant() { 52 | let target = ConfigBuilder::::default() 53 | // I hope nobody writes their config like this... 54 | .override_with(TomlSource::new("target = { Tuple = { 0 = 0, 1 = 1 } }")) 55 | .try_build() 56 | .expect("Failed to build Tuple"); 57 | assert_eq!( 58 | target, 59 | RootTarget { 60 | target: Target::Tuple(0, 1) 61 | } 62 | ); 63 | } 64 | 65 | #[test] 66 | fn field_variant() { 67 | let target = ConfigBuilder::::default() 68 | .override_with(TomlSource::new( 69 | "target = { Field = { field1 = 1, field2 = 2 } }", 70 | )) 71 | .try_build() 72 | .expect("Failed to build Field"); 73 | assert_eq!( 74 | target, 75 | RootTarget { 76 | target: Target::Field { 77 | field1: 1, 78 | field2: 2 79 | } 80 | } 81 | ); 82 | } 83 | 84 | #[test] 85 | fn field_merge() { 86 | let target = ConfigBuilder::::default() 87 | .override_with(TomlSource::new("target = { Field = { field2 = 2 } }")) 88 | .override_with(TomlSource::new("target = { Field = { field1 = 1 } }")) 89 | .try_build() 90 | .expect("Failed to build Field"); 91 | assert_eq!( 92 | target, 93 | RootTarget { 94 | target: Target::Field { 95 | field1: 1, 96 | field2: 2 97 | } 98 | } 99 | ); 100 | } 101 | 102 | #[test] 103 | fn mix_and_match() { 104 | let simple_source = TomlSource::new("target = \"Simple\""); 105 | let field_source = TomlSource::new("target = { Field = { field1 = 1, field2 = 2 } }"); 106 | 107 | let target = ConfigBuilder::::default() 108 | .override_with(field_source.clone()) 109 | .override_with(simple_source.clone()) 110 | .try_build() 111 | .expect("Failed to build from mixed source"); 112 | assert_eq!( 113 | target, 114 | RootTarget { 115 | target: Target::Simple 116 | } 117 | ); 118 | 119 | let target = ConfigBuilder::::default() 120 | .override_with(simple_source) 121 | .override_with(field_source) 122 | .try_build() 123 | .expect("Failed to build from mixed source"); 124 | assert_eq!( 125 | target, 126 | RootTarget { 127 | target: Target::Field { 128 | field1: 1, 129 | field2: 2 130 | } 131 | } 132 | ); 133 | } 134 | } 135 | 136 | #[cfg(feature = "json")] 137 | mod json { 138 | use confik::{ConfigBuilder, JsonSource}; 139 | 140 | use super::{RootTarget, Target}; 141 | 142 | /// toml parsing can't do a partial load of a tuple variant 143 | /// If we provide `target = { TupleVariant = { 0 = 0 } }`, the error indicates that 144 | /// it expects only a table of length 2 and it doesn't have an explicit null type 145 | /// to implement it the same way we did with json 146 | #[test] 147 | fn tuple_merge() { 148 | let target = ConfigBuilder::::default() 149 | // I hope nobody writes their config like this... 150 | .override_with(JsonSource::new(r#"{"target": { "Tuple": [0, null] }}"#)) 151 | .override_with(JsonSource::new(r#"{"target": { "Tuple": [null, 1] }}"#)) 152 | .try_build() 153 | .expect("Failed to build TupleV"); 154 | assert_eq!( 155 | target, 156 | RootTarget { 157 | target: Target::Tuple(0, 1) 158 | } 159 | ); 160 | 161 | let target = ConfigBuilder::::default() 162 | // I hope nobody writes their config like this... 163 | .override_with(JsonSource::new(r#"{"target": { "Tuple": [null, 1] }}"#)) 164 | .override_with(JsonSource::new(r#"{"target": { "Tuple": [0, null] }}"#)) 165 | .try_build() 166 | .expect("Failed to build Tuple"); 167 | assert_eq!( 168 | target, 169 | RootTarget { 170 | target: Target::Tuple(0, 1) 171 | } 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /confik/src/sources/file_source.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, path::PathBuf}; 2 | 3 | use cfg_if::cfg_if; 4 | use thiserror::Error; 5 | 6 | use crate::{ConfigurationBuilder, Source}; 7 | 8 | #[derive(Debug, Error)] 9 | #[error("Could not parse {}", .path.display())] 10 | struct FileError { 11 | path: PathBuf, 12 | 13 | #[source] 14 | kind: FileErrorKind, 15 | } 16 | 17 | #[derive(Debug, Error)] 18 | enum FileErrorKind { 19 | #[error(transparent)] 20 | CouldNotReadFile(#[from] std::io::Error), 21 | 22 | #[allow(dead_code)] 23 | #[error("{0} feature is not enabled")] 24 | MissingFeatureForExtension(&'static str), 25 | 26 | #[error("Unknown file extension")] 27 | UnknownExtension, 28 | 29 | #[cfg(feature = "toml")] 30 | #[error(transparent)] 31 | Toml(#[from] toml::de::Error), 32 | 33 | #[cfg(feature = "json")] 34 | #[error(transparent)] 35 | Json(#[from] serde_json::Error), 36 | } 37 | 38 | /// A [`Source`] referring to a file path. 39 | #[derive(Debug, Clone)] 40 | pub struct FileSource { 41 | path: PathBuf, 42 | allow_secrets: bool, 43 | } 44 | 45 | impl FileSource { 46 | /// Create a [`Source`] referring to a file path, 47 | /// 48 | /// The deserialization method will be determined by the file extension. 49 | /// 50 | /// Supported extensions: 51 | /// - `toml` 52 | /// - `json` 53 | pub fn new(path: impl Into) -> Self { 54 | Self { 55 | path: path.into(), 56 | allow_secrets: false, 57 | } 58 | } 59 | 60 | /// Allows this source to contain secrets. 61 | pub fn allow_secrets(mut self) -> Self { 62 | self.allow_secrets = true; 63 | self 64 | } 65 | 66 | fn deserialize(&self) -> Result { 67 | #[allow(unused_variables)] 68 | let contents = std::fs::read_to_string(&self.path)?; 69 | 70 | match self.path.extension().and_then(|ext| ext.to_str()) { 71 | Some("toml") => { 72 | cfg_if! { 73 | if #[cfg(feature = "toml")] { 74 | Ok(toml::from_str(&contents)?) 75 | } else { 76 | Err(FileErrorKind::MissingFeatureForExtension("toml")) 77 | } 78 | } 79 | } 80 | 81 | Some("json") => { 82 | cfg_if! { 83 | if #[cfg(feature = "json")] { 84 | Ok(serde_json::from_str(&contents)?) 85 | } else { 86 | Err(FileErrorKind::MissingFeatureForExtension("json")) 87 | } 88 | } 89 | } 90 | 91 | _ => Err(FileErrorKind::UnknownExtension), 92 | } 93 | } 94 | } 95 | 96 | impl Source for FileSource { 97 | fn allows_secrets(&self) -> bool { 98 | self.allow_secrets 99 | } 100 | 101 | fn provide(&self) -> Result> { 102 | self.deserialize().map_err(|err| { 103 | Box::new(FileError { 104 | path: self.path.clone(), 105 | kind: err, 106 | }) as _ 107 | }) 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use std::fs; 114 | 115 | use confik_macros::Configuration; 116 | 117 | use super::*; 118 | 119 | #[derive(Debug, Default, serde::Deserialize, Configuration)] 120 | struct NoopConfig {} 121 | 122 | #[derive(Debug, Default, serde::Deserialize, Configuration)] 123 | #[allow(dead_code)] 124 | struct SimpleConfig { 125 | foo: u64, 126 | } 127 | 128 | #[test] 129 | fn non_existent() { 130 | let source = FileSource::new("non-existent-config.toml"); 131 | let err = source.deserialize::>().unwrap_err(); 132 | assert!( 133 | err.to_string().contains("No such file or directory"), 134 | "unexpected error message: {err}", 135 | ); 136 | } 137 | 138 | #[test] 139 | fn unknown_extension() { 140 | let dir = tempfile::TempDir::new().unwrap(); 141 | 142 | let cfg_path = dir.path().join("config.cfg"); 143 | fs::write(&cfg_path, "").unwrap(); 144 | 145 | let source = FileSource::new(&cfg_path); 146 | let err = source.deserialize::>().unwrap_err(); 147 | assert!( 148 | err.to_string().contains("Unknown file extension"), 149 | "unexpected error message: {err}", 150 | ); 151 | 152 | dir.close().unwrap(); 153 | } 154 | 155 | #[cfg(feature = "json")] 156 | #[test] 157 | fn json() { 158 | let dir = tempfile::TempDir::new().unwrap(); 159 | 160 | let json_path = dir.path().join("config.json"); 161 | 162 | fs::write(&json_path, "{}").unwrap(); 163 | let source = FileSource::new(&json_path); 164 | let err = source.deserialize::>().unwrap_err(); 165 | assert!( 166 | err.to_string().contains("missing field"), 167 | "unexpected error message: {err}", 168 | ); 169 | 170 | fs::write(&json_path, "{\"foo\":42}").unwrap(); 171 | let source = FileSource::new(&json_path); 172 | let config = source.deserialize::>().unwrap(); 173 | assert_eq!(config.unwrap().foo, 42); 174 | 175 | dir.close().unwrap(); 176 | } 177 | 178 | #[cfg(feature = "toml")] 179 | #[test] 180 | fn toml() { 181 | let dir = tempfile::TempDir::new().unwrap(); 182 | 183 | let toml_path = dir.path().join("config.toml"); 184 | 185 | fs::write(&toml_path, "").unwrap(); 186 | let source = FileSource::new(&toml_path); 187 | let err = source.deserialize::>().unwrap_err(); 188 | assert!( 189 | err.to_string().contains("missing field"), 190 | "unexpected error message: {err}", 191 | ); 192 | 193 | fs::write(&toml_path, "foo = 42").unwrap(); 194 | let source = FileSource::new(&toml_path); 195 | let config = source.deserialize::>().unwrap(); 196 | assert_eq!(config.unwrap().foo, 42); 197 | 198 | dir.close().unwrap(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /confik/tests/third_party.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "secrecy", feature = "toml"))] 2 | mod secrecy { 3 | use confik::{Configuration, TomlSource}; 4 | use indoc::indoc; 5 | use secrecy::{ExposeSecret as _, SecretString}; 6 | 7 | #[test] 8 | fn secret_string() { 9 | #[derive(Debug, Configuration)] 10 | struct Config { 11 | #[confik(secret)] 12 | secret_string: SecretString, 13 | } 14 | 15 | let toml = indoc! {r#" 16 | secret_string = "SeriouslySecret" 17 | "#}; 18 | 19 | let config = Config::builder() 20 | .override_with(TomlSource::new(toml).allow_secrets()) 21 | .try_build() 22 | .unwrap(); 23 | 24 | assert_eq!( 25 | format!("{:?}", config.secret_string), 26 | "SecretBox([REDACTED])", 27 | ); 28 | assert_eq!( 29 | format!("{config:?}"), 30 | "Config { secret_string: SecretBox([REDACTED]) }", 31 | ); 32 | assert_eq!(config.secret_string.expose_secret(), "SeriouslySecret"); 33 | } 34 | 35 | #[test] 36 | fn secret_string_in_field_not_marked_secret() { 37 | #[derive(Debug, Configuration)] 38 | struct Config { 39 | secret_string: SecretString, 40 | } 41 | 42 | let toml = indoc! {r#" 43 | secret_string = "SeriouslySecret" 44 | "#}; 45 | 46 | // in Source without `.allow_secrets()` 47 | Config::builder() 48 | .override_with(TomlSource::new(toml)) 49 | .try_build() 50 | .unwrap_err(); 51 | 52 | // in Source with `.allow_secrets()` 53 | let config = Config::builder() 54 | .override_with(TomlSource::new(toml).allow_secrets()) 55 | .try_build() 56 | .unwrap(); 57 | 58 | assert_eq!( 59 | format!("{:?}", config.secret_string), 60 | "SecretBox([REDACTED])", 61 | ); 62 | assert_eq!( 63 | format!("{config:?}"), 64 | "Config { secret_string: SecretBox([REDACTED]) }", 65 | ); 66 | assert_eq!(config.secret_string.expose_secret(), "SeriouslySecret"); 67 | } 68 | } 69 | 70 | #[cfg(all(feature = "bigdecimal", feature = "toml"))] 71 | mod bigdecimal { 72 | use std::str::FromStr; 73 | 74 | use bigdecimal::BigDecimal; 75 | use confik::{Configuration, Error, TomlSource}; 76 | use indoc::formatdoc; 77 | 78 | #[derive(Configuration, Debug)] 79 | struct Config { 80 | big_decimal: BigDecimal, 81 | } 82 | 83 | #[test] 84 | fn bigdecimal() { 85 | let big_decimal = "1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573"; 86 | let toml = formatdoc! {r#" 87 | big_decimal = "{big_decimal}" 88 | "#}; 89 | 90 | let config = Config::builder() 91 | .override_with(TomlSource::new(toml)) 92 | .try_build() 93 | .expect("Failed to parse config"); 94 | 95 | assert_eq!( 96 | config.big_decimal, 97 | BigDecimal::from_str(big_decimal).unwrap() 98 | ); 99 | } 100 | 101 | #[test] 102 | fn bigdecimal_missing_err_propagation() { 103 | let toml = formatdoc! {r#" 104 | big_decimal = "" 105 | "#}; 106 | 107 | let config_parsing_err = Config::builder() 108 | .override_with(TomlSource::new(toml)) 109 | .try_build(); 110 | match config_parsing_err { 111 | Ok(_) => { 112 | panic!("Expected parsing error"); 113 | } 114 | Err(err) => match err { 115 | Error::Source(source_err, _config) => { 116 | assert!(source_err 117 | .to_string() 118 | .contains("Failed to parse empty string")); 119 | assert!(source_err.to_string().contains("big_decimal")); 120 | } 121 | 122 | _ => { 123 | panic!("Expected MissingValue error"); 124 | } 125 | }, 126 | } 127 | } 128 | } 129 | 130 | #[cfg(feature = "js_option")] 131 | mod js_option { 132 | use confik::Configuration; 133 | use js_option::JsOption; 134 | 135 | #[derive(Configuration, Debug)] 136 | struct Config { 137 | opt: JsOption, 138 | } 139 | 140 | #[test] 141 | fn undefined() { 142 | let config = Config::builder() 143 | .try_build() 144 | .expect("Should be valid without config"); 145 | assert_eq!(config.opt, JsOption::Undefined); 146 | } 147 | 148 | #[cfg(feature = "json")] 149 | #[test] 150 | fn null() { 151 | let json = r#"{ "opt": null }"#; 152 | 153 | let config = Config::builder() 154 | .override_with(confik::JsonSource::new(json)) 155 | .try_build() 156 | .expect("Failed to parse config"); 157 | assert_eq!(config.opt, JsOption::Null); 158 | } 159 | 160 | #[cfg(feature = "toml")] 161 | #[test] 162 | fn present() { 163 | let toml = "opt = 5"; 164 | 165 | let config = Config::builder() 166 | .override_with(confik::TomlSource::new(toml)) 167 | .try_build() 168 | .expect("Should be valid without config"); 169 | assert_eq!(config.opt, JsOption::Some(5)); 170 | } 171 | 172 | #[cfg(feature = "json")] 173 | #[test] 174 | fn merge() { 175 | #[derive(Debug, Configuration, PartialEq, Eq)] 176 | struct Config { 177 | one: JsOption, 178 | two: JsOption, 179 | three: JsOption, 180 | four: JsOption, 181 | } 182 | 183 | let base = r#"{ "two": null, "three": 5 }"#; 184 | let merge = r#"{ "one": 1, "two": 2, "three": 3}"#; 185 | 186 | let config = Config::builder() 187 | .override_with(confik::JsonSource::new(merge)) 188 | .override_with(confik::JsonSource::new(base)) 189 | .try_build() 190 | .expect("Failed to parse config"); 191 | 192 | assert_eq!( 193 | config, 194 | Config { 195 | one: JsOption::Some(1), 196 | two: JsOption::Null, 197 | three: JsOption::Some(5), 198 | four: JsOption::Undefined, 199 | } 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /confik/tests/option_builder/mod.rs: -------------------------------------------------------------------------------- 1 | use confik::Configuration; 2 | 3 | #[allow(dead_code)] // unused in no-default-features cases 4 | #[derive(Debug, Eq, PartialEq, Configuration)] 5 | struct Config { 6 | param: Option, 7 | } 8 | 9 | #[cfg(feature = "toml")] 10 | mod toml { 11 | use confik::{ConfigBuilder, Configuration, TomlSource}; 12 | 13 | use super::Config; 14 | 15 | #[test] 16 | fn not_present() { 17 | let config = Config::builder() 18 | .override_with(TomlSource::new("")) 19 | .try_build() 20 | .expect("Config with optional field not present failed to be created"); 21 | assert_eq!(None, config.param); 22 | } 23 | 24 | #[test] 25 | fn present() { 26 | let config = ConfigBuilder::::default() 27 | .override_with(TomlSource::new("param = 3")) 28 | .try_build() 29 | .expect("Explicit value"); 30 | assert_eq!(config, Config { param: Some(3) }); 31 | } 32 | } 33 | 34 | #[cfg(feature = "json")] 35 | mod json { 36 | use confik::{ConfigBuilder, JsonSource}; 37 | 38 | use super::Config; 39 | 40 | /// Toml doesn't have a null type 41 | #[test] 42 | fn explicit_null() { 43 | let config = ConfigBuilder::::default() 44 | .override_with(JsonSource::new("{\"param\": null}")) 45 | .try_build() 46 | .expect("Explicit null"); 47 | assert_eq!(config, Config { param: None }); 48 | } 49 | 50 | #[cfg(feature = "toml")] 51 | mod toml { 52 | use confik::{ConfigBuilder, JsonSource, TomlSource}; 53 | 54 | use super::Config; 55 | 56 | /// Toml doesn't have a null type 57 | #[test] 58 | fn mixed_expecting_none() { 59 | let config = ConfigBuilder::::default() 60 | .override_with(TomlSource::new("")) 61 | .override_with(TomlSource::new("param = 3")) 62 | .override_with(JsonSource::new("{\"param\": null}")) 63 | .override_with(TomlSource::new("")) 64 | .try_build() 65 | .expect("Explicit null"); 66 | assert_eq!(config, Config { param: None }); 67 | } 68 | 69 | /// Toml doesn't have a null type 70 | #[test] 71 | fn mixed_expecting_some() { 72 | let config = ConfigBuilder::::default() 73 | .override_with(TomlSource::new("")) 74 | .override_with(JsonSource::new("{\"param\": null}")) 75 | .override_with(TomlSource::new("param = 3")) 76 | .override_with(TomlSource::new("")) 77 | .try_build() 78 | .expect("Explicit null"); 79 | assert_eq!(config, Config { param: Some(3) }); 80 | } 81 | } 82 | } 83 | 84 | mod complex { 85 | use confik::Configuration; 86 | 87 | #[allow(dead_code)] // unused in no-default-features cases 88 | #[derive(Configuration, Debug, PartialEq, Eq)] 89 | struct Leaf { 90 | param: Option, 91 | } 92 | 93 | #[allow(dead_code)] // unused in no-default-features cases 94 | #[derive(Configuration, Debug, PartialEq, Eq)] 95 | struct Root { 96 | inner: Option, 97 | } 98 | 99 | #[derive(Configuration, Debug, PartialEq, Eq)] 100 | struct SecretLeaf { 101 | param: usize, 102 | } 103 | 104 | #[derive(Configuration, Debug, PartialEq, Eq)] 105 | struct SecretRoot { 106 | #[confik(secret)] 107 | inner: Option, 108 | } 109 | 110 | #[cfg(feature = "toml")] 111 | mod toml { 112 | use assert_matches::assert_matches; 113 | use confik::{Configuration, Error, TomlSource}; 114 | 115 | use super::{Root, SecretRoot}; 116 | 117 | #[test] 118 | fn merge_unspecified() { 119 | let config = Root::builder() 120 | .override_with(TomlSource::new("")) 121 | .override_with(TomlSource::new("[inner]\nparam = 3")) 122 | .try_build() 123 | .expect("Merge unspecified"); 124 | assert_eq!(Some(3), config.inner.unwrap().param); 125 | 126 | let config = Root::builder() 127 | .override_with(TomlSource::new("[inner]\nparam = 4")) 128 | .override_with(TomlSource::new("")) 129 | .try_build() 130 | .expect("Merge unspecified"); 131 | assert_eq!(Some(4), config.inner.unwrap().param); 132 | } 133 | 134 | #[test] 135 | fn unexpected_secret() { 136 | let err = SecretRoot::builder() 137 | .override_with(TomlSource::new("[inner]\nparam = 3")) 138 | .try_build() 139 | .expect_err("Wrongly built with unexpected secret"); 140 | assert_matches!( 141 | err, 142 | Error::UnexpectedSecret(path, _) if path.to_string().contains("`inner`") 143 | ); 144 | } 145 | 146 | #[cfg(feature = "json")] 147 | mod json { 148 | use confik::{Configuration, JsonSource, TomlSource}; 149 | 150 | use super::Root; 151 | 152 | /// Toml doesn't have a null type 153 | #[test] 154 | fn merge_none() { 155 | let config = Root::builder() 156 | .override_with(JsonSource::new(r#"{ "inner": { "param": null } }"#)) 157 | .override_with(TomlSource::new("[inner]\nparam = 3")) 158 | .try_build() 159 | .expect("Merge unspecified"); 160 | assert_eq!(Some(3), config.inner.unwrap().param); 161 | 162 | let config = Root::builder() 163 | .override_with(TomlSource::new("[inner]\nparam = 3")) 164 | .override_with(JsonSource::new(r#"{ "inner": { "param": null } }"#)) 165 | .try_build() 166 | .expect("Merge unspecified"); 167 | assert_eq!(None, config.inner.unwrap().param); 168 | } 169 | } 170 | } 171 | 172 | #[test] 173 | fn unspecified_secret() { 174 | let config = SecretRoot::builder() 175 | .try_build() 176 | .expect("Failed to build with optional secret"); 177 | assert_eq!(None, config.inner); 178 | } 179 | 180 | #[cfg(feature = "env")] 181 | mod env { 182 | use confik::{Configuration, EnvSource}; 183 | 184 | use super::{SecretLeaf, SecretRoot}; 185 | 186 | #[test] 187 | fn expected_secret() { 188 | let config = temp_env::with_var("inner__param", Some("5"), || { 189 | SecretRoot::builder() 190 | .override_with(EnvSource::new().allow_secrets()) 191 | .try_build() 192 | .expect("Optional secret in env is allowed") 193 | }); 194 | assert_eq!( 195 | config, 196 | SecretRoot { 197 | inner: Some(SecretLeaf { param: 5 }) 198 | } 199 | ); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /confik/src/sources/offset_source.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use crate::{ConfigurationBuilder, Source}; 4 | 5 | /// A [`Source`] containing another source that can build the target at an offset determined by 6 | /// the provided path. 7 | /// 8 | /// ```rust 9 | /// # #[cfg(feature = "toml")] 10 | /// # { 11 | /// use confik::{helpers::BuilderOf, Configuration, OffsetSource, TomlSource}; 12 | /// 13 | /// #[derive(Debug, Configuration, PartialEq, Eq)] 14 | /// struct Config { 15 | /// data: usize, 16 | /// leaf: LeafConfig, 17 | /// } 18 | /// 19 | /// #[derive(Debug, Configuration, PartialEq, Eq)] 20 | /// struct LeafConfig { 21 | /// data: usize, 22 | /// } 23 | /// 24 | /// let root_toml = "data = 4"; 25 | /// let leaf_toml = "data = 5"; 26 | /// 27 | /// let root_source = TomlSource::new(root_toml); 28 | /// let leaf_source = OffsetSource::new::>( 29 | /// TomlSource::new(leaf_toml), 30 | /// |b| &mut b.leaf, 31 | /// ); 32 | /// 33 | /// let config = Config::builder() 34 | /// .override_with(root_source) 35 | /// .override_with(leaf_source) 36 | /// .try_build() 37 | /// .expect("Valid source"); 38 | /// 39 | /// assert_eq!( 40 | /// config, 41 | /// Config { 42 | /// data: 4, 43 | /// leaf: LeafConfig { data: 5 } 44 | /// } 45 | /// ); 46 | /// # } 47 | /// ``` 48 | pub struct OffsetSource<'a, OffsetBuilder, PathFn> { 49 | inner_source: Box + 'a>, 50 | path: PathFn, 51 | } 52 | 53 | impl<'a, OffsetBuilder, PathFn> OffsetSource<'a, OffsetBuilder, PathFn> 54 | where 55 | OffsetBuilder: ConfigurationBuilder, 56 | { 57 | /// Creates a [`Source`] containing raw JSON data. 58 | pub fn new(inner_source: impl Source + 'a, path: PathFn) -> Self 59 | where 60 | TargetBuilder: ConfigurationBuilder, 61 | PathFn: for<'b> Fn(&'b mut TargetBuilder) -> &'b mut OffsetBuilder, 62 | { 63 | Self { 64 | inner_source: Box::new(inner_source), 65 | path, 66 | } 67 | } 68 | } 69 | 70 | impl<'a, OffsetBuilder, PathFn, TargetBuilder> Source 71 | for OffsetSource<'a, OffsetBuilder, PathFn> 72 | where 73 | TargetBuilder: ConfigurationBuilder, 74 | OffsetBuilder: ConfigurationBuilder, 75 | PathFn: for<'b> Fn(&'b mut TargetBuilder) -> &'b mut OffsetBuilder, 76 | { 77 | fn allows_secrets(&self) -> bool { 78 | self.inner_source.allows_secrets() 79 | } 80 | 81 | fn provide(&self) -> Result> { 82 | let mut builder = TargetBuilder::default(); 83 | *(self.path)(&mut builder) = self.inner_source.provide()?; 84 | Ok(builder) 85 | } 86 | } 87 | 88 | impl fmt::Debug for OffsetSource<'_, OffsetBuilder, PathFn> { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | f.debug_struct("OffsetSource") 91 | .field("inner_source", &self.inner_source) 92 | .finish_non_exhaustive() 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use crate::{helpers::BuilderOf, sources::test::TestSource, Configuration, OffsetSource}; 99 | 100 | #[derive(Debug, Configuration, PartialEq, Eq)] 101 | #[confik(forward(derive(Clone)))] 102 | struct Config { 103 | #[confik(default)] 104 | data: usize, 105 | leaf: LeafConfig, 106 | } 107 | 108 | #[derive(Debug, Configuration, PartialEq, Eq)] 109 | #[confik(forward(derive(Clone)))] 110 | struct LeafConfig { 111 | #[confik(default)] 112 | data: usize, 113 | } 114 | 115 | #[test] 116 | fn identity_offset() { 117 | let test_source_builder = BuilderOf:: { 118 | data: Some(6), 119 | ..Default::default() 120 | }; 121 | let inner = TestSource { 122 | data: test_source_builder, 123 | allow_secrets: false, 124 | }; 125 | 126 | // `std::convert::identity` can't handle the lifetimes here, probably due to early binding 127 | // leading to assumptions in the lifetimes. 128 | let source = OffsetSource::new(inner, |x| x); 129 | 130 | let config = Config::builder() 131 | .override_with(source) 132 | .try_build() 133 | .expect("Valid input"); 134 | 135 | assert_eq!( 136 | config, 137 | Config { 138 | data: 6, 139 | leaf: LeafConfig { data: 0 } 140 | } 141 | ) 142 | } 143 | 144 | #[test] 145 | fn leaf_offset() { 146 | let test_source_builder = BuilderOf:: { data: Some(6) }; 147 | let inner = TestSource { 148 | data: test_source_builder, 149 | allow_secrets: false, 150 | }; 151 | 152 | let source = OffsetSource::new::>(inner, |x| &mut x.leaf); 153 | 154 | let config = Config::builder() 155 | .override_with(source) 156 | .try_build() 157 | .expect("Valid input"); 158 | 159 | assert_eq!( 160 | config, 161 | Config { 162 | data: 0, 163 | leaf: LeafConfig { data: 6 } 164 | } 165 | ) 166 | } 167 | 168 | #[test] 169 | #[cfg(feature = "json")] 170 | fn data_offset_json() { 171 | let data_source = 172 | OffsetSource::new(crate::JsonSource::new("1"), |x: &mut BuilderOf| { 173 | &mut x.data 174 | }); 175 | let leaf_source = 176 | OffsetSource::new(crate::JsonSource::new("2"), |x: &mut BuilderOf| { 177 | &mut x.leaf.data 178 | }); 179 | 180 | let config = Config::builder() 181 | .override_with(data_source) 182 | .override_with(leaf_source) 183 | .try_build() 184 | .expect("Valid input"); 185 | 186 | assert_eq!( 187 | config, 188 | Config { 189 | data: 1, 190 | leaf: LeafConfig { data: 2 } 191 | } 192 | ) 193 | } 194 | 195 | #[test] 196 | #[cfg(feature = "env")] 197 | fn data_offset_env() { 198 | temp_env::with_var("data", Some("10"), || { 199 | // `std::convert::identity` can't handle the lifetimes here, probably due to early 200 | // binding leading to assumptions in the lifetimes. 201 | let data_source = OffsetSource::new(crate::EnvSource::new(), |x| x); 202 | let leaf_source = 203 | OffsetSource::new(crate::EnvSource::new(), |x: &mut BuilderOf| { 204 | &mut x.leaf 205 | }); 206 | 207 | let config = Config::builder() 208 | .override_with(data_source) 209 | .override_with(leaf_source) 210 | .try_build() 211 | .expect("Valid input"); 212 | 213 | assert_eq!( 214 | config, 215 | Config { 216 | data: 10, 217 | leaf: LeafConfig { data: 10 } 218 | } 219 | ) 220 | }) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /confik/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 0.15.1 6 | 7 | - Add `confik(crate = ...)` option to `Configuration` macro. 8 | 9 | ## 0.15.0 10 | 11 | - Add a new `confik(skip)` attribute. This allows skipping the field in the builder (and so it need not implement `Configuration` or be deserializable), however it must use `confik(default)` or `confik(default = ...)`, otherwise it can't be built. E.g. 12 | ```rust 13 | #[derive(Configuration)] 14 | struct Config { 15 | #[confik(skip, default = Instant::now())] 16 | loaded_at: Instant, 17 | } 18 | ``` 19 | - Implement `Configuration` for [`ahash::{AHashSet, AHashMap}`](https://docs.rs/ahash/0.8/ahash). 20 | - Add new `helper` module, with utilities for manually implementing more complex `Configuration` behaviour. 21 | - `UnkeyedContainerBuilder` can be used as a `Configuration::builder` for container types without separate keys (such as a `Vec` and `HashSet`). 22 | - See `UnkeyedContainerBuilder`'s docs for details and an example. 23 | - `KeyedContainerBuilder` can be used as a `Configuration::builder` for container types with explicit keys (such as `HashMap` and `BTreeMap`). 24 | - Using `KeyedContainerBuilder` requires implementing `KeyedContainer` for your type. 25 | - See `KeyedContainerBuilder`'s docs for details and an example. 26 | - A few type aliases, to make it easier to write and understand complex generics when manually implementing `Configuration`. 27 | - Add a new `OffsetSource` for when your configuration files point part way into your configuration. 28 | - For example, this would allow reading one `tls.toml` to multiple different TLS config structs; then allowing more specific configuration to override them. i.e. 29 | ```rust 30 | let common_tls_source = FileSource::new("tls.toml"); 31 | let config = Config::builder() 32 | .override_with(OffsetSource::new::>(common_tls_source.clone(), |b| &mut b.kafka.tls)) 33 | .override_with(OffsetSource::new::>(common_tls_source, |b| &mut b.server.tls)) 34 | .override_with(FileSource::new("config.toml")) 35 | .try_build()?; 36 | ``` 37 | - Update `toml` dependency to `0.9`. 38 | 39 | ## 0.14.0 40 | 41 | - Implement `Configuration` for atomic numeric and bool types. 42 | - Implement `Configuration` for [`js_option::JsOption`](https://docs.rs/js_option/0.1.1/js_option/enum.JsOption.html) 43 | - Add a new `confik(forward(...))` attribute. As well as allowing for forwarding general attributes to the builder, this: 44 | - Replaces `confik(forward_serde(...))`. E.g. 45 | ```rust 46 | #[derive(Configuration)] 47 | struct Config { 48 | #[confik(forward(serde(default)))] 49 | num: usize, 50 | } 51 | ``` 52 | - Replaces `confik(derive(...))`. E.g. 53 | ```rust 54 | #[derive(Configuration)] 55 | #[confik(forward(derive(Hash)))] 56 | struct Config(usize); 57 | ``` 58 | - Add a new `confik(name = ...)` attribute, that provides a custom name for the `Configuration::Builder` `struct` or `enum`. 59 | - This will also place the builder in the local module, so that its name is in a known location 60 | ```rust 61 | #[derive(Configuration)] 62 | #[confik(name = Builder)] 63 | struct Config {} 64 | ``` 65 | 66 | ## 0.13.0 67 | 68 | - Update `bytesize` dependency to `2`. 69 | - Update `ipnetwork` dependency to `0.21`. 70 | - Minimum supported Rust version (MSRV) is now 1.70. 71 | 72 | ## 0.12.0 73 | 74 | - Update `secrecy` dependency to `0.10`. 75 | 76 | ## 0.11.8 77 | 78 | - Implement `Configuration` for [`chrono::NaiveDateTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDateTime.html) 79 | 80 | ## 0.11.7 81 | 82 | - Implement `Configuration` for [`bigdecimal::BigDecimal`](https://docs.rs/bigdecimal/0.4/bigdecimal/struct.BigDecimal.html). 83 | 84 | ## 0.11.6 85 | 86 | - Implement `Configuration` for [`bytesize::ByteSize`](https://docs.rs/bytesize/1/bytesize/struct.ByteSize.html). 87 | 88 | ## 0.11.5 89 | 90 | - Implement `Configuration` for [`chrono::NaiveDate`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveDate.html). 91 | - Implement `Configuration` for [`chrono::NaiveTime`](https://docs.rs/chrono/0.4/chrono/naive/struct.NaiveTime.html). 92 | 93 | ## 0.11.4 94 | 95 | - Override the following lints in macro generated code: `missing_copy_implementations`, `missing_debug_implementations`, `variant_size_differences` 96 | 97 | ## 0.11.3 98 | 99 | - Implement `Configuration` for [`camino::Utf8PathBuf`](https://docs.rs/camino/1/camino/struct.Utf8PathBuf.html). 100 | 101 | ## 0.11.2 102 | 103 | - Implement `Configuration` for [`ipnetwork::IpNetwork`](https://docs.rs/ipnetwork/0.20/ipnetwork/enum.IpNetwork.html). 104 | 105 | ## 0.11.1 106 | 107 | - Parsing of database kind is now case-insensitive. 108 | - Minimum supported Rust version (MSRV) is now 1.67 due to `toml_edit` dependency. 109 | 110 | ## 0.11.0 111 | 112 | - Add support for `#[confik(try_from = "")]` field attribute, following the rules of `from` but using `TryFrom`. This will not break existing code unless it contains manual implementations of `Configuration`. 113 | - Add `FailedTryInto` type. 114 | - Add `Error::TryInto` variant. 115 | - `.try_build()` methods now use `Error` as their return type. 116 | 117 | ## 0.10.2 118 | 119 | - Remove `Debug` implementation from configuration builders. 120 | - Remove `Debug` requirement for leaf configuration. 121 | - Fix `Configuration` derive with `where` clauses. 122 | 123 | ## 0.10.1 124 | 125 | - Implement `Configuration` for [`secrecy::SecretString`](https://docs.rs/secrecy/0.8/secrecy/type.SecretString.html). This type is always considered a secret, and can only be loaded from `Source`s which `.allow_secrets()`. 126 | - Add `SecretOption`, an alternative to `Option` as a `Configuration::Builder` for **types** which are always secret. 127 | 128 | ## 0.10.0 129 | 130 | - The index of an unexpected secret in now included when one is found in an unkeyed container (such as a `Vec`). Note that this will provide little to no information for unsorted/arbitrarily containers like a `HashSet`. 131 | - `Option`s will now obey `default`s when they have received no explicit configuration. Note that explicit `null`s, such as in JSON, will set the `Option` to `None`, not to the default. 132 | - Containers, such as `Vec` and `HashMap`, will now obey `default`s when they have received no explicit configuration. 133 | - Partial configuration will return an error, even if there is a default set at a higher level. E.g., 134 | 135 | ```rust 136 | struct Data { 137 | a: usize, 138 | b: usize 139 | } 140 | 141 | struct Config { 142 | #[confik(default = Data { a: 1, b: 2 })] 143 | data: Data, 144 | } 145 | ``` 146 | 147 | with configuration: 148 | 149 | ```toml 150 | [data] 151 | a = "5" 152 | ``` 153 | 154 | will return an error indicating `b` is missing, instead of ignoring the provided configuration. 155 | 156 | ## 0.9.0 157 | 158 | - Optional crate features no longer have the `with-` prefix, e.g.: `with-uuid` -> `uuid`. 159 | 160 | ## 0.8.0 161 | 162 | - Attributes that receive expressions (`default` and `from`) now need to be unquoted, e.g.: 163 | 164 | ```diff 165 | - struct Config { #[config(default = "\"Hello World\"") param: String } 166 | + struct Config { #[config(default = "Hello World") param: String } 167 | 168 | - struct Config { #[config(default = "5_usize") param: usize } 169 | + struct Config { #[config(default = 5_usize) param: usize } 170 | ``` 171 | 172 | ## 0.7.0 173 | 174 | - Initial release. 175 | -------------------------------------------------------------------------------- /confik/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("./lib.md")] 2 | #![deny(rust_2018_idioms, nonstandard_style, future_incompatible)] 3 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 4 | 5 | use std::{borrow::Cow, error::Error as StdError, ops::Not}; 6 | 7 | #[doc(hidden)] 8 | pub use confik_macros::*; 9 | use serde::de::DeserializeOwned; 10 | 11 | #[doc(hidden)] 12 | pub mod __exports { 13 | /// Re-export [`Deserialize`] for use in case `serde` is not otherwise used 14 | /// whilst we are. 15 | /// 16 | /// As serde then calls into other serde functions, we need to re-export the whole of serde, 17 | /// instead of just [`Deserialize`]. 18 | /// 19 | /// [`Deserialize`]: serde::Deserialize 20 | pub use serde as __serde; 21 | } 22 | 23 | // Enable use of macros inside the crate 24 | #[allow(unused_extern_crates)] // false positive 25 | extern crate self as confik; 26 | 27 | mod builder; 28 | #[cfg(feature = "common")] 29 | pub mod common; 30 | mod errors; 31 | pub mod helpers; 32 | mod path; 33 | mod secrets; 34 | mod sources; 35 | mod std_impls; 36 | mod third_party; 37 | 38 | use self::path::Path; 39 | #[cfg(feature = "env")] 40 | pub use self::sources::env_source::EnvSource; 41 | #[cfg(feature = "json")] 42 | pub use self::sources::json_source::JsonSource; 43 | #[cfg(feature = "toml")] 44 | pub use self::sources::toml_source::TomlSource; 45 | pub use self::{ 46 | builder::ConfigBuilder, 47 | errors::Error, 48 | secrets::{SecretBuilder, SecretOption, UnexpectedSecret}, 49 | sources::{file_source::FileSource, offset_source::OffsetSource, Source}, 50 | }; 51 | 52 | /// Captures the path of a missing value. 53 | #[derive(Debug, Default, thiserror::Error)] 54 | #[error("Missing value for path `{0}`")] 55 | pub struct MissingValue(Path); 56 | 57 | impl MissingValue { 58 | /// Prepends a path segment as we return back up the call-stack. 59 | #[must_use] 60 | pub fn prepend(mut self, path_segment: impl Into>) -> Self { 61 | self.0 .0.push(path_segment.into()); 62 | self 63 | } 64 | } 65 | 66 | /// Captures the path and error of a failed conversion. 67 | #[derive(Debug, thiserror::Error)] 68 | #[error("Failed try_into for path `{0}`")] 69 | pub struct FailedTryInto(Path, #[source] Box); 70 | 71 | impl FailedTryInto { 72 | /// Creates a new [`Self`] with a blank path. 73 | pub fn new(err: impl StdError + Send + Sync + 'static) -> Self { 74 | Self(Path::new(), Box::new(err)) 75 | } 76 | 77 | /// Prepends a path segment as we return back up the call-stack. 78 | #[must_use] 79 | pub fn prepend(mut self, path_segment: impl Into>) -> Self { 80 | self.0 .0.push(path_segment.into()); 81 | self 82 | } 83 | } 84 | 85 | /// Converts the sources, in order, into [`Configuration::Builder`] and 86 | /// [`ConfigurationBuilder::merge`]s them, passing any errors back. 87 | fn build_from_sources<'a, Target, Iter>(sources: Iter) -> Result 88 | where 89 | Target: Configuration, 90 | Iter: IntoIterator + 'a>>, 91 | { 92 | sources 93 | .into_iter() 94 | // Convert each source to a `Target::Builder` 95 | .map::, _>( 96 | |source: Box + 'a>| { 97 | let debug = || format!("{source:?}"); 98 | let res = source.provide().map_err(|e| Error::Source(e, debug()))?; 99 | if source.allows_secrets().not() { 100 | res.contains_non_secret_data() 101 | .map_err(|e| Error::UnexpectedSecret(e, debug()))?; 102 | } 103 | Ok(res) 104 | }, 105 | ) 106 | // Merge the builders 107 | .reduce(|first, second| Ok(Target::Builder::merge(first?, second?))) 108 | // If there was no data then we're missing values 109 | .ok_or_else(|| Error::MissingValue(MissingValue::default()))?? 110 | .try_build() 111 | } 112 | 113 | /// The target to be deserialized from multiple sources. 114 | /// 115 | /// This will normally be created by the derive macro which also creates a [`ConfigurationBuilder`] 116 | /// implementation. 117 | /// 118 | /// For types with no contents, e.g. empty structs, or simple enums, this can be implemented very 119 | /// easily by specifying only the builder type as `Option`. For anything more complicated, 120 | /// complete target and builder implementations will be needed. 121 | /// 122 | /// # Examples 123 | /// 124 | /// ``` 125 | /// use confik::Configuration; 126 | /// 127 | /// #[derive(serde::Deserialize)] 128 | /// enum MyEnum { A, B, C } 129 | /// 130 | /// impl Configuration for MyEnum { 131 | /// type Builder = Option; 132 | /// } 133 | /// ``` 134 | pub trait Configuration: Sized { 135 | /// The builder that accumulates the deserializations. 136 | type Builder: ConfigurationBuilder; 137 | 138 | /// Creates an instance of [`ConfigBuilder`] tied to this type. 139 | #[must_use] 140 | fn builder<'a>() -> ConfigBuilder<'a, Self> { 141 | ConfigBuilder::::default() 142 | } 143 | } 144 | 145 | /// A builder for a multi-source config deserialization. 146 | /// 147 | /// This will almost never be implemented manually, instead being derived. 148 | /// 149 | /// Builders must implement [`Default`] so that if the structure is nested in another then it being 150 | /// missing is not an error. 151 | /// For trivial cases, this is solved by using an `Option`. 152 | /// See the worked example on [`Configuration`]. 153 | pub trait ConfigurationBuilder: Default + DeserializeOwned { 154 | /// The target that will be converted into. See [`Configuration`]. 155 | type Target; 156 | 157 | /// Combines two builders recursively, preferring `self`'s data, if present. 158 | #[must_use] 159 | fn merge(self, other: Self) -> Self; 160 | 161 | /// This will probably delegate to `TryInto` but allows it to be implemented for types foreign 162 | /// to the library. 163 | fn try_build(self) -> Result; 164 | 165 | /// Called recursively on each field, aiming to hit all [`SecretBuilder`]s. This is only called 166 | /// when [`Source::allows_secrets`] is `false`. 167 | /// 168 | /// If any data is present then `Ok(true)` is returned, unless the data is wrapped in a 169 | /// [`SecretBuilder`] in which case [`UnexpectedSecret`] is passed, which will then be built 170 | /// into the path to the secret data. 171 | fn contains_non_secret_data(&self) -> Result; 172 | } 173 | 174 | /// Implementations for trivial types via `Option`. 175 | /// 176 | /// This can also be used for user types, such as an `enum` with no variants containing fields. See 177 | /// the worked example on [`Configuration`]. 178 | impl ConfigurationBuilder for Option 179 | where 180 | T: DeserializeOwned + Configuration, 181 | { 182 | type Target = T; 183 | 184 | fn merge(self, other: Self) -> Self { 185 | self.or(other) 186 | } 187 | 188 | fn try_build(self) -> Result { 189 | self.ok_or_else(|| Error::MissingValue(MissingValue::default())) 190 | } 191 | 192 | /// Should not have an `Option` wrapping a secret as ` as ConfigurationBuilder` is 193 | /// used for terminal types, therefore the `SecretBuilder` wrapping would be external to it. 194 | fn contains_non_secret_data(&self) -> Result { 195 | Ok(self.is_some()) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /confik/src/third_party.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of [`Configuration`](crate::Configuration) for frequently used types from other 2 | //! crates. 3 | 4 | #[cfg(feature = "bytesize")] 5 | mod bytesize { 6 | impl crate::Configuration for bytesize::ByteSize { 7 | type Builder = Option; 8 | } 9 | } 10 | 11 | #[cfg(feature = "camino")] 12 | mod camino { 13 | impl crate::Configuration for camino::Utf8PathBuf { 14 | type Builder = Option; 15 | } 16 | } 17 | 18 | #[cfg(feature = "chrono")] 19 | mod chrono { 20 | use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; 21 | use serde::de::DeserializeOwned; 22 | 23 | use crate::Configuration; 24 | 25 | impl Configuration for DateTime 26 | where 27 | Self: DeserializeOwned, 28 | { 29 | type Builder = Option; 30 | } 31 | 32 | impl Configuration for NaiveTime { 33 | type Builder = Option; 34 | } 35 | 36 | impl Configuration for NaiveDate { 37 | type Builder = Option; 38 | } 39 | 40 | impl Configuration for NaiveDateTime { 41 | type Builder = Option; 42 | } 43 | 44 | #[cfg(test)] 45 | #[cfg(feature = "toml")] 46 | mod tests { 47 | use crate::TomlSource; 48 | 49 | #[test] 50 | fn naive_time_format() { 51 | use chrono::NaiveTime; 52 | 53 | use crate::Configuration; 54 | 55 | #[derive(Configuration)] 56 | struct Config { 57 | time: NaiveTime, 58 | } 59 | 60 | let toml = r#" 61 | time = "10:00" 62 | "#; 63 | 64 | assert_eq!( 65 | Config::builder() 66 | .override_with(TomlSource::new(toml)) 67 | .try_build() 68 | .unwrap() 69 | .time, 70 | NaiveTime::from_hms_opt(10, 0, 0).unwrap() 71 | ); 72 | } 73 | 74 | #[test] 75 | fn naive_date_format() { 76 | use chrono::NaiveDate; 77 | 78 | use crate::Configuration; 79 | 80 | #[derive(Configuration)] 81 | struct Config { 82 | date: NaiveDate, 83 | } 84 | 85 | let toml = r#" 86 | date = "2013-08-09" 87 | "#; 88 | 89 | assert_eq!( 90 | Config::builder() 91 | .override_with(TomlSource::new(toml)) 92 | .try_build() 93 | .unwrap() 94 | .date, 95 | NaiveDate::from_ymd_opt(2013, 8, 9).unwrap() 96 | ); 97 | } 98 | } 99 | } 100 | 101 | #[cfg(feature = "rust_decimal")] 102 | mod decimal { 103 | use rust_decimal::Decimal; 104 | 105 | use crate::Configuration; 106 | 107 | impl Configuration for Decimal { 108 | type Builder = Option; 109 | } 110 | } 111 | 112 | #[cfg(feature = "ipnetwork")] 113 | mod ipnetwork { 114 | use ipnetwork::IpNetwork; 115 | 116 | use crate::Configuration; 117 | 118 | impl Configuration for IpNetwork { 119 | type Builder = Option; 120 | } 121 | } 122 | 123 | #[cfg(feature = "js_option")] 124 | mod js_option { 125 | use js_option::JsOption; 126 | use serde::de::DeserializeOwned; 127 | 128 | use crate::{ 129 | helpers::{BuilderOf, TargetOf}, 130 | Configuration, ConfigurationBuilder, 131 | }; 132 | 133 | impl Configuration for JsOption 134 | where 135 | T: DeserializeOwned + Configuration, 136 | { 137 | type Builder = JsOption>; 138 | } 139 | 140 | impl ConfigurationBuilder for JsOption 141 | where 142 | T: DeserializeOwned + ConfigurationBuilder, 143 | { 144 | type Target = JsOption>; 145 | 146 | fn merge(self, other: Self) -> Self { 147 | match (self, other) { 148 | // If both `Some` then merge the contained builders 149 | (Self::Some(us), Self::Some(other)) => Self::Some(us.merge(other)), 150 | // If we don't have a value then always take the other 151 | (Self::Undefined, other) => other, 152 | // Either: 153 | // - We're explicitly `Null` 154 | // - We're explicitly `Some` and the other is `Undefined` or `Null` 155 | // 156 | // In either case, just take our value, which should be preferred to other. 157 | (us, _) => us, 158 | } 159 | } 160 | 161 | fn try_build(self) -> Result { 162 | match self { 163 | Self::Undefined => Ok(Self::Target::Undefined), 164 | Self::Null => Ok(Self::Target::Null), 165 | Self::Some(val) => Ok(Self::Target::Some(val.try_build()?)), 166 | } 167 | } 168 | 169 | fn contains_non_secret_data(&self) -> Result { 170 | match self { 171 | Self::Some(data) => data.contains_non_secret_data(), 172 | 173 | // An explicit `Null` is counted as data, overriding any default. 174 | Self::Null => Ok(true), 175 | 176 | Self::Undefined => Ok(false), 177 | } 178 | } 179 | } 180 | } 181 | 182 | #[cfg(feature = "secrecy")] 183 | mod secrecy { 184 | use secrecy::SecretString; 185 | 186 | use crate::{Configuration, SecretOption}; 187 | 188 | impl Configuration for SecretString { 189 | type Builder = SecretOption; 190 | } 191 | } 192 | 193 | #[cfg(feature = "url")] 194 | mod url { 195 | use url::Url; 196 | 197 | use crate::Configuration; 198 | 199 | impl Configuration for Url { 200 | type Builder = Option; 201 | } 202 | } 203 | 204 | #[cfg(feature = "uuid")] 205 | mod uuid { 206 | use uuid::Uuid; 207 | 208 | use crate::Configuration; 209 | 210 | impl Configuration for Uuid { 211 | type Builder = Option; 212 | } 213 | } 214 | 215 | #[cfg(feature = "bigdecimal")] 216 | mod bigdecimal { 217 | use bigdecimal::BigDecimal; 218 | 219 | use crate::Configuration; 220 | 221 | impl Configuration for BigDecimal { 222 | type Builder = Option; 223 | } 224 | } 225 | 226 | #[cfg(feature = "ahash")] 227 | mod ahash { 228 | use std::{fmt::Display, hash::Hash}; 229 | 230 | use ahash::{AHashMap, AHashSet}; 231 | use serde::de::DeserializeOwned; 232 | 233 | use crate::{ 234 | helpers::{BuilderOf, KeyedContainer, KeyedContainerBuilder, UnkeyedContainerBuilder}, 235 | Configuration, 236 | }; 237 | 238 | impl Configuration for AHashSet 239 | where 240 | T: Configuration + Hash + Eq, 241 | BuilderOf: Hash + Eq + 'static, 242 | { 243 | type Builder = UnkeyedContainerBuilder>, Self>; 244 | } 245 | 246 | impl KeyedContainer for AHashMap 247 | where 248 | K: Hash + Eq, 249 | { 250 | type Key = K; 251 | type Value = V; 252 | 253 | fn insert(&mut self, k: Self::Key, v: Self::Value) { 254 | self.insert(k, v); 255 | } 256 | 257 | fn remove(&mut self, k: &Self::Key) -> Option { 258 | self.remove(k) 259 | } 260 | } 261 | 262 | impl Configuration for AHashMap 263 | where 264 | K: Hash + Eq + Display + DeserializeOwned + 'static, 265 | V: Configuration, 266 | BuilderOf: 'static, 267 | { 268 | type Builder = KeyedContainerBuilder>, Self>; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /confik/tests/secret/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | use std::{ 4 | fmt::Debug, 5 | hash::{BuildHasher, Hasher}, 6 | }; 7 | 8 | use confik::Configuration; 9 | use serde::Deserialize; 10 | 11 | #[allow(dead_code)] // unused in no-default-features cases 12 | #[derive(Debug, Default, PartialEq, Eq, Configuration, Deserialize)] 13 | struct Num(usize); 14 | 15 | #[allow(dead_code)] // unused in no-default-features cases 16 | #[derive(Debug, Default, PartialEq, Eq, Configuration, Deserialize)] 17 | struct PartiallySecret { 18 | public: Num, 19 | #[confik(secret)] 20 | secret: Num, 21 | } 22 | 23 | #[allow(dead_code)] // unused in no-default-features cases 24 | #[derive(Debug, Default, PartialEq, Eq, Configuration, Deserialize)] 25 | struct NotSecret { 26 | public: PartiallySecret, 27 | } 28 | 29 | #[allow(dead_code)] // unused in no-default-features cases 30 | #[derive(Debug, Default, PartialEq, Eq, Hash, Configuration, Deserialize)] 31 | struct MaybeSecret { 32 | non_secret: Option, 33 | #[confik(secret)] 34 | secret: Option, 35 | } 36 | 37 | #[allow(dead_code)] // unused in no-default-features cases 38 | #[derive(Debug, Configuration, Deserialize)] 39 | struct MaybeSecretVec { 40 | seq: Vec, 41 | } 42 | 43 | #[allow(dead_code)] // unused in no-default-features cases 44 | #[derive(Debug, Configuration, Deserialize)] 45 | struct MaybeSecretArray { 46 | seq: [MaybeSecret; 2], 47 | } 48 | 49 | #[cfg(feature = "json")] 50 | mod json { 51 | use assert_matches::assert_matches; 52 | use confik::{ConfigBuilder, Error, JsonSource}; 53 | 54 | use super::NotSecret; 55 | 56 | #[test] 57 | fn check_json_is_not_secret() { 58 | let target = ConfigBuilder::::default() 59 | .override_with(JsonSource::new(r#"{"public": {"public": 1, "secret": 2}}"#)) 60 | .try_build() 61 | .expect_err("JSON deserialization is not a secret source"); 62 | 63 | assert_matches!( 64 | &target, 65 | Error::UnexpectedSecret(path, _) if path.to_string().contains("public.secret") 66 | ); 67 | } 68 | } 69 | 70 | #[cfg(feature = "toml")] 71 | mod toml { 72 | use std::{ 73 | collections::{BTreeMap, HashMap}, 74 | fmt::Debug, 75 | }; 76 | 77 | use assert_matches::assert_matches; 78 | use confik::{ConfigBuilder, Configuration, TomlSource}; 79 | use indoc::indoc; 80 | use serde::{de::DeserializeOwned, Deserialize}; 81 | 82 | use super::{DeterministicHash, MaybeSecret, MaybeSecretArray, MaybeSecretVec, NotSecret}; 83 | 84 | #[test] 85 | fn check_toml_is_not_secret() { 86 | use confik::Error; 87 | 88 | let target = ConfigBuilder::::default() 89 | .override_with(TomlSource::new("[public]\npublic = 1\nsecret = 2")) 90 | .try_build() 91 | .expect_err("Toml deserialization is not a secret source"); 92 | 93 | assert_matches!( 94 | &target, 95 | Error::UnexpectedSecret(path, _) if path.to_string().contains("`public.secret`") 96 | ); 97 | } 98 | 99 | /// This functions and all tests using are to catch the issue in FUT-5298 100 | /// in which depending on ordering a non-secret `Source` may not be caught 101 | fn check_secret_error_seq_propagation(expected_path: &str) 102 | where 103 | T: DeserializeOwned + Configuration + Debug, 104 | { 105 | use confik::Error; 106 | 107 | let target = ConfigBuilder::::default() 108 | .override_with(TomlSource::new(indoc! {r#" 109 | [[seq]] 110 | non_secret = "non_secret" 111 | [[seq]] 112 | secret = "secret" 113 | "#})) 114 | .try_build() 115 | .expect_err("Toml deserialization is not a secret source"); 116 | 117 | assert_matches!( 118 | &target, 119 | Error::UnexpectedSecret(path, _) if path.to_string().contains(&format!("`{expected_path}`")) 120 | ); 121 | } 122 | 123 | #[test] 124 | fn check_secret_error_vec_propagation() { 125 | check_secret_error_seq_propagation::("seq.1.secret"); 126 | } 127 | 128 | #[test] 129 | fn check_secret_error_array_propagation() { 130 | check_secret_error_seq_propagation::("seq.1.secret"); 131 | } 132 | 133 | fn check_secret_error_map_propagation() 134 | where 135 | M: for<'a> Deserialize<'a> + Configuration + Debug, 136 | { 137 | use confik::Error; 138 | 139 | let target = ConfigBuilder::::default() 140 | .override_with(TomlSource::new(indoc! {r#" 141 | [a] 142 | non_secret = "non_secret" 143 | [b] 144 | secret = "secret" 145 | "#})) 146 | .try_build() 147 | .expect_err("Toml deserialization is not a secret source"); 148 | 149 | assert_matches!( 150 | &target, 151 | Error::UnexpectedSecret(path, _) if path.to_string().contains("`b.secret`") 152 | ); 153 | } 154 | 155 | #[test] 156 | fn check_secret_error_hashmap_propagation() { 157 | check_secret_error_map_propagation::>(); 158 | } 159 | 160 | #[test] 161 | fn check_secret_error_btreemap_propagation() { 162 | check_secret_error_map_propagation::>(); 163 | } 164 | } 165 | 166 | #[cfg(feature = "env")] 167 | mod env { 168 | use confik::{ConfigBuilder, EnvSource}; 169 | 170 | use super::{NotSecret, Num, PartiallySecret}; 171 | 172 | #[test] 173 | fn check_env_is_secret() { 174 | let result = temp_env::with_vars( 175 | vec![("PUBLIC__PUBLIC", Some("3")), ("PUBLIC__SECRET", Some("4"))], 176 | || { 177 | ConfigBuilder::::default() 178 | .override_with(EnvSource::new().allow_secrets()) 179 | .try_build() 180 | }, 181 | ); 182 | assert_eq!( 183 | result.expect("Env is a secret source"), 184 | NotSecret { 185 | public: PartiallySecret { 186 | public: Num(3), 187 | secret: Num(4), 188 | } 189 | } 190 | ); 191 | } 192 | 193 | #[cfg(feature = "toml")] 194 | mod toml { 195 | use confik::{ConfigBuilder, EnvSource, TomlSource}; 196 | 197 | use super::{NotSecret, Num, PartiallySecret}; 198 | 199 | #[test] 200 | fn check_partial_secret() { 201 | let result = temp_env::with_vars(vec![("public__secret", Some("5"))], || { 202 | ConfigBuilder::::default() 203 | .override_with(EnvSource::new().allow_secrets()) 204 | .override_with(TomlSource::new("[public]\npublic = 6")) 205 | .try_build() 206 | }); 207 | 208 | assert_eq!( 209 | result.expect("Env is a secret source"), 210 | NotSecret { 211 | public: PartiallySecret { 212 | public: Num(6), 213 | secret: Num(5), 214 | } 215 | } 216 | ); 217 | } 218 | 219 | #[test] 220 | fn check_partial_secret_with_prefix() { 221 | let result = 222 | temp_env::with_vars(vec![("my_prefix_public__secret", Some("5"))], move || { 223 | let mut config = envious::Config::new(); 224 | config.with_prefix("my_prefix_"); 225 | 226 | ConfigBuilder::::default() 227 | .override_with(EnvSource::new().with_config(config).allow_secrets()) 228 | .override_with(TomlSource::new("[public]\npublic = 6")) 229 | .try_build() 230 | }); 231 | 232 | assert_eq!( 233 | result.expect("Env is a secret source"), 234 | NotSecret { 235 | public: PartiallySecret { 236 | public: Num(6), 237 | secret: Num(5), 238 | } 239 | } 240 | ); 241 | } 242 | } 243 | } 244 | 245 | /// In order to have the `HashMap` case fail FUT-5298 deterministically, 246 | /// we need to ensure the entires are ordered deterministically. 247 | #[allow(dead_code)] // unused in no-default-features cases 248 | #[derive(Debug, Default)] 249 | struct DeterministicHash(u64); 250 | 251 | impl BuildHasher for DeterministicHash { 252 | type Hasher = Self; 253 | 254 | fn build_hasher(&self) -> Self::Hasher { 255 | Self::default() 256 | } 257 | } 258 | 259 | impl Hasher for DeterministicHash { 260 | fn write(&mut self, bytes: &[u8]) { 261 | // We want `a` before `b` same as all the others would store it 262 | match bytes[0] { 263 | b'a' => self.0 = 1, 264 | b'b' => self.0 = 2, 265 | // Not sure where this comes from, but ignore it 266 | 255 => (), 267 | b => unimplemented!("{}: {}", b, String::from_utf8_lossy(bytes)), 268 | } 269 | } 270 | 271 | fn finish(&self) -> u64 { 272 | self.0 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /confik/src/std_impls.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of [`Configuration`](crate::Configuration) for standard library types. 2 | 3 | use std::{ 4 | collections::{BTreeMap, BTreeSet, HashMap, HashSet}, 5 | ffi::OsString, 6 | fmt::Display, 7 | hash::{BuildHasher, Hash}, 8 | marker::PhantomData, 9 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 10 | path::PathBuf, 11 | sync::atomic::{ 12 | AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32, 13 | AtomicU64, AtomicU8, AtomicUsize, 14 | }, 15 | time::{Duration, SystemTime}, 16 | }; 17 | 18 | use serde::{de::DeserializeOwned, Deserialize}; 19 | 20 | use crate::{ 21 | helpers::{ 22 | BuilderOf, KeyedContainer, KeyedContainerBuilder, TargetOf, UnkeyedContainerBuilder, 23 | }, 24 | Configuration, ConfigurationBuilder, Error, MissingValue, UnexpectedSecret, 25 | }; 26 | 27 | /// Convenience macro for the large number of foreign library types to implement the 28 | /// [`Configuration`] using an [`Option`] as their [`ConfigurationBuilder`]. 29 | macro_rules! impl_multi_source_via_option { 30 | ($type:ty) => { 31 | impl Configuration for $type { 32 | type Builder = Option; 33 | } 34 | }; 35 | 36 | ($($type:ty),* $(,)?) => { 37 | $( 38 | impl_multi_source_via_option! { $type } 39 | )* 40 | }; 41 | } 42 | 43 | impl_multi_source_via_option! { 44 | // Signed integers 45 | i8, i16, i32, i64, i128, isize, 46 | 47 | // Unsigned integers 48 | u8, u16, u32, u64, u128, usize, 49 | 50 | // Floats 51 | f32, f64, 52 | 53 | // Networking types 54 | SocketAddr, SocketAddrV4, SocketAddrV6, IpAddr, Ipv4Addr, Ipv6Addr, 55 | 56 | // Time 57 | Duration, SystemTime, 58 | 59 | // Other standard types 60 | String, OsString, PathBuf, char, bool, 61 | 62 | // Atomic types 63 | AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicIsize, 64 | AtomicU8, AtomicU16, AtomicU32, AtomicU64, AtomicUsize, 65 | AtomicBool, 66 | } 67 | 68 | // Containers 69 | impl Configuration for Vec 70 | where 71 | T: Configuration, 72 | BuilderOf: 'static, 73 | { 74 | type Builder = UnkeyedContainerBuilder>, Self>; 75 | } 76 | 77 | impl Configuration for BTreeSet 78 | where 79 | T: Configuration + Ord, 80 | BuilderOf: Ord + 'static, 81 | { 82 | type Builder = UnkeyedContainerBuilder>, Self>; 83 | } 84 | 85 | impl Configuration for HashSet 86 | where 87 | T: Configuration + Eq + Hash, 88 | BuilderOf: Hash + Eq + 'static, 89 | S: BuildHasher + Default + 'static, 90 | { 91 | type Builder = UnkeyedContainerBuilder, S>, Self>; 92 | } 93 | 94 | impl KeyedContainer for BTreeMap 95 | where 96 | K: Ord, 97 | { 98 | type Key = K; 99 | type Value = V; 100 | 101 | fn insert(&mut self, k: Self::Key, v: Self::Value) { 102 | self.insert(k, v); 103 | } 104 | 105 | fn remove(&mut self, k: &Self::Key) -> Option { 106 | self.remove(k) 107 | } 108 | } 109 | 110 | impl Configuration for BTreeMap 111 | where 112 | K: Ord + Display + DeserializeOwned + 'static, 113 | V: Configuration, 114 | BuilderOf: 'static, 115 | { 116 | type Builder = KeyedContainerBuilder>, Self>; 117 | } 118 | 119 | impl KeyedContainer for HashMap 120 | where 121 | K: Hash + Eq, 122 | S: BuildHasher + Default, 123 | { 124 | type Key = K; 125 | type Value = V; 126 | 127 | fn insert(&mut self, k: Self::Key, v: Self::Value) { 128 | self.insert(k, v); 129 | } 130 | 131 | fn remove(&mut self, k: &Self::Key) -> Option { 132 | self.remove(k) 133 | } 134 | } 135 | 136 | impl Configuration for HashMap 137 | where 138 | K: Hash + Eq + Display + DeserializeOwned + 'static, 139 | V: Configuration, 140 | BuilderOf: 'static, 141 | S: Default + BuildHasher + 'static, 142 | { 143 | type Builder = KeyedContainerBuilder, S>, Self>; 144 | } 145 | 146 | impl Configuration for [T; N] 147 | where 148 | [BuilderOf; N]: DeserializeOwned + Default, 149 | T: Configuration, 150 | { 151 | type Builder = [BuilderOf; N]; 152 | } 153 | 154 | impl ConfigurationBuilder for [T; N] 155 | where 156 | Self: DeserializeOwned + Default, 157 | T: ConfigurationBuilder, 158 | { 159 | type Target = [TargetOf; N]; 160 | 161 | fn merge(self, other: Self) -> Self { 162 | let mut iter = other.into_iter(); 163 | self.map(|us| us.merge(iter.next().unwrap())) 164 | } 165 | 166 | fn try_build(self) -> Result { 167 | self.into_iter() 168 | .enumerate() 169 | .map(|(index, val)| { 170 | val.try_build().map_err(|err| match err { 171 | Error::MissingValue(err) => Error::MissingValue(err.prepend(index.to_string())), 172 | err => err, 173 | }) 174 | }) 175 | .collect::, _>>()? 176 | .try_into() 177 | .map_err(|vec: Vec<_>| { 178 | Error::MissingValue(MissingValue::default().prepend(vec.len().to_string())) 179 | }) 180 | } 181 | 182 | fn contains_non_secret_data(&self) -> Result { 183 | self.iter() 184 | .map(ConfigurationBuilder::contains_non_secret_data) 185 | .enumerate() 186 | .try_fold(false, |has_secret, (index, val)| { 187 | Ok(val.map_err(|err| err.prepend(index.to_string()))? || has_secret) 188 | }) 189 | } 190 | } 191 | 192 | /// `PhantomData` does not need a builder, however we cannot use `()` as that would make `T` 193 | /// unconstrained. Instead just making it use itself as a builder and rely on serde handling it 194 | /// alright. 195 | impl Configuration for PhantomData { 196 | type Builder = Self; 197 | } 198 | 199 | /// `PhantomData` does not need a builder, however we cannot use `()` as that would make `T` 200 | /// unconstrained. Instead just making it use itself as a builder and rely on serde handling it 201 | /// alright. 202 | impl ConfigurationBuilder for PhantomData { 203 | type Target = Self; 204 | 205 | fn merge(self, _other: Self) -> Self { 206 | self 207 | } 208 | 209 | fn try_build(self) -> Result { 210 | Ok(self) 211 | } 212 | 213 | fn contains_non_secret_data(&self) -> Result { 214 | Ok(false) 215 | } 216 | } 217 | 218 | /// Build an `Option` with a custom structure as we want `None` to be an explicit value that will 219 | /// not be overwritten. 220 | impl Configuration for Option 221 | where 222 | OptionBuilder>: DeserializeOwned, 223 | { 224 | type Builder = OptionBuilder>; 225 | } 226 | 227 | /// Build an `Option` with a custom structure as we want `None` to be an explicit value that will 228 | /// not be overwritten. 229 | #[derive(Debug, Default, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)] 230 | #[serde(from = "Option")] 231 | pub enum OptionBuilder { 232 | /// No item has been provided yet. 233 | /// 234 | /// Default to `None` but allow overwriting by later [`merge`][ConfigurationBuilder::merge]s. 235 | #[default] 236 | Unspecified, 237 | 238 | /// Explicit `None`. 239 | /// 240 | /// Will not be overwritten by later [`merge`][ConfigurationBuilder::merge]s. 241 | None, 242 | 243 | /// Explicit `Some`. 244 | /// 245 | /// Will not be overwritten by later [`merge`][ConfigurationBuilder::merge]s. 246 | Some(T), 247 | } 248 | 249 | impl From> for OptionBuilder { 250 | fn from(opt: Option) -> Self { 251 | opt.map_or(Self::None, |val| Self::Some(val)) 252 | } 253 | } 254 | 255 | impl ConfigurationBuilder for OptionBuilder 256 | where 257 | Self: DeserializeOwned, 258 | { 259 | type Target = Option>; 260 | 261 | fn merge(self, other: Self) -> Self { 262 | match (self, other) { 263 | // If both `Some` then merge the contained builders 264 | (Self::Some(us), Self::Some(other)) => Self::Some(us.merge(other)), 265 | // If we don't have a value then always take the other 266 | (Self::Unspecified, other) => other, 267 | // Either: 268 | // - We're explicitly `None` 269 | // - We're explicitly `Some` and the other is `Unspecified` or `None` 270 | // 271 | // In either case, just take our value, which should be preferred to other. 272 | (us, _) => us, 273 | } 274 | } 275 | 276 | fn try_build(self) -> Result { 277 | match self { 278 | Self::Unspecified | Self::None => Ok(None), 279 | Self::Some(val) => Ok(Some(val.try_build()?)), 280 | } 281 | } 282 | 283 | fn contains_non_secret_data(&self) -> Result { 284 | match self { 285 | Self::Some(data) => data.contains_non_secret_data(), 286 | 287 | // An explicit `None` is counted as data, overriding any default. 288 | Self::None => Ok(true), 289 | 290 | Self::Unspecified => Ok(false), 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /confik/src/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for manual implementations of [`Configuration`]. 2 | //! 3 | //! Where possible, the derive should be prefered, but sometimes a manual implementation is 4 | //! required. 5 | 6 | use std::{fmt::Display, marker::PhantomData}; 7 | 8 | use serde::{de::DeserializeOwned, Deserialize}; 9 | 10 | use crate::{Configuration, ConfigurationBuilder, Error, MissingValue, UnexpectedSecret}; 11 | 12 | /// Type alias for easier usage of [`Configuration`] in complex generic statements 13 | pub type BuilderOf = ::Builder; 14 | 15 | /// Type alias for easier usage of [`KeyedContainerBuilder`] and [`UnkeyedContainerBuilder`] in complex generic statements 16 | pub type ItemOf = ::Item; 17 | 18 | /// Type alias for easier usage of [`KeyedContainerBuilder`] in complex generic statements 19 | pub type KeyOf = ::Key; 20 | 21 | /// Type alias for easier usage of [`ConfigurationBuilder`] in complex generic statements 22 | pub type TargetOf = ::Target; 23 | 24 | /// Type alias for easier usage of [`KeyedContainerBuilder`] in complex generic statements 25 | pub type ValueOf = ::Value; 26 | 27 | /// Builder type for unkeyed containers such as [`Vec`] (as opposed to keyed containers like 28 | /// [`HashMap`](std::collections::HashMap)). 29 | /// 30 | /// This is not required to be used, but is a convient shortcut for unkeyed container types' 31 | /// implementations. 32 | /// 33 | /// For keyed containers, see [`KeyedContainerBuilder`]. 34 | /// 35 | /// Example usage: 36 | /// ```rust 37 | /// use confik::{ 38 | /// helpers::{BuilderOf, UnkeyedContainerBuilder}, 39 | /// Configuration, 40 | /// }; 41 | /// use serde::Deserialize; 42 | /// 43 | /// struct MyVec { 44 | /// // ... 45 | /// # __: Vec, 46 | /// } 47 | /// 48 | /// impl<'de, T> Deserialize<'de> for MyVec { 49 | /// fn deserialize(deserializer: D) -> Result 50 | /// where 51 | /// D: serde::Deserializer<'de>, 52 | /// { 53 | /// // ... 54 | /// # unimplemented!() 55 | /// } 56 | /// } 57 | /// 58 | /// impl Default for MyVec { 59 | /// fn default() -> Self { 60 | /// // ... 61 | /// # unimplemented!() 62 | /// } 63 | /// } 64 | /// 65 | /// impl IntoIterator for MyVec { 66 | /// type Item = T; 67 | /// 68 | /// type IntoIter = // ... 69 | /// # as IntoIterator>::IntoIter; 70 | /// 71 | /// fn into_iter(self) -> Self::IntoIter { 72 | /// // ... 73 | /// # unimplemented!() 74 | /// } 75 | /// } 76 | /// 77 | /// impl<'a, T> IntoIterator for &'a MyVec { 78 | /// type Item = &'a T; 79 | /// 80 | /// type IntoIter = // ... 81 | /// # <&'a [T] as IntoIterator>::IntoIter; 82 | /// 83 | /// fn into_iter(self) -> Self::IntoIter { 84 | /// // ... 85 | /// # unimplemented!() 86 | /// } 87 | /// } 88 | /// 89 | /// impl FromIterator for MyVec { 90 | /// fn from_iter>(iter: I) -> Self { 91 | /// // ... 92 | /// # unimplemented!() 93 | /// } 94 | /// } 95 | /// 96 | /// impl Configuration for MyVec 97 | /// where 98 | /// T: Configuration, 99 | /// BuilderOf: 'static, 100 | /// { 101 | /// type Builder = UnkeyedContainerBuilder>, Self>; 102 | /// } 103 | /// ``` 104 | #[derive(Debug, Default, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)] 105 | #[serde(from = "Container")] 106 | pub enum UnkeyedContainerBuilder { 107 | /// No data has been provided yet. 108 | /// 109 | /// Default to `None` but allow overwriting by later [`merge`][ConfigurationBuilder::merge]s. 110 | #[default] 111 | Unspecified, 112 | 113 | /// Data has been provided. 114 | /// 115 | /// Will not be overwritten by later [`merge`][ConfigurationBuilder::merge]s. 116 | Some(Container), 117 | 118 | /// Never instantiated, used to hold the [`Target`][ConfigurationBuilder::Target] type. 119 | _PhantomData(PhantomData Target>), 120 | } 121 | 122 | impl From for UnkeyedContainerBuilder { 123 | fn from(value: Container) -> Self { 124 | Self::Some(value) 125 | } 126 | } 127 | 128 | impl ConfigurationBuilder for UnkeyedContainerBuilder 129 | where 130 | Self: DeserializeOwned, 131 | Container: IntoIterator + 'static, 132 | ItemOf: ConfigurationBuilder, 133 | Target: Default + FromIterator>>, 134 | for<'a> &'a Container: IntoIterator>, 135 | { 136 | type Target = Target; 137 | 138 | fn merge(self, other: Self) -> Self { 139 | if matches!(self, Self::Unspecified) { 140 | other 141 | } else { 142 | self 143 | } 144 | } 145 | 146 | fn try_build(self) -> Result { 147 | match self { 148 | Self::Unspecified => Err(Error::MissingValue(MissingValue::default())), 149 | Self::Some(val) => val 150 | .into_iter() 151 | .map(ConfigurationBuilder::try_build) 152 | .collect(), 153 | Self::_PhantomData(_) => unreachable!("PhantomData is never instantiated"), 154 | } 155 | } 156 | 157 | fn contains_non_secret_data(&self) -> Result { 158 | match self { 159 | Self::Unspecified => Ok(false), 160 | 161 | // An explicit empty container is counted as as data, overriding any default. 162 | // If this branch is ever reached, then there is some data, even if it is empty. 163 | // So always return either an error or `true`. 164 | Self::Some(val) => val 165 | .into_iter() 166 | .map(ConfigurationBuilder::contains_non_secret_data) 167 | .enumerate() 168 | .find(|(_index, result)| result.is_err()) 169 | .map(|(index, result)| result.map_err(|err| err.prepend(index.to_string()))) 170 | .unwrap_or(Ok(true)), 171 | 172 | Self::_PhantomData(_) => unreachable!("PhantomData is never instantiated"), 173 | } 174 | } 175 | } 176 | 177 | /// Trait governing access to keyed containers like [`HashMap`](std::collections::HashMap) (as 178 | /// opposed to unkeyed containers like [`Vec`]). 179 | /// 180 | /// This trait purely exists to allow for simple usage of [`KeyedContainerBuilder`]. See the docs 181 | /// there for details. 182 | pub trait KeyedContainer { 183 | type Key; 184 | type Value; 185 | 186 | fn insert(&mut self, k: Self::Key, v: Self::Value); 187 | fn remove(&mut self, k: &Self::Key) -> Option; 188 | } 189 | 190 | /// Builder type for keyed containers, such as [`HashMap`](std::collections::HashMap) (as opposed 191 | /// to unkeyed containers like [`Vec`]). This is not required to be used, but is a convient 192 | /// shortcut for map types' implementations. 193 | /// 194 | /// Types using this as their builder must implement [`KeyedContainer`]. 195 | /// 196 | /// For unkeyed containers, see [`UnkeyedContainerBuilder`]. 197 | /// 198 | /// Example usage: 199 | /// ```rust 200 | /// use std::fmt::Display; 201 | /// 202 | /// use confik::{ 203 | /// helpers::{BuilderOf, KeyedContainer, KeyedContainerBuilder}, 204 | /// Configuration, 205 | /// }; 206 | /// use serde::Deserialize; 207 | /// 208 | /// struct MyMap { 209 | /// // ... 210 | /// # __: Vec<(K, V)>, 211 | /// } 212 | /// 213 | /// impl<'de, K, V> Deserialize<'de> for MyMap { 214 | /// fn deserialize(deserializer: D) -> Result 215 | /// where 216 | /// D: serde::Deserializer<'de>, 217 | /// { 218 | /// // ... 219 | /// # unimplemented!() 220 | /// } 221 | /// } 222 | /// 223 | /// impl Default for MyMap { 224 | /// fn default() -> Self { 225 | /// // ... 226 | /// # unimplemented!() 227 | /// } 228 | /// } 229 | /// 230 | /// impl IntoIterator for MyMap { 231 | /// type Item = (K, V); 232 | /// 233 | /// type IntoIter = // ... 234 | /// # as IntoIterator>::IntoIter; 235 | /// 236 | /// fn into_iter(self) -> Self::IntoIter { 237 | /// // ... 238 | /// # unimplemented!() 239 | /// } 240 | /// } 241 | /// 242 | /// impl<'a, K, V> IntoIterator for &'a MyMap { 243 | /// type Item = (&'a K, &'a V); 244 | /// 245 | /// type IntoIter = // ... 246 | /// # <&'a std::collections::HashMap as IntoIterator>::IntoIter; 247 | /// 248 | /// fn into_iter(self) -> Self::IntoIter { 249 | /// // ... 250 | /// # unimplemented!() 251 | /// } 252 | /// } 253 | /// 254 | /// impl FromIterator<(K, V)> for MyMap { 255 | /// fn from_iter>(iter: I) -> Self { 256 | /// // ... 257 | /// # unimplemented!() 258 | /// } 259 | /// } 260 | /// 261 | /// impl KeyedContainer for MyMap { 262 | /// type Key = K; 263 | /// 264 | /// type Value = V; 265 | /// 266 | /// fn insert(&mut self, k: Self::Key, v: Self::Value) { 267 | /// // ... 268 | /// # unimplemented!() 269 | /// } 270 | /// 271 | /// fn remove(&mut self, k: &Self::Key) -> Option { 272 | /// // ... 273 | /// # unimplemented!() 274 | /// } 275 | /// } 276 | /// 277 | /// impl Configuration for MyMap 278 | /// where 279 | /// K: Display + 'static, 280 | /// V: Configuration, 281 | /// BuilderOf: 'static, 282 | /// { 283 | /// type Builder = KeyedContainerBuilder>, Self>; 284 | /// } 285 | /// ``` 286 | #[derive(Debug, Default, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)] 287 | #[serde(from = "Container")] 288 | pub enum KeyedContainerBuilder { 289 | /// No data has been provided yet. 290 | /// 291 | /// Default to `None` but allow overwriting by later [`merge`][ConfigurationBuilder::merge]s. 292 | #[default] 293 | Unspecified, 294 | 295 | /// Data has been provided. 296 | /// 297 | /// Will not be overwritten by later [`merge`][ConfigurationBuilder::merge]s. 298 | Some(Container), 299 | 300 | /// Never instantiated, used to hold the [`Target`][ConfigurationBuilder::Target] type. 301 | _PhantomData(PhantomData Target>), 302 | } 303 | 304 | impl From for KeyedContainerBuilder { 305 | fn from(value: Container) -> Self { 306 | Self::Some(value) 307 | } 308 | } 309 | 310 | impl ConfigurationBuilder for KeyedContainerBuilder 311 | where 312 | Self: DeserializeOwned, 313 | Container: 314 | KeyedContainer + IntoIterator, ValueOf)> + 'static, 315 | KeyOf: Display, 316 | ValueOf: ConfigurationBuilder + 'static, 317 | Target: Default + FromIterator<(KeyOf, TargetOf>)>, 318 | for<'a> &'a Container: IntoIterator, &'a ValueOf)>, 319 | { 320 | type Target = Target; 321 | 322 | fn merge(self, other: Self) -> Self { 323 | match (self, other) { 324 | (Self::_PhantomData(_), _) | (_, Self::_PhantomData(_)) => { 325 | unreachable!("PhantomData is never instantiated") 326 | } 327 | (Self::Unspecified, other) => other, 328 | (us, Self::Unspecified) => us, 329 | (Self::Some(mut us), Self::Some(other)) => { 330 | for (key, their_val) in other { 331 | let val = if let Some(our_val) = us.remove(&key) { 332 | our_val.merge(their_val) 333 | } else { 334 | their_val 335 | }; 336 | 337 | us.insert(key, val); 338 | } 339 | 340 | Self::Some(us) 341 | } 342 | } 343 | } 344 | 345 | fn try_build(self) -> Result { 346 | match self { 347 | Self::Unspecified => Err(Error::MissingValue(MissingValue::default())), 348 | Self::Some(val) => val 349 | .into_iter() 350 | .map(|(key, value)| Ok((key, value.try_build()?))) 351 | .collect(), 352 | Self::_PhantomData(_) => unreachable!("PhantomData is never instantiated"), 353 | } 354 | } 355 | 356 | fn contains_non_secret_data(&self) -> Result { 357 | match self { 358 | Self::Unspecified => Ok(false), 359 | 360 | // An explicit empty container is counted as as data, overriding any default. 361 | // If this branch is ever reached, then there is some data, even if it is empty. 362 | // So always return either an error or `true`. 363 | Self::Some(val) => val 364 | .into_iter() 365 | .map(|(key, value)| (key, value.contains_non_secret_data())) 366 | .find(|(_key, result)| result.is_err()) 367 | .map(|(key, result)| result.map_err(|err| err.prepend(key.to_string()))) 368 | .unwrap_or(Ok(true)), 369 | 370 | Self::_PhantomData(_) => unreachable!("PhantomData is never instantiated"), 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /confik/src/lib.md: -------------------------------------------------------------------------------- 1 | # `confik` 2 | 3 | `confik` is a library for reading application configuration split across multiple sources. 4 | 5 | ## Example 6 | 7 | Assume that `config.toml` contains: 8 | 9 | ```toml 10 | host=google.com 11 | username=root 12 | ``` 13 | 14 | and the environment contains: 15 | 16 | ```bash 17 | PASSWORD=hunter2 18 | ``` 19 | 20 | then: 21 | 22 | ```no_run 23 | # #[cfg(all(feature = "toml", feature = "env"))] 24 | # { 25 | use confik::{Configuration, EnvSource, FileSource, TomlSource}; 26 | 27 | #[derive(Debug, PartialEq, Configuration)] 28 | struct Config { 29 | host: String, 30 | username: String, 31 | 32 | #[confik(secret)] 33 | password: String, 34 | } 35 | 36 | let config = Config::builder() 37 | .override_with(FileSource::new("config.toml")) 38 | .override_with(EnvSource::new().allow_secrets()) 39 | .try_build() 40 | .unwrap(); 41 | 42 | assert_eq!( 43 | config, 44 | Config { 45 | host: "google.com".to_string(), 46 | username: "root".to_string(), 47 | password: "hunter2".to_string(), 48 | } 49 | ); 50 | # } 51 | ``` 52 | 53 | ## Sources 54 | 55 | A [`Source`] is any type that can create [`ConfigurationBuilder`]s. This crate implements the following sources: 56 | 57 | - [`EnvSource`]: Loads configuration from environment variables using the [`envious`] crate. Requires the `env` feature. (Enabled by default.) 58 | - [`FileSource`]: Loads configuration from a file, detecting `json` or `toml` files based on the file extension. Requires the `json` and `toml` feature respectively. (`toml` is enabled by default.) 59 | - [`TomlSource`]: Loads configuration from a TOML string literal. Requires the `toml` feature. (Enabled by default.) 60 | - [`JsonSource`]: Loads configuration from a JSON string literal. Requires the `json` feature. 61 | - [`OffsetSource`]: Loads configuration from an inner source that is provided to it, but applied to a particular offset of the root configuration builder. 62 | 63 | ## Secrets 64 | 65 | Fields annotated with `#[confik(secret)]` will only be read from secure sources. This serves as a runtime check that no secrets have been stored in insecure places such as world-readable files. 66 | 67 | If a secret is found in an insecure source, an error will be returned. You can opt into loading secrets on a source-by-source basis. 68 | 69 | ## Macro usage 70 | 71 | The derive macro is called `Configuration` and is used as normal: 72 | 73 | ```rust 74 | #[derive(confik::Configuration)] 75 | struct Config { 76 | data: usize, 77 | } 78 | ``` 79 | 80 | ### Forwarding Attributes 81 | 82 | This allows forwarding any kind of attribute on to the builder. 83 | 84 | #### Serde 85 | 86 | The serde attributes used for customizing a `Deserialize` derive are achieved by adding `#[confik(forward(serde(...)))]` attributes. 87 | 88 | For example: 89 | 90 | ```rust 91 | # use confik::Configuration; 92 | #[derive(Configuration, Debug, PartialEq, Eq)] 93 | struct Field { 94 | #[confik(forward(serde(rename = "other_name")))] 95 | field1: usize, 96 | } 97 | ``` 98 | 99 | #### Derives 100 | 101 | If you need additional derives for your type, these can be added via `#[confik(forward(derive...))]` attributes. 102 | 103 | For example: 104 | 105 | ```rust 106 | # use confik::Configuration; 107 | #[derive(Debug, Configuration, Hash, Eq, PartialEq)] 108 | #[confik(forward(derive(Hash, Eq, PartialEq)))] 109 | struct Value { 110 | inner: String, 111 | } 112 | ``` 113 | 114 | ### Defaults 115 | 116 | Defaults are specified on a per-field basis. 117 | 118 | - Defaults only apply if no data has been read for that field. E.g., if `data` in the below example has one value read in, it will return an error. 119 | 120 | ```rust 121 | # #[cfg(feature = "toml")] 122 | # { 123 | use confik::{Configuration, TomlSource}; 124 | 125 | #[derive(Debug, Configuration)] 126 | struct Data { 127 | a: usize, 128 | b: usize, 129 | } 130 | 131 | #[derive(Debug, Configuration)] 132 | struct Config { 133 | #[confik(default = Data { a: 1, b: 2 })] 134 | data: Data 135 | } 136 | 137 | // Data is not specified, the default is used. 138 | let config = Config::builder() 139 | .try_build() 140 | .unwrap(); 141 | assert_eq!(config.data.a, 1); 142 | 143 | let toml = r#" 144 | [data] 145 | a = 1234 146 | "#; 147 | 148 | // Data is partially specified, but is insufficient to create it. The default is not used 149 | // and an error is returned. 150 | let config = Config::builder() 151 | .override_with(TomlSource::new(toml)) 152 | .try_build() 153 | .unwrap_err(); 154 | 155 | let toml = r#" 156 | [data] 157 | a = 1234 158 | b = 4321 159 | "#; 160 | 161 | // Data is fully specified and the default is not used. 162 | let config = Config::builder() 163 | .override_with(TomlSource::new(toml)) 164 | .try_build() 165 | .unwrap(); 166 | assert_eq!(config.data.a, 1234); 167 | # } 168 | ``` 169 | 170 | - Defaults can be given by any rust expression, and have [`Into::into`] run over them. E.g., 171 | 172 | ```rust 173 | const DEFAULT_VALUE: u8 = 4; 174 | 175 | #[derive(confik::Configuration)] 176 | struct Config { 177 | #[confik(default = DEFAULT_VALUE)] 178 | a: u32, 179 | #[confik(default = "hello world")] 180 | b: String, 181 | #[confik(default = 5f32)] 182 | c: f32, 183 | } 184 | ``` 185 | 186 | - Alternatively, a default without a given value called [`Default::default`]. E.g., 187 | 188 | ```rust 189 | use confik::{Configuration}; 190 | 191 | #[derive(Configuration)] 192 | struct Config { 193 | #[confik(default)] 194 | a: usize 195 | } 196 | 197 | let config = Config::builder().try_build().unwrap(); 198 | assert_eq!(config.a, 0); 199 | ``` 200 | 201 | ### Handling Foreign Types 202 | 203 | This crate provides implementations of [`Configuration`] for a number of `std` types and the following third-party crates. Implementations for third-party crates are feature gated. 204 | 205 | - `ahash`: v0.8 206 | - `bigdecimal`: v0.4 207 | - `bytesize`: v2 208 | - `camino`: v1 209 | - `chrono`: v0.4 210 | - `ipnetwork`: v0.21 211 | - `js_option`: v0.1 212 | - `rust_decimal`: v1 213 | - `secrecy`: v0.10 (Note that `#[config(secret)]` is not needed, although it is harmless, for these types as they are always treated as secrets.) 214 | - `url`: v1 215 | - `uuid`: v1 216 | 217 | If there's another foreign type used in your config, then you will not be able to implement [`Configuration`] for it. Instead any type that implements [`Into`] or [`TryInto`] can be used. 218 | 219 | ```rust 220 | struct ForeignType { 221 | data: usize, 222 | } 223 | 224 | #[derive(confik::Configuration)] 225 | struct MyForeignTypeCopy { 226 | data: usize 227 | } 228 | 229 | impl From for ForeignType { 230 | fn from(copy: MyForeignTypeCopy) -> Self { 231 | Self { 232 | data: copy.data, 233 | } 234 | } 235 | } 236 | 237 | #[derive(confik::Configuration)] 238 | struct MyForeignTypeIsize { 239 | data: isize 240 | } 241 | 242 | impl TryFrom for ForeignType { 243 | type Error = >::Error; 244 | 245 | fn try_from(copy: MyForeignTypeIsize) -> Result { 246 | Ok(Self { 247 | data: copy.data.try_into()?, 248 | }) 249 | } 250 | } 251 | 252 | #[derive(confik::Configuration)] 253 | struct Config { 254 | #[confik(from = MyForeignTypeCopy)] 255 | foreign_data: ForeignType, 256 | 257 | #[confik(try_from = MyForeignTypeIsize)] 258 | foreign_data_isized: ForeignType, 259 | } 260 | ``` 261 | 262 | ### Named builders 263 | 264 | If you want to directly access the builders, you can provide them with a name. This will also place the builder in the local module, to ensure there's a known path with which to reference them. 265 | 266 | ```rust 267 | #[derive(confik::Configuration)] 268 | #[confik(name = Builder)] 269 | struct Config { 270 | data: usize, 271 | } 272 | 273 | let _ = Builder { data: Default::default() }; 274 | ``` 275 | 276 | ### Field and Builder visibility 277 | 278 | Field and builder visibility are directly inherited from the underlying type. E.g. 279 | 280 | ```rust 281 | use confik::helpers::BuilderOf; 282 | 283 | mod config { 284 | #[derive(confik::Configuration)] 285 | pub struct Config { 286 | pub data: usize, 287 | } 288 | } 289 | 290 | let _ = BuilderOf:: { data: Default::default() }; 291 | ``` 292 | 293 | ### Skipping fields 294 | 295 | Fields can be skipped if necessary. This allows having types that cannot implement `Configuration` or be deserializable. However the field must have a `confik(default)` or `confik(default = ...)` attribute, otherwise it can't be built. E.g. 296 | 297 | ```rust 298 | # use std::time::Instant; 299 | #[derive(confik::Configuration)] 300 | struct Config { 301 | #[confik(skip, default = Instant::now())] 302 | loaded_at: Instant, 303 | } 304 | ``` 305 | 306 | ### Specifying `confik` Base 307 | 308 | Specify a path to the `confik` crate instance to use when referring to `confik` APIs from generated code. This is normally only applicable when invoking re-exported `confik` derives from a public macro in a different crate or when renaming `confik` in your Cargo manifest. 309 | 310 | ```rust,ignore 311 | # use std::time::Instant; 312 | #[derive(confik::Configuration)] 313 | #[confik(crate = reexported_confik)] 314 | struct Config { 315 | // ... 316 | } 317 | ``` 318 | 319 | ## Macro Limitations 320 | 321 | ### Custom `Deserialize` Implementations 322 | 323 | If you're using a custom `Deserialize` implementation, then you cannot use the `Configuration` derive macro. Instead, define the necessary config implementation manually like so: 324 | 325 | ```rust 326 | #[derive(Debug, serde_with::DeserializeFromStr)] 327 | enum MyEnum { 328 | Foo, 329 | Bar, 330 | }; 331 | 332 | impl std::str::FromStr for MyEnum { 333 | // ... 334 | # type Err = String; 335 | # fn from_str(_: &str) -> Result { unimplemented!() } 336 | } 337 | 338 | impl confik::Configuration for MyEnum { 339 | type Builder = Option; 340 | } 341 | ``` 342 | 343 | Note that the `Option` builder type only works for simple types. For more info, see the docs on [`Configuration`] and [`ConfigurationBuilder`]. 344 | 345 | ## Manual implementations 346 | 347 | It is strongly recommended to use the `derive` macro where possible. However, there may be cases where this is not possible. For some cases there are additional attributes available in the `derive` macro to tweak the behaviour, see the section on Handling Foreign Types. 348 | 349 | If you would like to manually implement `Configuration` for a type anyway, then this can mostly be broken down to three cases. 350 | 351 | ### Simple cases 352 | 353 | If your type cannot be partial specified (e.g. `usize`, `String`), then a simple `Option` builder can be used. 354 | 355 | ```rust 356 | #[derive(Debug, serde_with::DeserializeFromStr)] 357 | enum MyEnum { 358 | Foo, 359 | Bar, 360 | }; 361 | 362 | impl std::str::FromStr for MyEnum { 363 | // ... 364 | # type Err = String; 365 | # fn from_str(_: &str) -> Result { unimplemented!() } 366 | } 367 | 368 | impl confik::Configuration for MyEnum { 369 | type Builder = Option; 370 | } 371 | ``` 372 | 373 | ### Containers 374 | 375 | Unless your container holds another container, which already implements `Configuration`, you'll likely need to implement `Configuration` yourself, instead of with a `derive`. There are two type of containers that may need to be handled here. 376 | 377 | #### Keyed Containers 378 | 379 | Keyed containers have their contents separate from their keys. Examples of these are [`HashMap`](std::collections::HashMap) and [`BTreeMap`](std::collections::BTreeMap). Whilst the implementations can be provided fully, there are helpers available. These are the [`KeyedContainerBuilder`][KeyedContainerBuilder] type and the [`KeyedContainer`][KeyedContainer] trait. 380 | 381 | A type which implements all of [`KeyedContainer`][KeyedContainer], [`Deserialize`][serde::Deserialize], [`FromIterator`], [`Default`], and [`IntoIterator`] (for both the type and a reference to the type) can then use [`KeyedContainerBuilder`][KeyedContainerBuilder] as their builder. See [`KeyedContainerBuilder`][KeyedContainerBuilder] for an example. 382 | 383 | Note that the key needs to implement `Display` so that an accurate error stack can be generated. 384 | 385 | [KeyedContainerBuilder]: crate::helpers::KeyedContainerBuilder 386 | [KeyedContainer]: crate::helpers::KeyedContainer 387 | 388 | #### Unkeyed Containers 389 | 390 | Unkeyed containers are types without a separate key. This includes [`Vec`], but also types like [`HashSet`](std::collections::HashSet). Whilst the implementations can be provided fully, there is a helper available. This is the [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder]. 391 | 392 | A type which implements all of [`Deserialize`][serde::Deserialize], [`FromIterator`], [`Default`], and [`IntoIterator`] (for both the type and a reference to the type) can then use [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder] as their builder. See [`UnkeyedContainerBuilder`][UnkeyedContainerBuilder] for an example. 393 | 394 | [UnkeyedContainerBuilder]: crate::helpers::UnkeyedContainerBuilder 395 | 396 | #### Other complex cases 397 | 398 | For other complex cases, where `derive`s cannot work, the type is not simple enough to use an `Option` builder, and is not a container, there is currently no additional support. Please read through the [`Configuration`] and [`ConfigurationBuilder`] traits and implement them as appropriate. 399 | 400 | If you believe your type is following a common pattern where we could provide more support, please raise an issue (or even better an MR). 401 | --------------------------------------------------------------------------------