x52dev / confik

Multi-source configuration library for Rust.
32 stars 2 forks source link

Tagged enums require tag set in both TOML and Env sources #118

Open RapidPencil opened 3 months ago

RapidPencil commented 3 months ago

Say you have a serde tagged enum by #[confik(forward_serde(tag = "type"))] in the configuration:

use confik::Configuration;

#[derive(Debug, Clone, Configuration)]
#[confik(forward_serde(tag = "type", rename_all = "snake_case"))]
enum Config {
    Foo(Foo),
    Bar(Bar),
}

#[derive(Debug, Clone, Configuration)]
struct Foo {
    foo: String,
}

#[derive(Debug, Clone, Configuration)]
struct Bar {
    bar: String,
}

fn main() {
    let config = Config::builder()
        .override_with(confik::FileSource::new("config.toml"))
        .override_with(confik::EnvSource::new().allow_secrets())
        .try_build()
        .unwrap();
}

with config.toml

type = "foo"
foo = "hello"

and environment variable

TYPE=foo

The above works. However, remove either of type = "foo" from config.toml or TYPE=foo from the environment, and this fails with an error:

(...)
missing field `type`
(...)
tenuous-guidance commented 2 months ago

There are some limitations to confik that make this harder to avoid, but I think the actual bug is probably in serde. E.g.

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize, Default)]
struct ConfigOuter(#[serde(default)] Config);

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Config {
    Foo(Foo),
    Bar(Bar),
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
struct Foo {
    foo: String,
}

#[derive(Debug, Clone, Deserialize)]
struct Bar {
    bar: String,
}

#[test]
fn test() {
    // This fails with:
    //
    // ---- test stdout ----
    // thread 'test' panicked at src/main.rs:27:58:
    // called `Result::unwrap()` on an `Err` value: Error("missing field `type`", line: 1, column: 2)
    // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    //
    // However it should work and produce `ConfigOuter(Config::Unknown)`

    let config: ConfigOuter = serde_json::from_str("{}").unwrap();
}
tenuous-guidance commented 2 months ago

Similarity, serde(other) also fails for your example Config:

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum Config {
    Foo(Foo),
    Bar(Bar),
    #[serde(other)]
    Unknown,
}

#[derive(Debug, Clone, Deserialize)]
struct Foo {
    foo: String,
}

#[derive(Debug, Clone, Deserialize)]
struct Bar {
    bar: String,
}

#[test]
fn test() {
    // This fails with:
    //
    // ---- test stdout ----
    // thread 'test' panicked at src/main.rs:27:58:
    // called `Result::unwrap()` on an `Err` value: Error("missing field `type`", line: 1, column: 2)
    // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    //
    // However it should work and produce `Config::Unknown`

    let config: Config = serde_json::from_str("{}").unwrap();
}