OCaml Library Collection
/

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}/data

No 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 e

Note: 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 *)