This cookbook provides complete examples for common INI configuration patterns. Each example is self-contained and can be adapted to your use case.
For runnable code, see test/cookbook.ml in the source repository.
Optional Values and Defaults
Handle missing options gracefully with defaults or optional fields.
type database_config = {
host : string;
port : int; (* Uses default if missing *)
password : string option; (* Optional field *)
}
let database_codec = Init.Section.(
obj (fun host port password -> { host; port; password })
|> mem "host" Init.string ~enc:(fun c -> c.host)
(* dec_absent provides a default value when the option is missing *)
|> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port)
(* opt_mem decodes to None when the option is missing *)
|> opt_mem "password" Init.string ~enc:(fun c -> c.password)
|> finish
)Lists and Comma-Separated Values
Parse comma-separated lists of values.
type config = {
hosts : string list;
ports : int list;
}
let section_codec = Init.Section.(
obj (fun hosts ports -> { hosts; ports })
|> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts)
|> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports)
|> finish
)Configuration file:
[cluster] hosts = node1.example.com, node2.example.com, node3.example.com ports = 8080, 8081, 8082
Handling Unknown Options
Three strategies for dealing with options you didn't expect:
Skip Unknown (Default)
Silently ignore extra options:
let section_codec = Init.Section.(
obj (fun known_key -> known_key)
|> mem "known_key" Init.string ~enc:Fun.id
|> skip_unknown (* This is the default *)
|> finish
)Error on Unknown
Strict validation - reject unexpected options:
let section_codec = Init.Section.(
obj (fun known_key -> known_key)
|> mem "known_key" Init.string ~enc:Fun.id
|> error_unknown (* Reject unknown options *)
|> finish
)Keep Unknown
Capture unknown options for pass-through:
type config = {
known_key : string;
extra : (string * string) list;
}
let section_codec = Init.Section.(
obj (fun known_key extra -> { known_key; extra })
|> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
|> keep_unknown ~enc:(fun c -> c.extra)
|> finish
)Interpolation
Basic Interpolation
Variable substitution using %(name)s syntax:
let paths_codec = Init.Section.(
obj (fun base data logs -> (base, data, logs))
|> mem "base" Init.string ~enc:(fun (b,_,_) -> b)
|> mem "data" Init.string ~enc:(fun (_,d,_) -> d)
|> mem "logs" Init.string ~enc:(fun (_,_,l) -> l)
|> finish
)Configuration file:
[paths] base = /opt/myapp data = %(base)s/data logs = %(base)s/logs
After interpolation: data = /opt/myapp/data, logs = /opt/myapp/logs.
Extended Interpolation
Cross-section references using $\{section:name\} syntax:
let config = { Init_bytesrw.default_config with
interpolation = `Extended_interpolation }Configuration file:
[common]
base = /opt/myapp
[server]
data_dir = ${common:base}/dataNo Interpolation
Disable interpolation for files with literal % characters:
let config = Init_bytesrw.raw_config
(* Or: *)
let config = { Init_bytesrw.default_config with
interpolation = `No_interpolation }Multi-File Configuration
Layer multiple configuration files, with later files overriding earlier ones:
(* Read base config, then override with environment-specific settings *)
let load_config ~env =
let base = read_file "config/default.ini" in
let env_config = read_file (Printf.sprintf "config/%s.ini" env) in
(* Parse base first, then override with env_config *)
match Init_bytesrw.decode_string config_codec base with
| Error e -> Error e
| Ok base_config ->
(* Merge or override as needed *)
...Layout-Preserving Round-Trips
Preserve formatting when modifying configuration files:
(* Decode with layout preservation *)
let result = Init_bytesrw.decode_string
~layout:true (* Preserve whitespace *)
~locs:true (* Preserve locations *)
config_codec ini_text
(* Modify and re-encode - formatting is preserved *)
match result with
| Ok config ->
let modified = { config with port = 9000 } in
Init_bytesrw.encode_string config_codec modified
| Error e -> Error eNote: Comments are NOT preserved during round-trips, matching Python's configparser behavior.
Enums and Custom Types
Parse enumerated values:
type log_level = Debug | Info | Warn | Error
let log_level_codec = Init.enum [
"debug", Debug;
"info", Info;
"warn", Warn;
"error", Error;
]
(* Aliases work too *)
let environment_codec = Init.enum [
"development", Development;
"dev", Development; (* Alias *)
"production", Production;
"prod", Production; (* Alias *)
]The DEFAULT Section
The DEFAULT section provides fallback values for all other sections:
[DEFAULT] timeout = 30 [production] host = api.example.com port = 443 [staging] host = staging.example.com port = 8443 timeout = 60
Both production and staging sections can access timeout, but staging overrides the default value.
Custom Boolean Formats
Different applications use different boolean representations:
(* Python-compatible: 1/yes/true/on or 0/no/false/off *)
|> mem "flag" Init.bool
(* Strict formats *)
|> mem "enabled" Init.bool_01 (* Only 0 or 1 *)
|> mem "active" Init.bool_yesno (* Only yes or no *)
|> mem "debug" Init.bool_truefalse (* Only true or false *)
|> mem "feature" Init.bool_onoff (* Only on or off *)