serde-vars 0.3.1

Conveniently expose (environment) variables to your serde based data structures, like configurations.
Documentation
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};

use serde::Deserialize;
use serde_vars::MapSource;

macro_rules! test_lookup {
    ($name:ident, $value:literal, $ty:ty) => {
        test_lookup!($name, $value, $ty, $value);
    };
    ($name:ident, $value:literal, $ty:ty, $expected:literal) => {
        #[test]
        fn $name() {
            let mut source =
                MapSource::new(HashMap::from([("FOO".to_owned(), $value.to_string())]));
            let mut de = serde_json::Deserializer::from_str("\"${FOO}\"");

            let s: $ty = serde_vars::deserialize(&mut de, &mut source).unwrap();
            assert_eq!(s, $expected);
        }
    };
}

test_lookup!(test_lookup_string, "bAr", String);
test_lookup!(test_lookup_string_wrapped, r#""bAr""#, String, "bAr");
test_lookup!(test_lookup_string_int_wrapped, r#""123""#, String, "123");
test_lookup!(test_lookup_i8, -2, i8);
test_lookup!(test_lookup_i16, -200, i16);
test_lookup!(test_lookup_i32, -2000, i32);
test_lookup!(test_lookup_i64, -20000, i64);
test_lookup!(test_lookup_u8, 20, u8);
test_lookup!(test_lookup_u16, 200, u16);
test_lookup!(test_lookup_u32, 2000, u32);
test_lookup!(test_lookup_u64, 20000, u64);
test_lookup!(test_lookup_f32, 1.0, f32);
test_lookup!(test_lookup_f64, 2.0, f64);

macro_rules! test_missing {
    ($name:ident, $ty:ty) => {
        #[test]
        fn $name() {
            let mut source = MapSource::default();
            let mut de = serde_json::Deserializer::from_str("\"${DOES_NOT_EXIST}\"");

            let err: Result<$ty, _> = serde_vars::deserialize(&mut de, &mut source);
            insta::assert_debug_snapshot!(
                err,
                @r###"
            Err(
                Error("got variable `${DOES_NOT_EXIST}`, but it does not exist", line: 0, column: 0),
            )
            "###
            );
        }
    };
}

test_missing!(test_missing_string, String);
test_missing!(test_missing_i8, i8);
test_missing!(test_missing_i16, i16);
test_missing!(test_missing_i32, i32);
test_missing!(test_missing_i64, i64);
test_missing!(test_missing_u8, u8);
test_missing!(test_missing_u16, u16);
test_missing!(test_missing_u32, u32);
test_missing!(test_missing_u64, u64);
test_missing!(test_missing_f32, f32);
test_missing!(test_missing_f64, f64);

macro_rules! test_invalid {
    ($name:ident, $ty:ty, $value:literal, $($err:tt)*) => {
        #[test]
        fn $name() {
            let mut source =
                MapSource::new(HashMap::from([("MY_VAR".to_owned(), $value.to_string())]));
            let mut de = serde_json::Deserializer::from_str("\"${MY_VAR}\"");

            let err: Result<$ty, _> = serde_vars::deserialize(&mut de, &mut source);
            insta::assert_debug_snapshot!(err.unwrap_err(), @$($err)*);
        }
    };
}

test_invalid!(
    test_invalid_string_bool,
    String,
    "true",
    r#"Error("invalid value: boolean `true`, expected variable `${MY_VAR}` to be a string", line: 0, column: 0)"#
);
test_invalid!(
    test_invalid_string_signed,
    String,
    "-123",
    r#"Error("invalid value: integer `-123`, expected variable `${MY_VAR}` to be a string", line: 0, column: 0)"#
);
test_invalid!(
    test_invalid_string_unsigned,
    String,
    "123",
    r#"Error("invalid value: integer `123`, expected variable `${MY_VAR}` to be a string", line: 0, column: 0)"#
);
test_invalid!(
    test_invalid_string_float,
    String,
    "-123.0",
    r#"Error("invalid value: floating point `-123.0`, expected variable `${MY_VAR}` to be a string", line: 0, column: 0)"#
);

#[derive(Debug, Deserialize)]
#[expect(dead_code, reason = "debug impl is used to assert")]
struct Complex<'a> {
    bool: bool,

    i8: i8,
    i16: i16,
    i32: i32,
    i64: i64,

    u8: u8,
    u16: u16,
    u32: u32,
    u64: u64,

    f32: f32,
    f64: f64,

    #[serde(borrow)]
    str: Cow<'a, str>,
    string: String,
    byte_buf: serde_bytes::ByteBuf,

    map_u32: BTreeMap<String, u32>,
    vec_u32: Vec<u32>,
    opt_u32: Option<u32>,

    nested: Nested<'a>,
    r#enum: EnumAny,
    new_type: NewType,
    tuple: (String, u32),
}

#[derive(Debug, Deserialize)]
#[expect(dead_code, reason = "debug impl is used to assert")]
struct Nested<'a> {
    str: Cow<'a, str>,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "v")]
#[expect(dead_code, reason = "debug impl is used to assert")]
enum EnumAny {
    VariantA { value: String },
    VariantB { value: u32 },
}

#[derive(Debug, Deserialize)]
#[expect(dead_code, reason = "debug impl is used to assert")]
struct NewType(String);

#[test]
fn test_complex_vars() {
    let mut source = MapSource::new(HashMap::from([
        ("bool".to_owned(), "true".to_owned()),
        ("i8".to_owned(), "-20".to_owned()),
        ("i16".to_owned(), "-200".to_owned()),
        ("i32".to_owned(), "42".to_owned()),
        ("i64".to_owned(), "1337".to_owned()),
        ("u8".to_owned(), "2".to_owned()),
        ("u16".to_owned(), "20".to_owned()),
        ("u32".to_owned(), "420".to_owned()),
        ("u64".to_owned(), "13370".to_owned()),
        ("f32".to_owned(), "1.0".to_owned()),
        ("f64".to_owned(), "2.0".to_owned()),
        ("str".to_owned(), "foo_str".to_owned()),
        ("string".to_owned(), "bar_string".to_owned()),
        ("byte_buf".to_owned(), "aaa".to_owned()),
        ("map_u32_0".to_owned(), "100".to_owned()),
        ("map_u32_1".to_owned(), "200".to_owned()),
        ("vec_u32_0".to_owned(), "3".to_owned()),
        ("vec_u32_1".to_owned(), "6".to_owned()),
        ("opt_u32".to_owned(), "42".to_owned()),
        ("nested_str".to_owned(), "meow".to_owned()),
        ("enum".to_owned(), "777".to_owned()),
        ("new_type".to_owned(), "not_so_new".to_owned()),
        ("tuple_string".to_owned(), "tuple".to_owned()),
        ("tuple_u32".to_owned(), "123".to_owned()),
    ]));
    let mut de = serde_json::Deserializer::from_str(
        r#"{
        "bool": "${bool}",

        "i8": "${i8}",
        "i16": "${i16}",
        "i32": "${i32}",
        "i64": "${i64}",

        "u8": "${u8}",
        "u16": "${u16}",
        "u32": "${u32}",
        "u64": "${u64}",

        "f32": "${f32}",
        "f64": "${f64}",

        "str": "${str}",
        "string": "${string}",
        "byte_buf": "${byte_buf}",

        "map_u32": {"${KEY}": "${map_u32_0}", "other": "${map_u32_1}", "another": 42},
        "vec_u32": [0, 1, 2, "${vec_u32_0}", 4, 5, "${vec_u32_1}"],
        "opt_u32": "${opt_u32}",

        "nested": {"str": "${nested_str}"},
        "enum": {"v": "VariantB", "value": "${enum}"},
        "new_type": "${new_type}",
        "tuple": ["${tuple_string}", "${tuple_u32}"]
    }"#,
    );

    let r: Complex = serde_vars::deserialize(&mut de, &mut source).unwrap();
    insta::assert_debug_snapshot!(r, @r###"
    Complex {
        bool: true,
        i8: -20,
        i16: -200,
        i32: 42,
        i64: 1337,
        u8: 2,
        u16: 20,
        u32: 420,
        u64: 13370,
        f32: 1.0,
        f64: 2.0,
        str: "foo_str",
        string: "bar_string",
        byte_buf: [
            97,
            97,
            97,
        ],
        map_u32: {
            "${KEY}": 100,
            "another": 42,
            "other": 200,
        },
        vec_u32: [
            0,
            1,
            2,
            3,
            4,
            5,
            6,
        ],
        opt_u32: Some(
            42,
        ),
        nested: Nested {
            str: "meow",
        },
        enum: VariantB {
            value: 777,
        },
        new_type: NewType(
            "not_so_new",
        ),
        tuple: (
            "tuple",
            123,
        ),
    }
    "###);
}

#[test]
fn test_complex_no_vars() {
    let mut source = MapSource::default();
    let mut de = serde_json::Deserializer::from_str(
        r#"{
        "bool": true,

        "i8": -20,
        "i16": -200,
        "i32": 42,
        "i64": 1337,

        "u8": 2,
        "u16": 20,
        "u32": 420,
        "u64": 13370,

        "f32": 1.0,
        "f64": 2.0,

        "str": "foo_str",
        "string": "bar_string",
        "byte_buf": "aaa",

        "map_u32": {"${KEY}": 100, "other": 200, "another": 42},
        "vec_u32": [0, 1, 2, 3, 4, 5, 6],
        "opt_u32": 42,

        "nested": {"str": "meow"},
        "enum": {"v": "VariantB", "value": 777},
        "new_type": "not_so_new",
        "tuple": ["tuple", 123]
    }"#,
    );

    let r: Complex = serde_vars::deserialize(&mut de, &mut source).unwrap();
    insta::assert_debug_snapshot!(r, @r###"
    Complex {
        bool: true,
        i8: -20,
        i16: -200,
        i32: 42,
        i64: 1337,
        u8: 2,
        u16: 20,
        u32: 420,
        u64: 13370,
        f32: 1.0,
        f64: 2.0,
        str: "foo_str",
        string: "bar_string",
        byte_buf: [
            97,
            97,
            97,
        ],
        map_u32: {
            "${KEY}": 100,
            "another": 42,
            "other": 200,
        },
        vec_u32: [
            0,
            1,
            2,
            3,
            4,
            5,
            6,
        ],
        opt_u32: Some(
            42,
        ),
        nested: Nested {
            str: "meow",
        },
        enum: VariantB {
            value: 777,
        },
        new_type: NewType(
            "not_so_new",
        ),
        tuple: (
            "tuple",
            123,
        ),
    }
    "###);
}

#[test]
fn test_enum_any_integer() {
    let mut source = MapSource::new(HashMap::from([("FOO".to_owned(), "123".to_owned())]));
    let mut de = serde_json::Deserializer::from_str(r#"{"v": "VariantB", "value": "${FOO}"}"#);

    let r: EnumAny = serde_vars::deserialize(&mut de, &mut source).unwrap();
    insta::assert_debug_snapshot!(r, @r###"
    VariantB {
        value: 123,
    }
    "###);
}

#[test]
fn test_enum_any_string() {
    let mut source = MapSource::new(HashMap::from([("FOO".to_owned(), "foobar".to_owned())]));
    let mut de = serde_json::Deserializer::from_str(r#"{"v": "VariantA", "value": "${FOO}"}"#);

    let r: EnumAny = serde_vars::deserialize(&mut de, &mut source).unwrap();
    insta::assert_debug_snapshot!(r, @r###"
    VariantA {
        value: "foobar",
    }
    "###);
}

#[test]
fn test_enum_any_string_num() {
    let mut source = MapSource::new(HashMap::from([("FOO".to_owned(), r#""123""#.to_owned())]));
    let mut de = serde_json::Deserializer::from_str(r#"{"v": "VariantA", "value": "${FOO}"}"#);

    let r: EnumAny = serde_vars::deserialize(&mut de, &mut source).unwrap();
    insta::assert_debug_snapshot!(r, @r###"
    VariantA {
        value: "123",
    }
    "###);
}