hocon-parser 1.6.1

Full Lightbend HOCON specification-compliant parser for Rust
Documentation
//! Cross-impl regression tests for go.hocon#128 — include-child
//! `${?ENV_VAR}` env-with-default pattern silently erases the prior
//! duplicate-key assignment when the env var is unset.
//!
//! Pattern under test (canonical Lightbend reference.conf idiom):
//!
//! ```hocon
//! registry {
//!   instance-id = "localhost"
//!   instance-id = ${?REGISTRY_INSTANCE_ID}
//! }
//! ```
//!
//! Spec basis: S7.1 (later non-object overrides earlier) +
//! S13.2/S13.11 (optional substitution undefined → field not created,
//! i.e. the second assignment "disappears", leaving the prior) +
//! S14b.2 (included keys merge per duplicate-key rules — include
//! boundary is invisible to the merge semantics).
//!
//! go.hocon v1.4.1–v1.5.2 lost the include-child's `priorValues` across
//! a separate lenient-resolve pass; rs.hocon merges include content
//! into the parent's tree at structure-build time
//! (`deep_merge_res_obj_into` preserves both fields and `prior_values`,
//! see `src/resolver/utils.rs`), so a single substitution-resolve pass
//! over the merged tree never strips the prior. These tests pin that
//! behaviour so a future refactor to a multi-pass shape can't silently
//! regress.
//!
//! Hermeticity: env is injected via `hocon::parse_with_env` (string
//! entry point, for the include "file" tests that build their input as
//! a string referencing a tempdir path), `Parser::parse_with_env` (for
//! the include package(...) tests that need a registry), and
//! `Parser::parse_with_options` (for the deferred-resolve path
//! exercised against an include package(...) source). `std::env` is
//! never read or mutated. Matches the cross-impl convention used by
//! `ts.hocon` (`parse(input, { env })`). As a result these tests are
//! safe to run in parallel — no shared mutable process state.
//!
//! Run: `cargo test --features include-package --test issue128_include_env_fallback`

#![cfg(feature = "include-package")]

use hocon::{ParseOptions, Parser, ResolveOptions};
use std::collections::HashMap;
use tempfile::tempdir;

const CHILD_DEFAULT_PLUS_OPTIONAL_FILE_UNSET: &str = r#"
registry {
  instance-id = "localhost"
  instance-id = ${?GH128_RS_FILE_UNSET}
}
"#;

const CHILD_DEFAULT_PLUS_OPTIONAL_FILE_SET: &str = r#"
registry {
  instance-id = "localhost"
  instance-id = ${?GH128_RS_FILE_SET}
}
"#;

const CHILD_DEFAULT_PLUS_OPTIONAL_PKG_UNSET: &str = r#"
registry {
  instance-id = "localhost"
  instance-id = ${?GH128_RS_PKG_UNSET}
}
"#;

const CHILD_DEFAULT_PLUS_OPTIONAL_PKG_SET: &str = r#"
registry {
  instance-id = "localhost"
  instance-id = ${?GH128_RS_PKG_SET}
}
"#;

const CHILD_DEFAULT_PLUS_OPTIONAL_PKG_DEFERRED: &str = r#"
registry {
  instance-id = "localhost"
  instance-id = ${?GH128_RS_PKG_DEFERRED}
}
"#;

// Deferred-resolve coverage uses `Parser::parse_with_options(...
// ParseOptions::defaults().with_resolve_substitutions(false).with_env(...))`,
// which threads the per-Parser package registry through phase 1 and
// returns an unresolved `Config`. Includes are already inlined at phase 1
// so the registry is no longer needed when `Config::resolve` runs phase 2.
// This pins go.hocon#128 (parity with the go.hocon
// TestIncludePackage_OptionalEnvFallback_DeferredPath_PreservesPriorDefault
// regression).

#[test]
fn issue128_include_file_env_unset_preserves_prior_default() {
    let dir = tempdir().unwrap();
    let dir_str = dir.path().display().to_string().replace('\\', "/");
    std::fs::write(
        dir.path().join("child.conf"),
        CHILD_DEFAULT_PLUS_OPTIONAL_FILE_UNSET,
    )
    .unwrap();
    let input = format!("include \"{}/child.conf\"\n", dir_str);
    let env: HashMap<String, String> = HashMap::new();
    let cfg = hocon::parse_with_env(&input, &env).expect("parse must succeed");
    let got = cfg
        .get_string("registry.instance-id")
        .expect("registry.instance-id missing — prior default must remain when ${?ENV} is unset");
    assert_eq!(got, "localhost");
}

#[test]
fn issue128_include_file_env_set_applies_env_value() {
    let dir = tempdir().unwrap();
    let dir_str = dir.path().display().to_string().replace('\\', "/");
    std::fs::write(
        dir.path().join("child.conf"),
        CHILD_DEFAULT_PLUS_OPTIONAL_FILE_SET,
    )
    .unwrap();
    let input = format!("include \"{}/child.conf\"\n", dir_str);
    let mut env: HashMap<String, String> = HashMap::new();
    env.insert("GH128_RS_FILE_SET".into(), "from-env".into());
    let cfg = hocon::parse_with_env(&input, &env).expect("parse must succeed");
    let got = cfg
        .get_string("registry.instance-id")
        .expect("registry.instance-id missing");
    assert_eq!(got, "from-env");
}

#[test]
fn issue128_include_package_env_unset_preserves_prior_default() {
    let parser = Parser::new().register_package(
        "github.com/o3co/rs.hocon/test/issue128-unset",
        "reference.conf",
        CHILD_DEFAULT_PLUS_OPTIONAL_PKG_UNSET,
    );
    let env: HashMap<String, String> = HashMap::new();
    let cfg = parser
        .parse_with_env(
            r#"include package("github.com/o3co/rs.hocon/test/issue128-unset", "reference.conf")"#,
            &env,
        )
        .expect("parse must succeed");
    let got = cfg
        .get_string("registry.instance-id")
        .expect("registry.instance-id missing — prior default must remain when ${?ENV} is unset");
    assert_eq!(got, "localhost");
}

#[test]
fn issue128_include_package_env_set_applies_env_value() {
    let parser = Parser::new().register_package(
        "github.com/o3co/rs.hocon/test/issue128-set",
        "reference.conf",
        CHILD_DEFAULT_PLUS_OPTIONAL_PKG_SET,
    );
    let mut env: HashMap<String, String> = HashMap::new();
    env.insert("GH128_RS_PKG_SET".into(), "from-pkg-env".into());
    let cfg = parser
        .parse_with_env(
            r#"include package("github.com/o3co/rs.hocon/test/issue128-set", "reference.conf")"#,
            &env,
        )
        .expect("parse must succeed");
    let got = cfg
        .get_string("registry.instance-id")
        .expect("registry.instance-id missing");
    assert_eq!(got, "from-pkg-env");
}

#[test]
fn issue128_include_package_deferred_env_unset_preserves_prior_default() {
    let parser = Parser::new().register_package(
        "github.com/o3co/rs.hocon/test/issue128-deferred",
        "reference.conf",
        CHILD_DEFAULT_PLUS_OPTIONAL_PKG_DEFERRED,
    );
    let env: HashMap<String, String> = HashMap::new();
    let opts = ParseOptions::defaults()
        .with_resolve_substitutions(false)
        .with_env(env);
    let unresolved = parser
        .parse_with_options(
            r#"include package("github.com/o3co/rs.hocon/test/issue128-deferred", "reference.conf")"#,
            opts,
        )
        .expect("parse must succeed");
    let resolved = unresolved
        .resolve(ResolveOptions::defaults().with_use_system_environment(false))
        .expect("Resolve must succeed");
    let got = resolved
        .get_string("registry.instance-id")
        .expect("registry.instance-id missing — prior default must remain when ${?ENV} is unset");
    assert_eq!(got, "localhost");
}