provisioning/schemas/platform/common/helpers.ncl

113 lines
3.7 KiB
Text

# Configuration Composition Helpers
#
# Provides utilities for merging configurations from multiple layers:
# 1. Schema (type contracts)
# 2. Service Defaults (base values)
# 3. Mode Overlay (mode-specific tuning)
# 4. User Customization (overrides)
{
# Recursively merge two record configurations
# Override values take precedence over base values
# When both values are records, merge recursively (deep merge)
#
# Example:
# let base = { server = { port = 9000, workers = 4 } }
# let mode = { server = { workers = 16 } }
# merge_with_override base mode
# # Result: { server = { port = 9000, workers = 16 } }
merge_with_override = fun base override =>
if (std.typeof base) == "record" && (std.typeof override) == "record" then
let base_fields = std.record.fields base in
let override_fields = std.record.fields override in
# Pass 1: iterate base fields, merging with override where present
base_fields
|> std.array.fold_left
(fun acc key =>
let base_value = std.record.get key base in
if std.record.has_field key override then
let override_value = std.record.get key override in
if (std.typeof base_value) == "record" && (std.typeof override_value) == "record" then
acc
|> std.record.insert key (
merge_with_override base_value override_value
)
else
# Override value takes precedence
acc |> std.record.insert key override_value
else
# Keep base value
acc |> std.record.insert key base_value
)
{}
# Pass 2: add fields exclusive to override
|> (fun merged =>
override_fields
|> std.array.fold_left
(fun acc key =>
if !(std.record.has_field key base) then
acc |> std.record.insert key (std.record.get key override)
else
acc
)
merged
)
else
# If either is not a record, override takes precedence
if (std.typeof override) == "null" then base else override,
# Alias for backwards compatibility
deep_merge = fun a b => merge_with_override a b,
# Compose configuration from multiple layers with proper merging
#
# Layer 1: defaults (base values)
# Layer 2: mode_config (mode-specific overrides)
# Layer 3: user_overrides (user customization)
#
# Example:
# compose_config
# orchestrator_defaults
# solo_mode_config
# user_customization
compose_config = fun defaults mode_config user_overrides =>
let base_merged = merge_with_override defaults mode_config in
merge_with_override base_merged user_overrides,
# Transform configuration for runtime (identity for now, extensible)
# This is a hook for service-specific transformations
transform_for_runtime = fun config service_name =>
config,
# Flatten nested record into dot-notation for TOML export
# Example:
# flatten_config { server = { port = 9000 } }
# # Result: { "server.port" = 9000 }
flatten_config = fun config =>
let rec flatten_with_prefix = fun prefix config =>
if (std.typeof config) == "record" then
std.record.fields config
|> std.array.fold_left
(fun acc key =>
let value = std.record.get key config in
let full_key = if std.string.is_empty prefix then
key
else
prefix ++ "." ++ key
in
if (std.typeof value) == "record" then
acc
|> std.record.merge (flatten_with_prefix full_key value)
else
acc |> std.record.insert full_key value
)
{}
else
config
in
flatten_with_prefix "" config,
}