# Config Merger - Combine defaults, infrastructure config, and secrets let helpers = import "schemas/platform/common/helpers.ncl" in { # Merge configuration with overrides using deep merge # Preserves nested structures, doesn't lose partial updates merge_deep = fun base override => helpers.compose_config base override base, # Configuration pipeline: defaults → infrastructure → secrets pipeline = fun defaults infrastructure_config secrets => let step1 = compose_config defaults infrastructure_config {} in compose_config step1 secrets {}, # Compose configurations with proper precedence # Order: base (lowest) → layer1 → layer2 (highest) compose_config = fun base layer1 layer2 => let merge_two = fun a b => std.record.fold ( fun acc key value => if std.record.has_field key a && std.record.is_record (std.record.get key a) && std.record.is_record value then acc & { (key) = merge_two (std.record.get key a) value, } else acc & { (key) = value, } ) a b in let combined = merge_two base layer1 in merge_two combined layer2, # Extract environment-specific configuration by_environment = fun config environment => let base_config = config | std.record.get_path ["environments", "all"] in let env_config = config | std.record.get_path ["environments", environment] in compose_config base_config env_config {}, # Build final deployment configuration # Inputs: defaults, user overrides, secrets, deployment mode build = fun defaults user_config secrets deployment_mode environment => let base = compose_config defaults user_config {} in let with_secrets = compose_config base secrets {} in let mode_config = ( with_secrets | std.record.get_path ["deployment_modes", deployment_mode] | default {} ) in let final = compose_config with_secrets mode_config {} in { config = final, environment = environment, deployment_mode = deployment_mode, merged_from = ["defaults", "user_config", "secrets", "mode_specific"], }, # Validate merged configuration completeness validate_complete = fun config required_paths => let check_path = fun path => let try_get = std.record.get_path (std.string.split "." path) config in if try_get == null || (std.string.is_string try_get && std.string.is_empty try_get) then { path = path, valid = false } else { path = path, valid = true } in let results = std.array.map check_path required_paths in let invalid = std.array.filter (fun r => not r.valid) results in { valid = std.array.length invalid == 0, checked_paths = std.array.length required_paths, invalid_paths = invalid, }, # Extract configuration for specific component # Useful for creating component-specific config files extract_component = fun config component_name => let component = std.record.get component_name config in if std.record.is_record component then component else {}, # Flatten nested configuration for environment variables flatten = fun config prefix => let flatten_impl = fun obj current_prefix => std.record.fold ( fun acc key value => let new_key = $"($current_prefix)_($key)" in if std.record.is_record value && not (std.string.is_string value) then acc & (flatten_impl value new_key) else acc & { (new_key) = if std.string.is_string value then value else std.string.from_number value, } ) {} obj in flatten_impl config prefix, # Create configuration snapshot for audit/compliance snapshot = fun config metadata => { timestamp = metadata.timestamp, environment = metadata.environment, version = metadata.config_version, deployment_mode = metadata.deployment_mode, config_hash = ( config | std.json.stringify | std.crypto.sha256 ), merged_sources = metadata.sources, audit_log = metadata.audit_log, }, # Deep merge utility - recursive merge of nested records deep_merge = fun records => let merge_pair = fun a b => std.record.fold ( fun acc key value => if std.record.has_field key a then let existing = std.record.get key a in if std.record.is_record existing && std.record.is_record value then acc & { (key) = deep_merge [existing, value] } else acc & { (key) = value } else acc & { (key) = value } ) a b in std.array.fold_left merge_pair {} records, }