hcl-rs 0.19.7

HCL parser and formatter for rust with serde support
Documentation
mod common;

use common::assert_serialize;
use hcl::{
    expr::{BinaryOp, BinaryOperator, Conditional, Expression, Traversal, Variable},
    ser::{block, doubly_labeled_block, labeled_block},
    Map,
};
use indexmap::indexmap;
use indoc::indoc;
use serde::Serialize;

#[test]
fn custom_block() {
    #[derive(Serialize)]
    struct Config {
        #[serde(serialize_with = "block")]
        block: Map<&'static str, &'static str>,
    }

    let config = Config {
        block: indexmap! { "a" => "b", "c" => "d" },
    };

    let expected = indoc! {r#"
        block {
          a = "b"
          c = "d"
        }
    "#};

    assert_serialize(config, expected);
}

#[test]
fn custom_labeled_block() {
    #[derive(Serialize)]
    struct Config {
        #[serde(serialize_with = "labeled_block")]
        block: Map<&'static str, Map<&'static str, &'static str>>,
    }

    let config = Config {
        block: indexmap! {
            "one" => indexmap! { "a" => "b" },
            "two" => indexmap! { "c" => "d" },
        },
    };

    let expected = indoc! {r#"
        block "one" {
          a = "b"
        }

        block "two" {
          c = "d"
        }
    "#};

    assert_serialize(config, expected);
}

#[test]
fn custom_doubly_labeled_block() {
    #[derive(Serialize)]
    struct Config {
        #[serde(serialize_with = "doubly_labeled_block")]
        block: Map<&'static str, Map<&'static str, Map<&'static str, &'static str>>>,
    }

    let config = Config {
        block: indexmap! {
            "foo" => indexmap! {
                "one" => indexmap! { "a" => "b" },
                "two" => indexmap! { "c" => "d" },
            },
        },
    };

    let expected = indoc! {r#"
        block "foo" "one" {
          a = "b"
        }

        block "foo" "two" {
          c = "d"
        }
    "#};

    assert_serialize(config, expected);
}

#[test]
fn custom_block_structs() {
    #[derive(Serialize)]
    struct A {
        b: &'static str,
        #[serde(serialize_with = "block")]
        d: Vec<D>,
    }

    #[derive(Serialize)]
    struct B {
        #[serde(serialize_with = "labeled_block")]
        c1: C,
        #[serde(serialize_with = "labeled_block")]
        c2: Vec<C>,
    }

    #[derive(Serialize)]
    struct C {
        d: D,
    }

    #[derive(Serialize)]
    struct D {
        e: &'static str,
    }

    #[derive(Serialize)]
    struct Config {
        #[serde(serialize_with = "block")]
        a: A,
        #[serde(serialize_with = "labeled_block")]
        b: B,
    }

    let config = Config {
        a: A {
            b: "c",
            d: vec![D { e: "f1" }, D { e: "f2" }],
        },
        b: B {
            c1: C { d: D { e: "f3" } },
            c2: vec![C { d: D { e: "f4" } }, C { d: D { e: "f5" } }],
        },
    };

    let expected = indoc! {r#"
        a {
          b = "c"

          d {
            e = "f1"
          }

          d {
            e = "f2"
          }
        }

        b "c1" "d" {
          e = "f3"
        }

        b "c2" "d" {
          e = "f4"
        }

        b "c2" "d" {
          e = "f5"
        }
    "#};

    assert_serialize(config, expected);
}

#[test]
fn custom_terraform_blocks() {
    type Body = Map<&'static str, Value>;
    type LabeledBlock = Map<&'static str, Body>;

    #[derive(Serialize)]
    #[serde(untagged)]
    enum Value {
        Expression(Expression),
        #[serde(serialize_with = "block")]
        Block(Vec<Body>),
        #[serde(serialize_with = "labeled_block")]
        LabeledBlock(LabeledBlock),
    }

    #[derive(Serialize)]
    struct Config {
        #[serde(rename = "resource", serialize_with = "doubly_labeled_block")]
        resources: Map<&'static str, LabeledBlock>,
    }

    let config = Config {
        resources: indexmap! {
            "aws_s3_bucket" => indexmap! {
                "mybucket" => indexmap! {
                    "bucket" => Value::Expression("mybucket".into()),
                    "force_destroy" => Value::Expression(true.into()),
                    "server_side_encryption_configuration" => Value::Block(vec![
                        indexmap! {
                            "rule" => Value::Block(vec![
                                indexmap! {
                                    "apply_server_side_encryption_by_default" => Value::Block(vec![
                                        indexmap! {
                                            "kms_master_key_id" => Value::Expression(
                                                Traversal::builder(Variable::new("aws_kms_key").unwrap())
                                                    .attr("mykey")
                                                    .attr("arn")
                                                    .build()
                                                    .into(),
                                            ),
                                            "sse_algorithm" => Value::Expression("aws:kms".into()),
                                        }
                                    ]),
                                },
                            ]),
                        },
                    ]),
                    "dynamic" => Value::LabeledBlock(indexmap! {
                        "kubernetes_network_config" => indexmap! {
                            "for_each" => Value::Expression(
                                Conditional::new(
                                    BinaryOp::new(
                                        Traversal::builder(Variable::new("var").unwrap())
                                            .attr("service_ipv4_cidr")
                                            .build(),
                                        BinaryOperator::Eq,
                                        Expression::Null,
                                    ),
                                    Expression::Array(vec![]),
                                    Expression::from_iter([
                                        Traversal::builder(Variable::new("var").unwrap())
                                            .attr("service_ipv4_cidr")
                                            .build(),
                                    ]),
                                )
                                .into(),
                            ),
                            "content" => Value::Block(vec![
                                indexmap! {
                                    "service_ipv4_cidr" => Value::Expression(
                                        Traversal::builder(Variable::new("kubernetes_network_config").unwrap())
                                            .attr("value")
                                            .build()
                                            .into(),
                                    ),
                                },
                            ]),
                        },
                    }),
                    "tags" => Value::Expression(
                        Expression::from_iter([("application", "myapp"), ("environment", "dev")]),
                    ),
                },
            },
        },
    };

    let expected = indoc! {r#"
        resource "aws_s3_bucket" "mybucket" {
          bucket = "mybucket"
          force_destroy = true

          server_side_encryption_configuration {
            rule {
              apply_server_side_encryption_by_default {
                kms_master_key_id = aws_kms_key.mykey.arn
                sse_algorithm = "aws:kms"
              }
            }
          }

          dynamic "kubernetes_network_config" {
            for_each = var.service_ipv4_cidr == null ? [] : [var.service_ipv4_cidr]

            content {
              service_ipv4_cidr = kubernetes_network_config.value
            }
          }

          tags = {
            "application" = "myapp"
            "environment" = "dev"
          }
        }
    "#};

    assert_serialize(config, expected);
}