UnionConfig
Type-safe configuration for F# using discriminated unions. Read from env vars, .env files, or AWS SSM Parameter Store with typed parsing, validation, and secret masking.
One package, zero external dependencies.
type AppConfig =
| DatabaseUrl
| ApiKey
| MaxRetries
| DebugMode
let configDef =
function
| DatabaseUrl ->
{ Name = "DATABASE_URL"
Kind = Manual
ValueType = StringType
Requirement = Required
IsSecret = false
Doc =
{ Description = "PostgreSQL connection string"
HowToFind = "Check your database provider dashboard"
ManagementUrl = None } }
| ApiKey ->
{ Name = "API_KEY"
Kind = Manual
ValueType = StringType
Requirement = Required
IsSecret = true
Doc =
{ Description = "External API key"
HowToFind = "Generate at https://dashboard.example.com/keys"
ManagementUrl = Some(Uri "https://dashboard.example.com/keys") } }
| MaxRetries ->
{ Name = "MAX_RETRIES"
Kind = AutoGenerated(Some "3")
ValueType = IntType
Requirement = Optional
IsSecret = false
Doc =
{ Description = "Max retry attempts"
HowToFind = "Set to desired retry count (default: 3)"
ManagementUrl = None } }
| DebugMode ->
{ Name = "DEBUG_MODE"
Kind = AutoGenerated(Some "false")
ValueType = BoolType
Requirement = Optional
IsSecret = false
Doc =
{ Description = "Enable debug logging"
HowToFind = "Set to true or 1 to enable"
ManagementUrl = None } }
Installation
|
Reading Config
open UnionConfig.Types
open UnionConfig.Reader
let dbUrl = readString (configDef DatabaseUrl)
let retries = readInt (configDef MaxRetries)
let debug = readBool (configDef DebugMode)
let dbPort = readIntOrDefault (configDef DatabasePort) 5432
// read returns Result<ConfigValue option, string>
match read (configDef LogLevel) with
| Ok(Some(StringValue level)) -> printfn "LOG_LEVEL: %s" level
| Ok None -> printfn "LOG_LEVEL: (not set)"
| Error msg -> printfn "LOG_LEVEL error: %s" msg
ConfigValue Extraction
Pipeline-friendly typed extraction:
let dbUrl = read (configDef DatabaseUrl) |> Result.map ConfigValue.stringOption
let retries = read (configDef MaxRetries) |> Result.map ConfigValue.intOption
let debug = read (configDef DebugMode) |> Result.map ConfigValue.boolOption
let timeout = read (configDef RequestTimeout) |> Result.map ConfigValue.floatOption
// Custom parsing
let parseLogLevel (s: string) =
match s.ToLowerInvariant() with
| "debug" | "info" | "warn" | "error" -> Some(s.ToUpperInvariant())
| _ -> None
let level = read (configDef LogLevel) |> Result.map (ConfigValue.customOption parseLogLevel)
ConfigRegistry
Reflection-based DU case discovery -- no manual allCases arrays:
open UnionConfig.ConfigRegistry
// Flat discovery
let allDefs = allDefs<AppConfig> configDef
let lookup = byName allDefs
let varNames = names allDefs
// Grouped discovery (nested DUs -> groups by wrapper case name)
let grouped = allDefsGrouped<AppConfig> configDef
// Returns: (string * ConfigVarDef array) array
Validation
let errors = validateRequired allDefs
// errors = ["DATABASE_URL: required but not set"; ...]
.env File Operations
open UnionConfig.EnvFile
let config = readEnvFile ".env"
// Diff two configs
let changes = compareConfigs staging prod
displayChanges changes
// Secret masking (PASSWORD, SECRET, KEY, API_KEY, SIGNING, TOKEN)
let display = maskValue "API_KEY" "sk-1234" // "sk-1***"
// Write sectioned .env file from grouped defs
let grouped = allDefsGrouped<AppConfig> configDef
let sections = defaultSections grouped currentValues
// Generate a "MISSING CONFIG" banner for required entries without values
let allDefs = allDefs<AppConfig> configDef
let headerLines = missingEntriesHeader allDefs currentValues
writeEnvFile ".env" headerLines sections
The generated .env file includes a header highlighting missing required entries:
|
Verification
open UnionConfig.Verification
let results =
allDefs
|> Array.map (fun def ->
let result =
match def.Kind with
| Infrastructure -> VerifySkipped "managed by infrastructure"
| AutoProvisioned -> VerifySkipped "auto-provisioned"
| Manual | AutoGenerated _ ->
match read def with
| Ok(Some _) -> VerifySuccess $"set (%A{def.ValueType})"
| _ -> VerifyFailed "not set"
(def.Name, result))
displayVerificationResults results
AWS SSM Parameter Store
UnionConfig includes an SSM config store that works with any parameter store backend. You provide the operations; UnionConfig handles path mapping, secret detection, and change application.
open UnionConfig.SsmConfigStore
// Provide your own SSM operations (e.g., via AWSSDK.SSM)
let ops: SsmOperations =
{ GetParameter = fun path -> (* your implementation *) None
SetParameter = fun path value isSecure -> (* your implementation *) Ok()
DeleteParameter = fun path -> (* your implementation *) Ok()
GetParametersByPath = fun prefix -> (* your implementation *) [] }
// Create a store with path mapping
let store: SsmConfigStore =
{ Operations = ops
PathMapping =
{ ToPath = fun name -> $"/myapp/staging/%s{name}"
FromPath = fun path -> path.Replace("/myapp/staging/", "")
PathPrefix = "/myapp/staging/" }
IsSecret = fun name -> name = "API_KEY" }
let value = getValue store "DATABASE_URL" // string option
let ok = setValue store "MAX_RETRIES" "5" // bool
let all = loadAll store varNames // Map<string, string>
let results = applyChanges store changes // (key * success * wasDelete) array
Interactive Editor
open UnionConfig.ConfigEditor
// Apply defaults without opening editor
let result = populateDefaults getValueFn setValueFn getDefaultsFn writeLocalFileFn
// Full workflow: load -> $EDITOR -> diff -> verify -> confirm -> apply
editConfig loadConfigFn setValueFn writeConfigFileFn verifyChangesFn
Reference
See the Example App for a complete working example covering the full public API.
Types
type ConfigVarKind = Manual | AutoGenerated of string option | Infrastructure | AutoProvisioned
type ConfigValueType = StringType | IntType | BoolType | FloatType
| CustomType of typeName: string * validate: (string -> string option)
type ConfigRequirement = Required | Optional
type ConfigVarDef = {
Name: string; Kind: ConfigVarKind; ValueType: ConfigValueType
Requirement: ConfigRequirement; IsSecret: bool; Doc: ConfigVarDoc }
type ConfigValue = StringValue of string | IntValue of int | BoolValue of bool | FloatValue of float
Key Functions
// Reader
Reader.read : ConfigVarDef -> Result<ConfigValue option, string>
Reader.readString : ConfigVarDef -> string
Reader.readInt : ConfigVarDef -> int
Reader.readBool : ConfigVarDef -> bool
Reader.readIntOrDefault : ConfigVarDef -> int -> int
Reader.readBoolOrDefault : ConfigVarDef -> bool -> bool
Reader.validateRequired : ConfigVarDef seq -> string list
// ConfigValue extraction
ConfigValue.string / stringOption : ConfigValue option -> string / string option
ConfigValue.int / intOption : ConfigValue option -> int / int option
ConfigValue.bool / boolOption : ConfigValue option -> bool / bool option
ConfigValue.float / floatOption : ConfigValue option -> float / float option
ConfigValue.custom / customOption : (string -> 'T option) -> ConfigValue option -> 'T / 'T option
// ConfigRegistry
ConfigRegistry.allDefs<'T> : ('T -> ConfigVarDef) -> ConfigVarDef array
ConfigRegistry.allDefsGrouped<'T> : ('T -> ConfigVarDef) -> (string * ConfigVarDef array) array
ConfigRegistry.byName : ConfigVarDef array -> Map<string, ConfigVarDef>
ConfigRegistry.names : ConfigVarDef array -> string array
// EnvFile
EnvFile.readEnvFile : string -> Map<string, string>
EnvFile.writeEnvFile : string -> string list -> EnvFileSection array -> unit
EnvFile.defaultSections : (string * ConfigVarDef array) array -> Map<string, string> -> EnvFileSection array
EnvFile.missingEntriesHeader : ConfigVarDef array -> Map<string, string> -> string list
EnvFile.compareConfigs : Map<string, string> -> Map<string, string> -> (string * string * string) array
EnvFile.maskValue : string -> string -> string
EnvFile.displayChanges : (string * string * string) array -> unit
// SsmConfigStore
SsmConfigStore.getValue : SsmConfigStore -> string -> string option
SsmConfigStore.setValue : SsmConfigStore -> string -> string -> bool
SsmConfigStore.deleteValue : SsmConfigStore -> string -> bool
SsmConfigStore.loadAll : SsmConfigStore -> string array -> Map<string, string>
SsmConfigStore.applyChanges : SsmConfigStore -> (string * string * string) array -> (string * bool * bool) array
// ConfigEditor
ConfigEditor.populateDefaults : (string -> string option) -> ... -> PopulateResult
ConfigEditor.editConfig : (unit -> Map<string, string>) -> ... -> unit
// Verification
Verification.displayVerificationResults : (string * VerificationResult) array -> unit
// Parsing
parseBool : string -> bool option
parseValue : ConfigValueType -> string -> Result<ConfigValue, string>
License
MIT
type AppConfig =
| DatabaseUrl
| ApiKey
| MaxRetries
| DebugMode
val configDef: _arg1: AppConfig -> 'a
union case AppConfig.DatabaseUrl: AppConfig
union case Option.None: Option<'T>
union case AppConfig.ApiKey: AppConfig
union case Option.Some: Value: 'T -> Option<'T>
union case AppConfig.MaxRetries: AppConfig
union case AppConfig.DebugMode: AppConfig
val dbUrl: obj
val retries: obj
val debug: obj
val dbPort: obj
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val level: string
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val msg: string
val dbUrl: Result<obj,obj>
Multiple items
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
module Result from Microsoft.FSharp.Core
--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val map: mapping: ('T -> 'U) -> result: Result<'T,'TError> -> Result<'U,'TError>
val retries: Result<obj,obj>
val debug: Result<obj,obj>
val timeout: Result<obj,obj>
val parseLogLevel: s: string -> string option
val s: string
Multiple items
val string: value: 'T -> string
--------------------
type string = System.String
val string: value: 'T -> string
--------------------
type string = System.String
System.String.ToLowerInvariant() : string
System.String.ToUpperInvariant() : string
val level: Result<obj,obj>
val allDefs: obj
val lookup: obj
val varNames: obj
val grouped: obj
val errors: obj
val config: obj
val changes: obj
val display: obj
val sections: obj
val headerLines: obj
module Array
from Microsoft.FSharp.Collections
val map: mapping: ('T -> 'U) -> array: 'T array -> 'U array
type 'T option = Option<'T>
type bool = System.Boolean
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
Multiple items
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float
Multiple items
val seq: sequence: 'T seq -> 'T seq
--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
val seq: sequence: 'T seq -> 'T seq
--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
type 'T list = List<'T>
type 'T array = 'T array
Multiple items
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
type unit = Unit
UnionConfig