OCaml codecs for Python INI file handling compatible with ConfigParser
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Init Cookbook
7
8 This file contains complete, runnable examples demonstrating common
9 patterns for using the Init library. Each example is self-contained
10 and can be adapted to your use case.
11
12 Run with: [dune exec ./test/cookbook.exe] *)
13
14(** {1 Basic Configuration}
15
16 The simplest use case: parse a configuration file with a single section
17 into an OCaml record. *)
18
19module Basic = struct
20 type server_config = {
21 host : string;
22 port : int;
23 debug : bool;
24 }
25
26 let server_codec = Init.Section.(
27 obj (fun host port debug -> { host; port; debug })
28 |> mem "host" Init.string ~enc:(fun c -> c.host)
29 |> mem "port" Init.int ~enc:(fun c -> c.port)
30 |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
31 |> finish
32 )
33
34 let config_codec = Init.Document.(
35 obj (fun server -> server)
36 |> section "server" server_codec ~enc:Fun.id
37 |> finish
38 )
39
40 let example () =
41 let ini = {|
42[server]
43host = localhost
44port = 8080
45debug = yes
46|} in
47 match Init_bytesrw.decode_string config_codec ini with
48 | Ok config ->
49 Printf.printf "Server: %s:%d (debug=%b)\n"
50 config.host config.port config.debug
51 | Error msg ->
52 Printf.printf "Error: %s\n" msg
53end
54
55(** {1 Optional Values and Defaults}
56
57 Handle missing options gracefully with defaults or optional fields. *)
58
59module Optional_values = struct
60 type database_config = {
61 host : string;
62 port : int; (* Uses default if missing *)
63 username : string;
64 password : string option; (* Optional field *)
65 ssl : bool;
66 }
67
68 let database_codec = Init.Section.(
69 obj (fun host port username password ssl ->
70 { host; port; username; password; ssl })
71 |> mem "host" Init.string ~enc:(fun c -> c.host)
72 (* dec_absent provides a default value when the option is missing *)
73 |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port)
74 |> mem "username" Init.string ~enc:(fun c -> c.username)
75 (* opt_mem decodes to None when the option is missing *)
76 |> opt_mem "password" Init.string ~enc:(fun c -> c.password)
77 |> mem "ssl" Init.bool ~dec_absent:true ~enc:(fun c -> c.ssl)
78 |> finish
79 )
80
81 let config_codec = Init.Document.(
82 obj Fun.id
83 |> section "database" database_codec ~enc:Fun.id
84 |> finish
85 )
86
87 let example () =
88 (* Minimal config - uses defaults *)
89 let ini = {|
90[database]
91host = db.example.com
92username = admin
93|} in
94 match Init_bytesrw.decode_string config_codec ini with
95 | Ok config ->
96 Printf.printf "DB: %s@%s:%d (ssl=%b, password=%s)\n"
97 config.username config.host config.port config.ssl
98 (match config.password with Some _ -> "***" | None -> "none")
99 | Error msg ->
100 Printf.printf "Error: %s\n" msg
101end
102
103(** {1 Multiple Sections}
104
105 Parse a configuration with multiple sections, some required and some
106 optional. *)
107
108module Multiple_sections = struct
109 type server = { host : string; port : int }
110 type database = { connection : string; pool_size : int }
111 type cache = { enabled : bool; ttl : int }
112 type config = {
113 server : server;
114 database : database;
115 cache : cache option; (* Optional section *)
116 }
117
118 let server_codec = Init.Section.(
119 obj (fun host port -> { host; port })
120 |> mem "host" Init.string ~enc:(fun s -> s.host)
121 |> mem "port" Init.int ~enc:(fun s -> s.port)
122 |> finish
123 )
124
125 let database_codec = Init.Section.(
126 obj (fun connection pool_size -> { connection; pool_size })
127 |> mem "connection" Init.string ~enc:(fun d -> d.connection)
128 |> mem "pool_size" Init.int ~dec_absent:10 ~enc:(fun d -> d.pool_size)
129 |> finish
130 )
131
132 let cache_codec = Init.Section.(
133 obj (fun enabled ttl -> { enabled; ttl })
134 |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
135 |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
136 |> finish
137 )
138
139 let config_codec = Init.Document.(
140 obj (fun server database cache -> { server; database; cache })
141 |> section "server" server_codec ~enc:(fun c -> c.server)
142 |> section "database" database_codec ~enc:(fun c -> c.database)
143 (* opt_section allows the section to be absent *)
144 |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
145 |> finish
146 )
147
148 let example () =
149 let ini = {|
150[server]
151host = api.example.com
152port = 443
153
154[database]
155connection = postgres://localhost/mydb
156
157[cache]
158enabled = yes
159ttl = 7200
160|} in
161 match Init_bytesrw.decode_string config_codec ini with
162 | Ok config ->
163 Printf.printf "Server: %s:%d\n" config.server.host config.server.port;
164 Printf.printf "Database: %s (pool=%d)\n"
165 config.database.connection config.database.pool_size;
166 (match config.cache with
167 | Some c -> Printf.printf "Cache: enabled=%b ttl=%d\n" c.enabled c.ttl
168 | None -> Printf.printf "Cache: disabled\n")
169 | Error msg ->
170 Printf.printf "Error: %s\n" msg
171end
172
173(** {1 Lists and Comma-Separated Values}
174
175 Parse comma-separated lists of values. *)
176
177module Lists = struct
178 type config = {
179 hosts : string list;
180 ports : int list;
181 tags : string list;
182 }
183
184 let section_codec = Init.Section.(
185 obj (fun hosts ports tags -> { hosts; ports; tags })
186 |> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts)
187 |> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports)
188 |> mem "tags" (Init.list Init.string) ~dec_absent:[] ~enc:(fun c -> c.tags)
189 |> finish
190 )
191
192 let config_codec = Init.Document.(
193 obj Fun.id
194 |> section "cluster" section_codec ~enc:Fun.id
195 |> finish
196 )
197
198 let example () =
199 let ini = {|
200[cluster]
201hosts = node1.example.com, node2.example.com, node3.example.com
202ports = 8080, 8081, 8082
203|} in
204 match Init_bytesrw.decode_string config_codec ini with
205 | Ok config ->
206 Printf.printf "Hosts: %s\n" (String.concat ", " config.hosts);
207 Printf.printf "Ports: %s\n"
208 (String.concat ", " (List.map string_of_int config.ports));
209 Printf.printf "Tags: %s\n"
210 (if config.tags = [] then "(none)" else String.concat ", " config.tags)
211 | Error msg ->
212 Printf.printf "Error: %s\n" msg
213end
214
215(** {1 Enums and Custom Types}
216
217 Parse enumerated values and custom types. *)
218
219module Enums = struct
220 type log_level = Debug | Info | Warn | Error
221 type environment = Development | Staging | Production
222
223 type config = {
224 log_level : log_level;
225 environment : environment;
226 max_connections : int;
227 }
228
229 let log_level_codec = Init.enum [
230 "debug", Debug;
231 "info", Info;
232 "warn", Warn;
233 "error", Error;
234 ]
235
236 let environment_codec = Init.enum [
237 "development", Development;
238 "dev", Development; (* Alias *)
239 "staging", Staging;
240 "production", Production;
241 "prod", Production; (* Alias *)
242 ]
243
244 let section_codec = Init.Section.(
245 obj (fun log_level environment max_connections ->
246 { log_level; environment; max_connections })
247 |> mem "log_level" log_level_codec ~dec_absent:Info
248 ~enc:(fun c -> c.log_level)
249 |> mem "environment" environment_codec ~enc:(fun c -> c.environment)
250 |> mem "max_connections" Init.int ~dec_absent:100
251 ~enc:(fun c -> c.max_connections)
252 |> finish
253 )
254
255 let config_codec = Init.Document.(
256 obj Fun.id
257 |> section "app" section_codec ~enc:Fun.id
258 |> finish
259 )
260
261 let example () =
262 let ini = {|
263[app]
264log_level = debug
265environment = prod
266|} in
267 match Init_bytesrw.decode_string config_codec ini with
268 | Ok config ->
269 let env_str = match config.environment with
270 | Development -> "development"
271 | Staging -> "staging"
272 | Production -> "production"
273 in
274 Printf.printf "Env: %s, Log: %s, MaxConn: %d\n"
275 env_str
276 (match config.log_level with
277 | Debug -> "debug" | Info -> "info"
278 | Warn -> "warn" | Error -> "error")
279 config.max_connections
280 | Error msg ->
281 Printf.printf "Error: %s\n" msg
282end
283
284(** {1 Handling Unknown Options}
285
286 Three strategies for dealing with options you didn't expect. *)
287
288module Unknown_options = struct
289 (* Strategy 1: Skip unknown (default) - silently ignore extra options *)
290 module Skip = struct
291 type config = { known_key : string }
292
293 let section_codec = Init.Section.(
294 obj (fun known_key -> { known_key })
295 |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
296 |> skip_unknown (* This is the default *)
297 |> finish
298 )
299
300 let _config_codec = Init.Document.(
301 obj Fun.id
302 |> section "test" section_codec ~enc:Fun.id
303 |> finish
304 )
305 end
306
307 (* Strategy 2: Error on unknown - strict validation *)
308 module Strict = struct
309 type config = { known_key : string }
310
311 let section_codec = Init.Section.(
312 obj (fun known_key -> { known_key })
313 |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
314 |> error_unknown (* Reject unknown options *)
315 |> finish
316 )
317
318 let _config_codec = Init.Document.(
319 obj Fun.id
320 |> section "test" section_codec ~enc:Fun.id
321 |> error_unknown (* Also reject unknown sections *)
322 |> finish
323 )
324 end
325
326 (* Strategy 3: Keep unknown - capture for pass-through *)
327 module Passthrough = struct
328 type config = {
329 known_key : string;
330 extra : (string * string) list;
331 }
332
333 let section_codec = Init.Section.(
334 obj (fun known_key extra -> { known_key; extra })
335 |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
336 |> keep_unknown ~enc:(fun c -> c.extra)
337 |> finish
338 )
339
340 let config_codec = Init.Document.(
341 obj Fun.id
342 |> section "test" section_codec ~enc:Fun.id
343 |> finish
344 )
345
346 let example () =
347 let ini = {|
348[test]
349known_key = hello
350extra1 = world
351extra2 = foo
352|} in
353 match Init_bytesrw.decode_string config_codec ini with
354 | Ok config ->
355 Printf.printf "Known: %s\n" config.known_key;
356 List.iter (fun (k, v) ->
357 Printf.printf "Extra: %s = %s\n" k v
358 ) config.extra
359 | Error msg ->
360 Printf.printf "Error: %s\n" msg
361 end
362end
363
364(** {1 Interpolation}
365
366 Variable substitution in values. *)
367
368module Interpolation = struct
369 (* Basic interpolation: %(name)s *)
370 module Basic = struct
371 type paths = {
372 base : string;
373 data : string;
374 logs : string;
375 config : string;
376 }
377
378 let paths_codec = Init.Section.(
379 obj (fun base data logs config -> { base; data; logs; config })
380 |> mem "base" Init.string ~enc:(fun p -> p.base)
381 |> mem "data" Init.string ~enc:(fun p -> p.data)
382 |> mem "logs" Init.string ~enc:(fun p -> p.logs)
383 |> mem "config" Init.string ~enc:(fun p -> p.config)
384 |> finish
385 )
386
387 let config_codec = Init.Document.(
388 obj Fun.id
389 |> section "paths" paths_codec ~enc:Fun.id
390 |> finish
391 )
392
393 let example () =
394 let ini = {|
395[paths]
396base = /opt/myapp
397data = %(base)s/data
398logs = %(base)s/logs
399config = %(base)s/etc
400|} in
401 match Init_bytesrw.decode_string config_codec ini with
402 | Ok paths ->
403 Printf.printf "Base: %s\n" paths.base;
404 Printf.printf "Data: %s\n" paths.data;
405 Printf.printf "Logs: %s\n" paths.logs;
406 Printf.printf "Config: %s\n" paths.config
407 | Error msg ->
408 Printf.printf "Error: %s\n" msg
409 end
410
411 (* Extended interpolation: ${section:name} *)
412 module Extended = struct
413 type common = { base : string }
414 type server = { data_dir : string; log_dir : string }
415 type config = { common : common; server : server }
416
417 let common_codec = Init.Section.(
418 obj (fun base -> { base })
419 |> mem "base" Init.string ~enc:(fun c -> c.base)
420 |> finish
421 )
422
423 let server_codec = Init.Section.(
424 obj (fun data_dir log_dir -> { data_dir; log_dir })
425 |> mem "data_dir" Init.string ~enc:(fun s -> s.data_dir)
426 |> mem "log_dir" Init.string ~enc:(fun s -> s.log_dir)
427 |> finish
428 )
429
430 let config_codec = Init.Document.(
431 obj (fun common server -> { common; server })
432 |> section "common" common_codec ~enc:(fun c -> c.common)
433 |> section "server" server_codec ~enc:(fun c -> c.server)
434 |> finish
435 )
436
437 let example () =
438 let config = { Init_bytesrw.default_config with
439 interpolation = `Extended_interpolation } in
440 let ini = {|
441[common]
442base = /opt/myapp
443
444[server]
445data_dir = ${common:base}/data
446log_dir = ${common:base}/logs
447|} in
448 match Init_bytesrw.decode_string ~config config_codec ini with
449 | Ok cfg ->
450 Printf.printf "Base: %s\n" cfg.common.base;
451 Printf.printf "Data: %s\n" cfg.server.data_dir;
452 Printf.printf "Log: %s\n" cfg.server.log_dir
453 | Error msg ->
454 Printf.printf "Error: %s\n" msg
455 end
456end
457
458(** {1 The DEFAULT Section}
459
460 The DEFAULT section provides fallback values for all other sections. *)
461
462module Defaults = struct
463 type section = {
464 host : string;
465 port : int;
466 timeout : int; (* Falls back to DEFAULT *)
467 }
468
469 type config = {
470 production : section;
471 staging : section;
472 }
473
474 let section_codec = Init.Section.(
475 obj (fun host port timeout -> { host; port; timeout })
476 |> mem "host" Init.string ~enc:(fun s -> s.host)
477 |> mem "port" Init.int ~enc:(fun s -> s.port)
478 |> mem "timeout" Init.int ~enc:(fun s -> s.timeout)
479 |> finish
480 )
481
482 let config_codec = Init.Document.(
483 obj (fun production staging -> { production; staging })
484 |> section "production" section_codec ~enc:(fun c -> c.production)
485 |> section "staging" section_codec ~enc:(fun c -> c.staging)
486 |> skip_unknown (* Ignore DEFAULT section in output *)
487 |> finish
488 )
489
490 let example () =
491 let ini = {|
492[DEFAULT]
493timeout = 30
494
495[production]
496host = api.example.com
497port = 443
498
499[staging]
500host = staging.example.com
501port = 8443
502timeout = 60
503|} in
504 match Init_bytesrw.decode_string config_codec ini with
505 | Ok config ->
506 Printf.printf "Production: %s:%d (timeout=%d)\n"
507 config.production.host config.production.port config.production.timeout;
508 Printf.printf "Staging: %s:%d (timeout=%d)\n"
509 config.staging.host config.staging.port config.staging.timeout
510 | Error msg ->
511 Printf.printf "Error: %s\n" msg
512end
513
514(** {1 Round-Trip Encoding}
515
516 Decode, modify, and re-encode a configuration. *)
517
518module Roundtrip = struct
519 type config = {
520 host : string;
521 port : int;
522 }
523
524 let section_codec = Init.Section.(
525 obj (fun host port -> { host; port })
526 |> mem "host" Init.string ~enc:(fun c -> c.host)
527 |> mem "port" Init.int ~enc:(fun c -> c.port)
528 |> finish
529 )
530
531 let config_codec = Init.Document.(
532 obj Fun.id
533 |> section "server" section_codec ~enc:Fun.id
534 |> finish
535 )
536
537 let example () =
538 (* Decode *)
539 let ini = {|
540[server]
541host = localhost
542port = 8080
543|} in
544 match Init_bytesrw.decode_string config_codec ini with
545 | Error msg ->
546 Printf.printf "Decode error: %s\n" msg
547 | Ok config ->
548 Printf.printf "Decoded: %s:%d\n" config.host config.port;
549
550 (* Modify *)
551 let modified = { config with port = 9000 } in
552
553 (* Encode *)
554 (match Init_bytesrw.encode_string config_codec modified with
555 | Ok output ->
556 Printf.printf "Encoded:\n%s" output
557 | Error msg ->
558 Printf.printf "Encode error: %s\n" msg)
559end
560
561(** {1 Custom Boolean Formats}
562
563 Different applications use different boolean representations. *)
564
565module Custom_booleans = struct
566 type config = {
567 python_style : bool; (* 1/yes/true/on or 0/no/false/off *)
568 strict_01 : bool; (* Only 0 or 1 *)
569 yes_no : bool; (* Only yes or no *)
570 on_off : bool; (* Only on or off *)
571 }
572
573 let section_codec = Init.Section.(
574 obj (fun python_style strict_01 yes_no on_off ->
575 { python_style; strict_01; yes_no; on_off })
576 |> mem "python_style" Init.bool ~enc:(fun c -> c.python_style)
577 |> mem "strict_01" Init.bool_01 ~enc:(fun c -> c.strict_01)
578 |> mem "yes_no" Init.bool_yesno ~enc:(fun c -> c.yes_no)
579 |> mem "on_off" Init.bool_onoff ~enc:(fun c -> c.on_off)
580 |> finish
581 )
582
583 let config_codec = Init.Document.(
584 obj Fun.id
585 |> section "flags" section_codec ~enc:Fun.id
586 |> finish
587 )
588
589 let example () =
590 let ini = {|
591[flags]
592python_style = YES
593strict_01 = 1
594yes_no = no
595on_off = on
596|} in
597 match Init_bytesrw.decode_string config_codec ini with
598 | Ok config ->
599 Printf.printf "python_style=%b strict_01=%b yes_no=%b on_off=%b\n"
600 config.python_style config.strict_01 config.yes_no config.on_off
601 | Error msg ->
602 Printf.printf "Error: %s\n" msg
603end
604
605(** {1 Error Handling}
606
607 Demonstrate different error scenarios and how to handle them. *)
608
609module Error_handling = struct
610 type config = { port : int }
611
612 let section_codec = Init.Section.(
613 obj (fun port -> { port })
614 |> mem "port" Init.int ~enc:(fun c -> c.port)
615 |> finish
616 )
617
618 let config_codec = Init.Document.(
619 obj Fun.id
620 |> section "server" section_codec ~enc:Fun.id
621 |> finish
622 )
623
624 let example () =
625 (* Missing section *)
626 let ini1 = {|
627[wrong_name]
628port = 8080
629|} in
630 (match Init_bytesrw.decode_string config_codec ini1 with
631 | Ok _ -> Printf.printf "Unexpected success\n"
632 | Error msg -> Printf.printf "Missing section: %s\n\n" msg);
633
634 (* Missing option *)
635 let ini2 = {|
636[server]
637host = localhost
638|} in
639 (match Init_bytesrw.decode_string config_codec ini2 with
640 | Ok _ -> Printf.printf "Unexpected success\n"
641 | Error msg -> Printf.printf "Missing option: %s\n\n" msg);
642
643 (* Type mismatch *)
644 let ini3 = {|
645[server]
646port = not_a_number
647|} in
648 (match Init_bytesrw.decode_string config_codec ini3 with
649 | Ok _ -> Printf.printf "Unexpected success\n"
650 | Error msg -> Printf.printf "Type mismatch: %s\n\n" msg);
651
652 (* Using structured errors *)
653 let ini4 = {|
654[server]
655port = abc
656|} in
657 match Init_bytesrw.decode_string' config_codec ini4 with
658 | Ok _ -> Printf.printf "Unexpected success\n"
659 | Error e ->
660 Printf.printf "Structured error:\n";
661 Printf.printf " Kind: %s\n" (Init.Error.kind_to_string (Init.Error.kind e));
662 Format.printf " Path: %a\n" Init.Path.pp (Init.Error.path e)
663end
664
665(** {1 Running Examples} *)
666
667let () =
668 Printf.printf "=== Basic Configuration ===\n";
669 Basic.example ();
670 Printf.printf "\n";
671
672 Printf.printf "=== Optional Values ===\n";
673 Optional_values.example ();
674 Printf.printf "\n";
675
676 Printf.printf "=== Multiple Sections ===\n";
677 Multiple_sections.example ();
678 Printf.printf "\n";
679
680 Printf.printf "=== Lists ===\n";
681 Lists.example ();
682 Printf.printf "\n";
683
684 Printf.printf "=== Enums ===\n";
685 Enums.example ();
686 Printf.printf "\n";
687
688 Printf.printf "=== Unknown Options (Passthrough) ===\n";
689 Unknown_options.Passthrough.example ();
690 Printf.printf "\n";
691
692 Printf.printf "=== Basic Interpolation ===\n";
693 Interpolation.Basic.example ();
694 Printf.printf "\n";
695
696 Printf.printf "=== Extended Interpolation ===\n";
697 Interpolation.Extended.example ();
698 Printf.printf "\n";
699
700 Printf.printf "=== DEFAULT Section ===\n";
701 Defaults.example ();
702 Printf.printf "\n";
703
704 Printf.printf "=== Round-Trip Encoding ===\n";
705 Roundtrip.example ();
706 Printf.printf "\n";
707
708 Printf.printf "=== Custom Booleans ===\n";
709 Custom_booleans.example ();
710 Printf.printf "\n";
711
712 Printf.printf "=== Error Handling ===\n";
713 Error_handling.example ()