OCaml codecs for Python INI file handling compatible with ConfigParser
1{0 Init Cookbook}
2
3This cookbook provides complete examples for common INI configuration patterns.
4Each example is self-contained and can be adapted to your use case.
5
6For runnable code, see [test/cookbook.ml] in the source repository.
7
8{1:optional_values Optional Values and Defaults}
9
10Handle missing options gracefully with defaults or optional fields.
11
12{[
13type database_config = {
14 host : string;
15 port : int; (* Uses default if missing *)
16 password : string option; (* Optional field *)
17}
18
19let database_codec = Init.Section.(
20 obj (fun host port password -> { host; port; password })
21 |> mem "host" Init.string ~enc:(fun c -> c.host)
22 (* dec_absent provides a default value when the option is missing *)
23 |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port)
24 (* opt_mem decodes to None when the option is missing *)
25 |> opt_mem "password" Init.string ~enc:(fun c -> c.password)
26 |> finish
27)
28]}
29
30{1:lists Lists and Comma-Separated Values}
31
32Parse comma-separated lists of values.
33
34{[
35type config = {
36 hosts : string list;
37 ports : int list;
38}
39
40let section_codec = Init.Section.(
41 obj (fun hosts ports -> { hosts; ports })
42 |> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts)
43 |> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports)
44 |> finish
45)
46]}
47
48Configuration file:
49{v
50[cluster]
51hosts = node1.example.com, node2.example.com, node3.example.com
52ports = 8080, 8081, 8082
53v}
54
55{1:unknown_options Handling Unknown Options}
56
57Three strategies for dealing with options you didn't expect:
58
59{2 Skip Unknown (Default)}
60
61Silently ignore extra options:
62{[
63let section_codec = Init.Section.(
64 obj (fun known_key -> known_key)
65 |> mem "known_key" Init.string ~enc:Fun.id
66 |> skip_unknown (* This is the default *)
67 |> finish
68)
69]}
70
71{2 Error on Unknown}
72
73Strict validation - reject unexpected options:
74{[
75let section_codec = Init.Section.(
76 obj (fun known_key -> known_key)
77 |> mem "known_key" Init.string ~enc:Fun.id
78 |> error_unknown (* Reject unknown options *)
79 |> finish
80)
81]}
82
83{2 Keep Unknown}
84
85Capture unknown options for pass-through:
86{[
87type config = {
88 known_key : string;
89 extra : (string * string) list;
90}
91
92let section_codec = Init.Section.(
93 obj (fun known_key extra -> { known_key; extra })
94 |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
95 |> keep_unknown ~enc:(fun c -> c.extra)
96 |> finish
97)
98]}
99
100{1:interpolation Interpolation}
101
102{2 Basic Interpolation}
103
104Variable substitution using [%(name)s] syntax:
105
106{[
107let paths_codec = Init.Section.(
108 obj (fun base data logs -> (base, data, logs))
109 |> mem "base" Init.string ~enc:(fun (b,_,_) -> b)
110 |> mem "data" Init.string ~enc:(fun (_,d,_) -> d)
111 |> mem "logs" Init.string ~enc:(fun (_,_,l) -> l)
112 |> finish
113)
114]}
115
116Configuration file:
117{v
118[paths]
119base = /opt/myapp
120data = %(base)s/data
121logs = %(base)s/logs
122v}
123
124After interpolation: [data = /opt/myapp/data], [logs = /opt/myapp/logs].
125
126{2 Extended Interpolation}
127
128Cross-section references using [$\{section:name\}] syntax:
129
130{[
131let config = { Init_bytesrw.default_config with
132 interpolation = `Extended_interpolation }
133]}
134
135Configuration file:
136{v
137[common]
138base = /opt/myapp
139
140[server]
141data_dir = ${common:base}/data
142v}
143
144{2 No Interpolation}
145
146Disable interpolation for files with literal [%] characters:
147
148{[
149let config = Init_bytesrw.raw_config
150(* Or: *)
151let config = { Init_bytesrw.default_config with
152 interpolation = `No_interpolation }
153]}
154
155{1:multifile Multi-File Configuration}
156
157Layer multiple configuration files, with later files overriding earlier ones:
158
159{[
160(* Read base config, then override with environment-specific settings *)
161let load_config ~env =
162 let base = read_file "config/default.ini" in
163 let env_config = read_file (Printf.sprintf "config/%s.ini" env) in
164 (* Parse base first, then override with env_config *)
165 match Init_bytesrw.decode_string config_codec base with
166 | Error e -> Error e
167 | Ok base_config ->
168 (* Merge or override as needed *)
169 ...
170]}
171
172{1:roundtrip Layout-Preserving Round-Trips}
173
174Preserve formatting when modifying configuration files:
175
176{[
177(* Decode with layout preservation *)
178let result = Init_bytesrw.decode_string
179 ~layout:true (* Preserve whitespace *)
180 ~locs:true (* Preserve locations *)
181 config_codec ini_text
182
183(* Modify and re-encode - formatting is preserved *)
184match result with
185| Ok config ->
186 let modified = { config with port = 9000 } in
187 Init_bytesrw.encode_string config_codec modified
188| Error e -> Error e
189]}
190
191{b Note:} Comments are NOT preserved during round-trips, matching Python's
192configparser behavior.
193
194{1:enums Enums and Custom Types}
195
196Parse enumerated values:
197
198{[
199type log_level = Debug | Info | Warn | Error
200
201let log_level_codec = Init.enum [
202 "debug", Debug;
203 "info", Info;
204 "warn", Warn;
205 "error", Error;
206]
207
208(* Aliases work too *)
209let environment_codec = Init.enum [
210 "development", Development;
211 "dev", Development; (* Alias *)
212 "production", Production;
213 "prod", Production; (* Alias *)
214]
215]}
216
217{1:defaults The DEFAULT Section}
218
219The DEFAULT section provides fallback values for all other sections:
220
221{v
222[DEFAULT]
223timeout = 30
224
225[production]
226host = api.example.com
227port = 443
228
229[staging]
230host = staging.example.com
231port = 8443
232timeout = 60
233v}
234
235Both [production] and [staging] sections can access [timeout], but [staging]
236overrides the default value.
237
238{1:booleans Custom Boolean Formats}
239
240Different applications use different boolean representations:
241
242{[
243(* Python-compatible: 1/yes/true/on or 0/no/false/off *)
244|> mem "flag" Init.bool
245
246(* Strict formats *)
247|> mem "enabled" Init.bool_01 (* Only 0 or 1 *)
248|> mem "active" Init.bool_yesno (* Only yes or no *)
249|> mem "debug" Init.bool_truefalse (* Only true or false *)
250|> mem "feature" Init.bool_onoff (* Only on or off *)
251]}