# Helper Functions for Workspace Configuration Composition # Provides utilities for deep merging, validation, and composition let std = import "std" in { # Deep merge records recursively # Unlike & operator (shallow merge), this preserves nested structure compose_config | doc "Recursively merge three records: base → mode_config → overrides" = fun base => fun mode_config => fun user_overrides => ( let merge_records = fun left => fun right => ( # For each field in right, either: # - If both are records: recursively merge # - Otherwise: right overwrites left std.record.to_array left | std.array.fold_left (fun acc => fun kv => let key = kv.key in let left_val = kv.value in let right_val = right | get key? in if right_val != null && left_val != null then if (std.typeof left_val) == "record" && (std.typeof right_val) == "record" then acc & {[key] = merge_records left_val right_val} else acc & {[key] = right_val} else if right_val != null then acc & {[key] = right_val} else acc & {[key] = left_val} ) right ) in merge_records (merge_records base mode_config) user_overrides ), # Validate required fields in config validate_required | doc "Ensure all required fields exist in config" = fun required_fields => fun config => ( let missing = ( required_fields | std.array.filter (fun field => (config | get field? | std.value.is_null) ) ) in if (missing | std.array.length) > 0 then std.contract.blame $"Missing required fields: " ++ (std.array.join ", " missing) else config ), # Validate no_use field and filter inactive servers filter_active_servers | doc "Filter out servers with not_use = true" = fun servers => ( servers | std.array.filter (fun server => (server | get not_use? | default false) == false ) ), # Extract provider type from config get_provider | doc "Extract provider name from workspace config" = fun config => ( config | get provider? | default "generic" ), # Map server to NixOS flake path to_flake_path | doc "Generate NixOS flake.nix directory path for server" = fun server => fun workspace_name => ( let hostname = server.hostname in let explicit_path = server | get flake_path? in if explicit_path != null && explicit_path != "" then explicit_path else $"workspaces/($workspace_name)/nixos/($hostname)" ), # Collect all unique taskservs across servers collect_taskservs | doc "Extract and deduplicate taskserv names from all servers" = fun servers => ( servers | std.array.fold_left (fun acc => fun server => let ts = server | get taskservs? | default [] in if (std.typeof ts) == "array" then ts | std.array.fold_left (fun acc2 => fun taskserv => let name = taskserv | get name? | default "unknown" in if (acc2 | std.array.contains name) then acc2 else acc2 | std.array.push name ) acc else acc ) [] ), # Create minimal server stub for testing minimal_server | doc "Generate minimal valid server record for testing" = fun hostname => { hostname = hostname, title = $"Server ($hostname)", plan = "1xCPU-1GB", not_use = false, extra_hostnames = [hostname], labels = "", taskservs = [], }, }