confik icon indicating copy to clipboard operation
confik copied to clipboard

Tagged enums require tag set in both TOML and Env sources

Open RapidPencil opened this issue 1 year ago • 2 comments

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`
(...)

RapidPencil avatar Jul 09 '24 11:07 RapidPencil

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 avatar Jul 26 '24 05:07 tenuous-guidance

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();
}

tenuous-guidance avatar Jul 26 '24 05:07 tenuous-guidance