Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

restricting eval body to subset of Expression variants #323

Open
iamjoncannon opened this issue Mar 24, 2024 · 2 comments
Open

restricting eval body to subset of Expression variants #323

iamjoncannon opened this issue Mar 24, 2024 · 2 comments
Labels
question Further information is requested

Comments

@iamjoncannon
Copy link

Hi Martin, first thanks so much for this library!

I have a question--

I would like to restrict a property on an HCL body to a subset of hcl::Expression variants during hcl::from_body(body) schema validation.

The body schema is:

pub struct CallBlock {
    base_url: hcl::Expression,
    path: Option<hcl::Expression>,
    headers: Option<hcl::Expression>,
    body: Option<HclObject>,
    after: Option<Vec<HclObject>>,
    outputs: Option<Vec<hcl::Traversal>>,
}

I can encapsulate the Expression::Object variant--

pub type HclObject = hcl::Object<hcl::ObjectKey, hcl::Expression>;

However, rather than the general hcl::Expression, I want to restrict headers to be either--

pub enum ObjectOrVariable {
    Object(HclObject),
    Variable(hcl::Variable),
}

(e.g. user HTTP headers can either be an object with a key or a general expression, or a variable. If the user screws up the header value its their problem, but we validate that its not a )

When I use this ObjectOrVariable enum as the header schema-- Option<Vec<ObjectOrVariable>> -- I get this error with correct HCL-- "unknown variant 'my-key', expected 'Object' or 'Variable'", where my-key was the header key passed.

Thanks in advance for any help, and apologies if I've missed something obvious.

@martinohmann
Copy link
Owner

Hi @iamjoncannon, could you provide me with a minimal reproducer for the issue? With the context you provided so far it seems like this could solved using the #[serde(untagged)] attribute on the ObjectOrVariable enum: https://serde.rs/enum-representations.html

@martinohmann martinohmann added the question Further information is requested label Mar 26, 2024
@iamjoncannon
Copy link
Author

iamjoncannon commented Apr 6, 2024

Hi @martinohmann, thanks for following up!

I wrote some unit tests to describe my ideal behavior to evaluate blocks during an initial parsing.

My suspicion is that I'm unable to define the types of these variants properly in my enums-- e.g. defining a list as a vec instead of the hcl::Expression::Array variant. Granted I'm not a Rust expert, but I wrestled with this enough to conclude that I might not be able to do this because the hcl::Expression variant types are private?

Being able to define and restrict user inputs for my app would be really, really helpful, so again thank you so much for any time you can spend on my issue.

// [dependencies]
// hcl-rs = "0.16.7"
// serde_json = "1.0.114"
// serde = "1.0.197"

use serde::{de::DeserializeOwned, Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
pub enum HeaderKey {
    Identifier(hcl::Identifier),
    Traversal(hcl::Traversal),
}

pub type HeaderObject = hcl::Object<hcl::ObjectKey, HeaderKey>;

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
pub enum HeaderObjectOrVariable {
    Object(HeaderObject),
    Traversal(hcl::Traversal),
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]
pub enum ValidHeaderCases {
    Traversal(hcl::Traversal),
    String(String),
    Array(Vec<HeaderObjectOrVariable>),
}

#[derive(Deserialize, Serialize, Debug)]
pub struct IdealRestrictedCallBlock {
    pub headers: ValidHeaderCases,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct UnrestrictedCallBlock {
    pub headers: hcl::Expression,
}

pub fn evaluate<T: DeserializeOwned>(input: &str) -> Result<T, hcl::Error> {
    let call_block_body: hcl::Body = hcl::from_str(input).unwrap();
    let block = call_block_body.into_blocks().next().unwrap().body;
    let call_block_res: Result<T, hcl::Error> = hcl::from_body(block);
    call_block_res
}

fn main() {}

#[cfg(test)]
mod tests {
    use std::fmt::Debug;

    use crate::{evaluate, IdealRestrictedCallBlock, UnrestrictedCallBlock, ValidHeaderCases};

    #[test]
    fn test_trivial_string() {
        // this is just to test untagged solution with a trivial case
        let test_block: &str = r#"
            get "example" {
                headers = "trivial case"
            }
        "#;

        run_unrestricted_and_ideal(test_block, "test_trivial_string");
    }

    #[test]
    fn test_header_block_as_variable() {
        let test_block: &str = r#"
            get "example" {
                headers = my.headers.var
            }
        "#;

        reporter(
            "test_header_block_as_variable UnrestrictedCallBlock",
            &evaluate::<UnrestrictedCallBlock>(test_block),
        );

        let res = evaluate::<IdealRestrictedCallBlock>(test_block);

        match res {
            Ok(res) => match res.headers {
                ValidHeaderCases::Traversal(_var) => assert!(true),
                ValidHeaderCases::String(str) => {
                    // the variable is evaluated as a string template
                    println!("variable evaluated as string: {str}");
                    assert!(false)
                }
                _ => assert!(false),
            },
            Err(err) => {
                eprintln!("test_header_block_as_variable err {err:?}");
                assert!(false);
            }
        }
    }

    #[test]
    fn test_headers_defined_as_variable() {
        let test_block: &str = r#"
            get "example" {
                headers = [
                    my.header.variable
                ]
            }
        "#;

        run_unrestricted_and_ideal(test_block, "test_headers_defined_as_variable");
    }

    #[test]
    fn test_headers_defined_as_string() {
        let test_block: &str = r#"
            get "example" {
                headers = [
                    { "authorization" : "let me in" }
                ]
            }
        "#;

        run_unrestricted_and_ideal(test_block, "test_headers_defined_as_string");
    }

    #[test]
    fn test_individual_headers_as_variables() {
        let test_block: &str = r#"
            get "example" {
                headers = [
                    my.header.variable
                ]
            }
        "#;
        run_unrestricted_and_ideal(test_block, "test_individual_headers_as_variables");
    }

    #[test]
    fn test_individual_headers_values_as_variables() {
        let test_block: &str = r#"
            get "example" {
                headers = [
                    { "authorization" : my.auth.var }
                ]
            }
        "#;
        run_unrestricted_and_ideal(test_block, "test_individual_headers_values_as_variables");
    }

    fn run_unrestricted_and_ideal(test_block: &str, test_name: &str) {
        reporter(
            &format!("{} UnrestrictedCallBlock", test_name),
            &evaluate::<UnrestrictedCallBlock>(test_block),
        );

        reporter(
            &format!("{} IdealRestrictedCallBlock", test_name),
            &evaluate::<IdealRestrictedCallBlock>(test_block),
        );
    }

    fn reporter<T: Debug>(name: &str, result: &Result<T, hcl::Error>) {
        match result {
            Ok(res) => {
                println!("result {name}: {res:?}");
                assert!(true)
            }
            Err(err) => {
                println!("err {name} {err:?}");
                assert!(false)
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants