# Configuration interpolation - Substitutes variables and patterns in config # NUSHELL 0.109 COMPLIANT - Using reduce --fold (Rule 3), do-complete (Rule 5), each (Rule 8) # helpers/environment star-import was dead — dropped (ADR-025 Phase 3 Layer 2). # Main interpolation entry point - interpolates all patterns in configuration export def interpolate-config [config: record]: nothing -> record { let base_result = (do { $config | get paths.base } | complete) let base_path = if $base_result.exit_code == 0 { $base_result.stdout } else { "" } if ($base_path | is-not-empty) { # Convert config to JSON, apply all interpolations, convert back let json_str = ($config | to json) let interpolated_json = (interpolate-all-patterns $json_str $config) ($interpolated_json | from json) } else { $config } } # Interpolate a single string value with configuration context export def interpolate-string [text: string, config: record]: nothing -> string { # Basic interpolation for {{paths.base}} pattern if ($text | str contains "{{paths.base}}") { let base_path = (get-config-value $config "paths.base" "") ($text | str replace --all "{{paths.base}}" $base_path) } else { $text } } # Get a nested configuration value using dot notation export def get-config-value [config: record, path: string, default_value: any]: nothing -> any { let path_parts = ($path | split row ".") # Navigate to the value using the path let result = ($path_parts | reduce --fold $config {|part, current| let access_result = (do { $current | get $part } | complete) if $access_result.exit_code == 0 { $access_result.stdout } else { null } }) if ($result | is-empty) { $default_value } else { $result } } # Apply all interpolation patterns to JSON string (Rule 3: using reduce --fold for sequence) def interpolate-all-patterns [json_str: string, config: record]: nothing -> string { # Apply each interpolation pattern in sequence using reduce --fold # This ensures patterns are applied in order and mutations are immutable let patterns = [ {name: "paths.base", fn: {|s, c| interpolate-base-path $s ($c | get paths.base | default "") }} {name: "env", fn: {|s, c| interpolate-env-variables $s}} {name: "datetime", fn: {|s, c| interpolate-datetime $s}} {name: "git", fn: {|s, c| interpolate-git-info $s}} {name: "sops", fn: {|s, c| interpolate-sops-config $s $c}} {name: "providers", fn: {|s, c| interpolate-provider-refs $s $c}} {name: "advanced", fn: {|s, c| interpolate-advanced-features $s $c}} ] $patterns | reduce --fold $json_str {|pattern, result| do { ($pattern.fn | call $result $config) } | complete | if $in.exit_code == 0 { $in.stdout } else { $result } } } # Interpolate base path pattern def interpolate-base-path [text: string, base_path: string]: nothing -> string { if ($text | str contains "{{paths.base}}") { ($text | str replace --all "{{paths.base}}" $base_path) } else { $text } } # Interpolate environment variables with security validation (Rule 8: using reduce --fold) def interpolate-env-variables [text: string]: nothing -> string { # Safe environment variables list (security allowlist) let safe_env_vars = [ "HOME" "USER" "HOSTNAME" "PWD" "SHELL" "PROVISIONING" "PROVISIONING_WORKSPACE_PATH" "PROVISIONING_INFRA_PATH" "PROVISIONING_SOPS" "PROVISIONING_KAGE" ] # Apply each env var substitution using reduce --fold (Rule 3: no mutable variables) let with_env = ($safe_env_vars | reduce --fold $text {|env_var, result| let pattern = $"\\{\\{env\\.($env_var)\\}\\}" let env_result = (do { $env | get $env_var } | complete) let env_value = if $env_result.exit_code == 0 { $env_result.stdout } else { "" } if ($env_value | is-not-empty) { ($result | str replace --regex $pattern $env_value) } else { $result } }) # Handle conditional environment variables interpolate-conditional-env $with_env } # Handle conditional environment variable interpolation def interpolate-conditional-env [text: string]: nothing -> string { let conditionals = [ {pattern: "{{env.HOME || \"/tmp\"}}", value: {|| ($env.HOME? | default "/tmp")}} {pattern: "{{env.USER || \"unknown\"}}", value: {|| ($env.USER? | default "unknown")}} ] $conditionals | reduce --fold $text {|cond, result| if ($result | str contains $cond.pattern) { let value = (($cond.value | call)) ($result | str replace --all $cond.pattern $value) } else { $result } } } # Interpolate date and time values def interpolate-datetime [text: string]: nothing -> string { let current_date = (date now | format date "%Y-%m-%d") let current_timestamp = (date now | format date "%s") let iso_timestamp = (date now | format date "%Y-%m-%dT%H:%M:%SZ") let with_date = ($text | str replace --all "{{now.date}}" $current_date) let with_timestamp = ($with_date | str replace --all "{{now.timestamp}}" $current_timestamp) ($with_timestamp | str replace --all "{{now.iso}}" $iso_timestamp) } # Interpolate git information (defaults to "unknown" to avoid hanging) def interpolate-git-info [text: string]: nothing -> string { let patterns = [ {pattern: "{{git.branch}}", value: "unknown"} {pattern: "{{git.commit}}", value: "unknown"} {pattern: "{{git.origin}}", value: "unknown"} ] $patterns | reduce --fold $text {|p, result| ($result | str replace --all $p.pattern $p.value) } } # Interpolate SOPS configuration references def interpolate-sops-config [text: string, config: record]: nothing -> string { let sops_key_result = (do { $config | get sops.age_key_file } | complete) let sops_key_file = if $sops_key_result.exit_code == 0 { $sops_key_result.stdout } else { "" } let with_key = if ($sops_key_file | is-not-empty) { ($text | str replace --all "{{sops.key_file}}" $sops_key_file) } else { $text } let sops_cfg_result = (do { $config | get sops.config_path } | complete) let sops_config_path = if $sops_cfg_result.exit_code == 0 { $sops_cfg_result.stdout } else { "" } if ($sops_config_path | is-not-empty) { ($with_key | str replace --all "{{sops.config_path}}" $sops_config_path) } else { $with_key } } # Interpolate cross-section provider references def interpolate-provider-refs [text: string, config: record]: nothing -> string { let providers_to_check = [ {pattern: "{{providers.aws.region}}", path: "providers.aws.region"} {pattern: "{{providers.default}}", path: "providers.default"} {pattern: "{{providers.upcloud.zone}}", path: "providers.upcloud.zone"} ] $providers_to_check | reduce --fold $text {|prov, result| let value_result = (do { let parts = ($prov.path | split row ".") if ($parts | length) == 2 { $config | get ($parts | first) | get ($parts | last) } else { $config | get ($parts | first) | get ($parts | get 1) | get ($parts | last) } } | complete) let value = if $value_result.exit_code == 0 { $value_result.stdout } else { "" } if ($value | is-not-empty) { ($result | str replace --all $prov.pattern $value) } else { $result } } } # Interpolate advanced features (function calls, environment-aware paths) def interpolate-advanced-features [text: string, config: record]: nothing -> string { let base_path_result = (do { $config | get paths.base } | complete) let base_path = if $base_path_result.exit_code == 0 { $base_path_result.stdout } else { "" } let with_path_join = if ($text | str contains "{{path.join(paths.base") { # Simple regex-based path.join replacement ($text | str replace --regex "\\{\\{path\\.join\\(paths\\.base,\\s*\"([^\"]+)\"\\)\\}\\}" $"($base_path)/$1") } else { $text } # Replace environment-aware paths let current_env_result = (do { $config | get current_environment } | complete) let current_env = if $current_env_result.exit_code == 0 { $current_env_result.stdout } else { "dev" } ($with_path_join | str replace --all "{{paths.base.\${env}}}" $"{{paths.base}}.($current_env)") } # Validate interpolation patterns and detect issues export def validate-interpolation [ config: record --detailed = false ]: nothing -> record { let json_str = ($config | to json) # Check for unresolved interpolation patterns let unresolved = (detect-unresolved-patterns $json_str) let unresolved_errors = if ($unresolved | length) > 0 { [{ type: "unresolved_interpolation", severity: "error", patterns: $unresolved, message: $"Unresolved interpolation patterns found: ($unresolved | str join ', ')" }] } else { [] } # Check for circular dependencies let circular = (detect-circular-dependencies $json_str) let circular_errors = if ($circular | length) > 0 { [{ type: "circular_dependency", severity: "error", dependencies: $circular, message: $"Circular interpolation dependencies detected" }] } else { [] } # Check for unsafe environment variable access let unsafe = (detect-unsafe-env-patterns $json_str) let unsafe_warnings = if ($unsafe | length) > 0 { [{ type: "unsafe_env_access", severity: "warning", variables: $unsafe, message: $"Potentially unsafe environment variable access" }] } else { [] } # Validate git context if needed let git_warnings = if ($json_str | str contains "{{git.") { let git_check = (do { ^git rev-parse --git-dir err> /dev/null } | complete) if ($git_check.exit_code != 0) { [{ type: "git_context", severity: "warning", message: "Git interpolation patterns found but not in a git repository" }] } else { [] } } else { [] } # Combine all results let all_errors = ($unresolved_errors | append $circular_errors) let all_warnings = ($unsafe_warnings | append $git_warnings) if (not $detailed) and (($all_errors | length) > 0) { let error_messages = ($all_errors | each { |err| $err.message }) error make {msg: ($error_messages | str join "; ")} } { valid: (($all_errors | length) == 0), errors: $all_errors, warnings: $all_warnings, summary: { total_errors: ($all_errors | length), total_warnings: ($all_warnings | length), interpolation_patterns_detected: (count-interpolation-patterns $json_str) } } } # Detect unresolved interpolation patterns def detect-unresolved-patterns [text: string]: nothing -> list { # Known patterns that should be handled let known_prefixes = ["paths" "env" "now" "git" "sops" "providers" "path"] # Extract all {{...}} patterns and check if they match known types let all_patterns = (do { $text | str replace --regex "\\{\\{([^}]+)\\}\\}" "$1" } | complete) if ($all_patterns.exit_code != 0) { return [] } # Check for unknown patterns (simplified detection) if ($text | str contains "{{unknown.") { ["unknown.*"] } else { [] } } # Detect circular interpolation dependencies def detect-circular-dependencies [text: string]: nothing -> list { if (($text | str contains "{{paths.base}}") and ($text | str contains "paths.base.*{{paths.base}}")) { ["paths.base -> paths.base"] } else { [] } } # Detect unsafe environment variable patterns def detect-unsafe-env-patterns [text: string]: nothing -> list { let dangerous_patterns = ["PATH" "LD_LIBRARY_PATH" "PYTHONPATH" "SHELL" "PS1"] # Use reduce --fold to find all unsafe patterns (Rule 3) $dangerous_patterns | reduce --fold [] {|pattern, unsafe_list| if ($text | str contains $"{{env.($pattern)}}") { ($unsafe_list | append $pattern) } else { $unsafe_list } } } # Count interpolation patterns in text for metrics def count-interpolation-patterns [text: string]: nothing -> number { # Count {{...}} occurrences ($text | str replace --all --regex "\\{\\{[^}]+\\}\\}" "" | length) - ($text | length) | math abs | ($text | length) - . | . / 4 # Approximate based on {{ }} length }