martinohmann / hcl-rs

HCL parsing and encoding libraries for rust with serde support
Apache License 2.0
124 stars 13 forks source link

unknown variant `${identifier.attr.subattr}`, expected one of `Null`, `Bool`, `Number`... #233

Open vlad-ivanov-name opened 1 year ago

vlad-ivanov-name commented 1 year ago

I ran into a seemingly complex edge case where deserialisation fails when an untagged enum is used together with deserialize_with. In the following example:

#![allow(unused)]

use serde::Deserialize;
use serde::Deserializer;
use serde_json;
use anyhow;
use hcl;

pub fn de_transform<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
    where
        D: Deserializer<'de>,
{
    let expressions: Result<Vec<hcl::Expression>, _> = Deserialize::deserialize(deserializer);

    if let Err(ref e) = expressions {
        eprintln!("{:?}", e)
    }

    Ok(expressions?.iter().map(|expr| format!("Parsed expression: {}", expr.to_string())).collect::<Vec<_>>())
}

#[derive(Deserialize, Debug, Clone)]
struct A {
    #[serde(deserialize_with = "de_transform")]
    pub value_a: Vec<String>
}

#[derive(Deserialize, Debug, Clone)]
struct B {
    #[serde(deserialize_with = "de_transform")]
    pub value_b: Vec<String>
}

#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum Test {
    A(A),
    B(B),
}

#[derive(Deserialize, Debug, Clone)]
struct AX {
    pub value_a: Vec<String>
}

#[derive(Deserialize, Debug, Clone)]
struct BX {
    pub value_b: Vec<String>
}

#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum TestX {
    AX(AX),
    BX(BX),
}

fn main() -> anyhow::Result<()> {
    let data = r#"
        value_a = [ident.prop1.subprop1, ident.prop2.subprop2]
    "#;

    let get_body = || {
        let body: Result<hcl::Body, _> = hcl::from_str(&data);
        body
    };

    let body_1 = get_body();
    // Will print body as expected
    println!("{:?}", body_1);

    let de_result_1: Result<A, _> = hcl::from_body(body_1?);
    // Will print struct as expected
    println!("{:?}", de_result_1);

    let de_result_2: Result<TestX, _> = hcl::from_body(get_body()?);
    // Will match enum case AX as expected
    println!("{:?}", de_result_2);

    // Will fail
    let de_result_3: Result<Test, _> = hcl::from_body(get_body()?);
    println!("{:?}", de_result_3);

    Ok(())
}

de_transform function will fail with:

Message { msg: "unknown variant `${ident.prop1.subprop1}`, expected one of `Null`, `Bool`, `Number`, `String`, `Array`, `Object`, `TemplateExpr`, `Variable`, `Traversal`, `FuncCall`, `Parenthesis`, `Conditional`, `Operation`, `ForExpr`, `Raw`", location: None }

I haven't been able to find a workaround unfortunately

martinohmann commented 1 year ago

Thanks for the detailed reproducer!

Since deserializing A by itself works, but Test doesn't it seems to be related to the enum handling within the hcl deserializer when custom deserialize logic is injected as in your case.

There are some hardcoded assumptions in there right now to ensure correct roundtripping of the hcl-rs types themselves. The error message surfaces that. This needs to be adjusted to handle more "foreign" type layouts passed in.

I'd be happy to get this fixed, but unfortunately I'm currently busy with some personal obligations over then next 2 or 3 months, so I'm not sure I can get my hands (and head) on it anytime soon.

But if you feel like digging deeper into it I'm happy to receive a PR in the meantime 😉