forked from
anil.recoil.org/monopam-myspace
My aggregated monorepo of OCaml code, automaintained
1{0 Cookbook}
2
3This cookbook provides patterns and recipes for common TOML tasks.
4Each section includes both conceptual explanation and working code
5examples. See {!module:Tomlt} for the full API reference.
6
7{1:conventions Conventions}
8
9Throughout this cookbook, we use the following conventions:
10
11- Codec values are named after their OCaml type (e.g., [config_codec]
12 for a [config] type)
13- The [~enc] parameter always extracts the field from the record
14- Codecs are defined using the applicative-style {!Tomlt.Table} builder
15
16{1:config_files Parsing Configuration Files}
17
18The most common use case: parsing a TOML configuration file into an
19OCaml record.
20
21{2 Basic Configuration}
22
23{[
24type database_config = {
25 host : string;
26 port : int;
27 name : string;
28}
29
30let database_config_codec =
31 Tomlt.(Table.(
32 obj (fun host port name -> { host; port; name })
33 |> mem "host" string ~enc:(fun c -> c.host)
34 |> mem "port" int ~enc:(fun c -> c.port)
35 |> mem "name" string ~enc:(fun c -> c.name)
36 |> finish
37 ))
38]}
39
40This handles TOML like:
41
42{v
43host = "localhost"
44port = 5432
45name = "myapp"
46v}
47
48{2 Nested Configuration}
49
50For nested tables, compose codecs:
51
52{[
53type server_config = {
54 host : string;
55 port : int;
56}
57
58type app_config = {
59 name : string;
60 server : server_config;
61 debug : bool;
62}
63
64let server_config_codec =
65 Tomlt.(Table.(
66 obj (fun host port -> { host; port })
67 |> mem "host" string ~enc:(fun s -> s.host)
68 |> mem "port" int ~enc:(fun s -> s.port)
69 |> finish
70 ))
71
72let app_config_codec =
73 Tomlt.(Table.(
74 obj (fun name server debug -> { name; server; debug })
75 |> mem "name" string ~enc:(fun c -> c.name)
76 |> mem "server" server_config_codec ~enc:(fun c -> c.server)
77 |> mem "debug" bool ~enc:(fun c -> c.debug)
78 |> finish
79 ))
80]}
81
82This handles:
83
84{v
85name = "My Application"
86debug = false
87
88[server]
89host = "0.0.0.0"
90port = 8080
91v}
92
93{2 Multi-Environment Configuration}
94
95A pattern for dev/staging/prod configurations:
96
97{[
98type env_config = {
99 database_url : string;
100 log_level : string;
101 cache_ttl : int;
102}
103
104type config = {
105 app_name : string;
106 development : env_config;
107 production : env_config;
108}
109
110let env_config_codec =
111 Tomlt.(Table.(
112 obj (fun database_url log_level cache_ttl ->
113 { database_url; log_level; cache_ttl })
114 |> mem "database_url" string ~enc:(fun e -> e.database_url)
115 |> mem "log_level" string ~enc:(fun e -> e.log_level)
116 |> mem "cache_ttl" int ~enc:(fun e -> e.cache_ttl)
117 |> finish
118 ))
119
120let config_codec =
121 Tomlt.(Table.(
122 obj (fun app_name development production ->
123 { app_name; development; production })
124 |> mem "app_name" string ~enc:(fun c -> c.app_name)
125 |> mem "development" env_config_codec ~enc:(fun c -> c.development)
126 |> mem "production" env_config_codec ~enc:(fun c -> c.production)
127 |> finish
128 ))
129]}
130
131{1:optional_values Optional and Absent Values}
132
133TOML tables may have optional members. Tomlt provides several ways
134to handle missing values.
135
136{2 Default Values with dec_absent}
137
138Use [~dec_absent] to provide a default when a key is missing:
139
140{[
141type settings = {
142 theme : string;
143 font_size : int;
144 show_line_numbers : bool;
145}
146
147let settings_codec =
148 Tomlt.(Table.(
149 obj (fun theme font_size show_line_numbers ->
150 { theme; font_size; show_line_numbers })
151 |> mem "theme" string ~enc:(fun s -> s.theme)
152 ~dec_absent:"default"
153 |> mem "font_size" int ~enc:(fun s -> s.font_size)
154 ~dec_absent:12
155 |> mem "show_line_numbers" bool ~enc:(fun s -> s.show_line_numbers)
156 ~dec_absent:true
157 |> finish
158 ))
159]}
160
161{v
162# All of these work:
163theme = "dark"
164
165# Or with defaults:
166# (empty table uses all defaults)
167v}
168
169{2 Option Types with opt_mem}
170
171Use {!Tomlt.Table.opt_mem} when the absence of a value is meaningful:
172
173{[
174type user = {
175 name : string;
176 email : string option;
177 phone : string option;
178}
179
180let user_codec =
181 Tomlt.(Table.(
182 obj (fun name email phone -> { name; email; phone })
183 |> mem "name" string ~enc:(fun u -> u.name)
184 |> opt_mem "email" string ~enc:(fun u -> u.email)
185 |> opt_mem "phone" string ~enc:(fun u -> u.phone)
186 |> finish
187 ))
188]}
189
190On encoding, [None] values are omitted from the output:
191
192{[
193(* This user: *)
194let user = { name = "Alice"; email = Some "alice@example.com"; phone = None }
195
196(* Encodes to: *)
197(* name = "Alice"
198 email = "alice@example.com"
199 # phone is omitted *)
200]}
201
202{2 Conditional Omission with enc_omit}
203
204Use [~enc_omit] to omit values that match a predicate:
205
206{[
207type config = {
208 name : string;
209 retries : int; (* omit if 0 *)
210}
211
212let config_codec =
213 Tomlt.(Table.(
214 obj (fun name retries -> { name; retries })
215 |> mem "name" string ~enc:(fun c -> c.name)
216 |> mem "retries" int ~enc:(fun c -> c.retries)
217 ~dec_absent:0
218 ~enc_omit:(fun r -> r = 0)
219 |> finish
220 ))
221]}
222
223{1:datetimes Working with Datetimes}
224
225TOML 1.1 supports four datetime formats. Tomlt provides Ptime-based
226codecs that handle all of them.
227
228{2 TOML Datetime Formats}
229
230{v
231# Offset datetime - full timestamp with timezone (unambiguous)
232published = 2024-01-15T10:30:00Z
233published = 2024-01-15T10:30:00-05:00
234
235# Local datetime - no timezone (wall clock time)
236meeting = 2024-01-15T10:30:00
237
238# Local date - date only
239birthday = 1979-05-27
240
241# Local time - time only
242alarm = 07:30:00
243v}
244
245{2 Basic Datetime Handling}
246
247Use {!Tomlt.ptime} to accept any datetime format and normalize to
248[Ptime.t]:
249
250{[
251type event = { name : string; timestamp : Ptime.t }
252
253let event_codec =
254 Tomlt.(Table.(
255 obj (fun name timestamp -> { name; timestamp })
256 |> mem "name" string ~enc:(fun e -> e.name)
257 |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp)
258 |> finish
259 ))
260
261(* All of these decode successfully: *)
262(* when = 2024-01-15T10:30:00Z -> offset datetime *)
263(* when = 2024-01-15T10:30:00 -> local datetime *)
264(* when = 2024-01-15 -> date only (midnight) *)
265(* when = 10:30:00 -> time only (today) *)
266]}
267
268{2 Strict Timestamp Validation}
269
270Use {!Tomlt.ptime_opt} when you require explicit timezone information:
271
272{[
273type audit_log = { action : string; timestamp : Ptime.t }
274
275let audit_codec =
276 Tomlt.(Table.(
277 obj (fun action timestamp -> { action; timestamp })
278 |> mem "action" string ~enc:(fun a -> a.action)
279 |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp)
280 |> finish
281 ))
282
283(* Valid: timestamp = 2024-01-15T10:30:00Z *)
284(* Valid: timestamp = 2024-01-15T10:30:00+05:30 *)
285(* Invalid: timestamp = 2024-01-15T10:30:00 (no timezone) *)
286(* Invalid: timestamp = 2024-01-15 (date only) *)
287]}
288
289{2 Date-Only Fields}
290
291Use {!Tomlt.ptime_date} for fields that should only contain dates:
292
293{[
294type person = { name : string; birthday : Ptime.date }
295
296let person_codec =
297 Tomlt.(Table.(
298 obj (fun name birthday -> { name; birthday })
299 |> mem "name" string ~enc:(fun p -> p.name)
300 |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday)
301 |> finish
302 ))
303
304(* birthday = 1979-05-27 -> (1979, 5, 27) *)
305]}
306
307{2 Time-Only Fields}
308
309Use {!Tomlt.ptime_span} for recurring times (as duration from midnight):
310
311{[
312type alarm = { label : string; time : Ptime.Span.t }
313
314let alarm_codec =
315 Tomlt.(Table.(
316 obj (fun label time -> { label; time })
317 |> mem "label" string ~enc:(fun a -> a.label)
318 |> mem "time" ptime_span ~enc:(fun a -> a.time)
319 |> finish
320 ))
321
322(* time = 07:30:00 -> 27000 seconds (7.5 hours from midnight) *)
323]}
324
325{2 Preserving Datetime Format}
326
327Use {!Tomlt.ptime_full} to preserve the exact datetime variant for
328roundtripping:
329
330{[
331type flexible_event = {
332 name : string;
333 when_ : Toml.ptime_datetime;
334}
335
336let flexible_codec =
337 Tomlt.(Table.(
338 obj (fun name when_ -> { name; when_ })
339 |> mem "name" string ~enc:(fun e -> e.name)
340 |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
341 |> finish
342 ))
343
344(* Decoding preserves the variant:
345 when = 2024-01-15T10:30:00Z -> `Datetime (ptime, Some 0)
346 when = 2024-01-15T10:30:00 -> `Datetime_local ptime
347 when = 2024-01-15 -> `Date (2024, 1, 15)
348 when = 10:30:00 -> `Time (10, 30, 0, 0)
349
350 Encoding reproduces the original format. *)
351]}
352
353{2 Timezone Handling}
354
355For local datetimes without explicit timezone, you can specify how
356to interpret them:
357
358{[
359(* Force UTC interpretation *)
360let utc_codec = Tomlt.ptime ~tz_offset_s:0 ()
361
362(* Force Eastern Time (-05:00 = -18000 seconds) *)
363let eastern_codec = Tomlt.ptime ~tz_offset_s:(-18000) ()
364
365(* Use system timezone (requires Tomlt_unix) *)
366let system_codec =
367 Tomlt.ptime ~get_tz:Tomlt_unix.current_tz_offset_s ()
368]}
369
370{1:arrays Working with Arrays}
371
372TOML 1.1 supports heterogeneous arrays, but most use cases involve
373homogeneous arrays of a single type.
374
375{2 Basic Arrays}
376
377{[
378type config = {
379 name : string;
380 ports : int list;
381 hosts : string list;
382}
383
384let config_codec =
385 Tomlt.(Table.(
386 obj (fun name ports hosts -> { name; ports; hosts })
387 |> mem "name" string ~enc:(fun c -> c.name)
388 |> mem "ports" (list int) ~enc:(fun c -> c.ports)
389 |> mem "hosts" (list string) ~enc:(fun c -> c.hosts)
390 |> finish
391 ))
392]}
393
394{v
395name = "load-balancer"
396ports = [80, 443, 8080]
397hosts = ["web1.example.com", "web2.example.com"]
398v}
399
400{2 Arrays of Tables}
401
402Use {!Tomlt.array_of_tables} for TOML's [[[name]]] syntax:
403
404{[
405type product = { name : string; price : float }
406type catalog = { products : product list }
407
408let product_codec =
409 Tomlt.(Table.(
410 obj (fun name price -> { name; price })
411 |> mem "name" string ~enc:(fun p -> p.name)
412 |> mem "price" float ~enc:(fun p -> p.price)
413 |> finish
414 ))
415
416let catalog_codec =
417 Tomlt.(Table.(
418 obj (fun products -> { products })
419 |> mem "products" (array_of_tables product_codec)
420 ~enc:(fun c -> c.products)
421 |> finish
422 ))
423]}
424
425{v
426[[products]]
427name = "Widget"
428price = 9.99
429
430[[products]]
431name = "Gadget"
432price = 19.99
433v}
434
435{2 Nested Arrays}
436
437Arrays can contain other arrays:
438
439{[
440type matrix = { rows : int list list }
441
442let matrix_codec =
443 Tomlt.(Table.(
444 obj (fun rows -> { rows })
445 |> mem "rows" (list (list int)) ~enc:(fun m -> m.rows)
446 |> finish
447 ))
448]}
449
450{v
451rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
452v}
453
454{2 Custom Array Types}
455
456Use {!val:Tomlt.Array.map} to decode into custom collection types:
457
458{[
459module IntSet = Set.Make(Int)
460
461let int_set_codec =
462 Tomlt.Array.(
463 map int
464 ~dec_empty:(fun () -> IntSet.empty)
465 ~dec_add:(fun x acc -> IntSet.add x acc)
466 ~dec_finish:(fun acc -> acc)
467 ~enc:{ fold = (fun f acc set -> IntSet.fold (fun x a -> f a x) set acc) }
468 |> finish
469 )
470]}
471
472{1:tables Nested Tables and Objects}
473
474{2 Inline Tables}
475
476Use {!Tomlt.Table.inline} to encode as inline tables:
477
478{[
479type point = { x : int; y : int }
480
481let point_codec =
482 Tomlt.(Table.(
483 obj (fun x y -> { x; y })
484 |> mem "x" int ~enc:(fun p -> p.x)
485 |> mem "y" int ~enc:(fun p -> p.y)
486 |> inline (* <- produces inline table *)
487 ))
488
489(* Encodes as: point = { x = 10, y = 20 } *)
490(* Instead of:
491 [point]
492 x = 10
493 y = 20 *)
494]}
495
496{2 Deeply Nested Structures}
497
498{[
499type address = { street : string; city : string }
500type company = { name : string; address : address }
501type employee = { name : string; company : company }
502
503let address_codec =
504 Tomlt.(Table.(
505 obj (fun street city -> { street; city })
506 |> mem "street" string ~enc:(fun a -> a.street)
507 |> mem "city" string ~enc:(fun a -> a.city)
508 |> finish
509 ))
510
511let company_codec =
512 Tomlt.(Table.(
513 obj (fun name address -> { name; address })
514 |> mem "name" string ~enc:(fun c -> c.name)
515 |> mem "address" address_codec ~enc:(fun c -> c.address)
516 |> finish
517 ))
518
519let employee_codec =
520 Tomlt.(Table.(
521 obj (fun name company -> { name; company })
522 |> mem "name" string ~enc:(fun e -> e.name)
523 |> mem "company" company_codec ~enc:(fun e -> e.company)
524 |> finish
525 ))
526]}
527
528{v
529name = "Alice"
530
531[company]
532name = "Acme Corp"
533
534[company.address]
535street = "123 Main St"
536city = "Springfield"
537v}
538
539{1:unknown_members Unknown Member Handling}
540
541By default, unknown members in TOML tables are ignored. You can
542change this behavior.
543
544{2 Ignoring Unknown Members (Default)}
545
546{[
547let config_codec =
548 Tomlt.(Table.(
549 obj (fun host -> host)
550 |> mem "host" string ~enc:Fun.id
551 |> skip_unknown (* default, can be omitted *)
552 |> finish
553 ))
554
555(* This works even with extra keys: *)
556(* host = "localhost"
557 unknown_key = "ignored" *)
558]}
559
560{2 Rejecting Unknown Members}
561
562Use {!Tomlt.Table.error_unknown} for strict parsing:
563
564{[
565let strict_config_codec =
566 Tomlt.(Table.(
567 obj (fun host port -> (host, port))
568 |> mem "host" string ~enc:fst
569 |> mem "port" int ~enc:snd
570 |> error_unknown (* <- rejects unknown keys *)
571 |> finish
572 ))
573
574(* Error on: host = "localhost"
575 port = 8080
576 typo = "oops" <- causes error *)
577]}
578
579{2 Collecting Unknown Members}
580
581Use {!Tomlt.Table.keep_unknown} to preserve unknown members:
582
583{[
584type config = {
585 name : string;
586 extra : (string * Toml.t) list;
587}
588
589let config_codec =
590 Tomlt.(Table.(
591 obj (fun name extra -> { name; extra })
592 |> mem "name" string ~enc:(fun c -> c.name)
593 |> keep_unknown (Mems.assoc value) ~enc:(fun c -> c.extra)
594 |> finish
595 ))
596
597(* Decoding:
598 name = "app"
599 foo = 42
600 bar = "hello"
601
602 Results in:
603 { name = "app"; extra = [("foo", Int 42L); ("bar", String "hello")] }
604*)
605]}
606
607{2 Typed Unknown Members}
608
609Collect unknown members with a specific type:
610
611{[
612module StringMap = Map.Make(String)
613
614type translations = {
615 default_lang : string;
616 strings : string StringMap.t;
617}
618
619let translations_codec =
620 Tomlt.(Table.(
621 obj (fun default_lang strings -> { default_lang; strings })
622 |> mem "default_lang" string ~enc:(fun t -> t.default_lang)
623 |> keep_unknown (Mems.string_map string) ~enc:(fun t -> t.strings)
624 |> finish
625 ))
626
627(* Decoding:
628 default_lang = "en"
629 hello = "Hello"
630 goodbye = "Goodbye"
631 thanks = "Thank you"
632
633 All string keys except default_lang go into the strings map.
634*)
635]}
636
637{1:validation Validation and Constraints}
638
639{2 Range Validation with iter}
640
641Use {!Tomlt.iter} to add validation:
642
643{[
644let port_codec =
645 Tomlt.(iter int
646 ~dec:(fun p ->
647 if p < 0 || p > 65535 then
648 failwith "port must be between 0 and 65535"))
649
650let percentage_codec =
651 Tomlt.(iter float
652 ~dec:(fun p ->
653 if p < 0.0 || p > 100.0 then
654 failwith "percentage must be between 0 and 100"))
655]}
656
657{2 String Enumerations}
658
659Use {!Tomlt.enum} for fixed string values:
660
661{[
662type log_level = Debug | Info | Warning | Error
663
664let log_level_codec =
665 Tomlt.enum [
666 "debug", Debug;
667 "info", Info;
668 "warning", Warning;
669 "error", Error;
670 ]
671
672type config = { level : log_level }
673
674let config_codec =
675 Tomlt.(Table.(
676 obj (fun level -> { level })
677 |> mem "level" log_level_codec ~enc:(fun c -> c.level)
678 |> finish
679 ))
680]}
681
682{2 Custom Transformations with map}
683
684Use {!Tomlt.map} to transform between representations:
685
686{[
687(* Store URI as string in TOML *)
688let uri_codec =
689 Tomlt.(map string
690 ~dec:Uri.of_string
691 ~enc:Uri.to_string)
692
693(* Parse comma-separated tags *)
694let tags_codec =
695 Tomlt.(map string
696 ~dec:(String.split_on_char ',')
697 ~enc:(String.concat ","))
698]}
699
700{1:roundtripping Roundtripping TOML}
701
702{2 Preserving Raw Values}
703
704Use {!Tomlt.value} to preserve parts of a document unchanged:
705
706{[
707type partial_config = {
708 version : int;
709 rest : Toml.t; (* preserve everything else *)
710}
711
712(* This requires a different approach - extract version,
713 keep the rest as raw TOML *)
714]}
715
716{2 Preserving Datetime Variants}
717
718Use {!Tomlt.ptime_full} to roundtrip datetime formats:
719
720{[
721type event = {
722 name : string;
723 when_ : Toml.ptime_datetime;
724}
725
726let event_codec =
727 Tomlt.(Table.(
728 obj (fun name when_ -> { name; when_ })
729 |> mem "name" string ~enc:(fun e -> e.name)
730 |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
731 |> finish
732 ))
733
734(* Input: when = 2024-01-15
735 Output: when = 2024-01-15 (not 2024-01-15T00:00:00Z) *)
736]}
737
738{1:error_handling Error Handling}
739
740{2 Result-Based Decoding}
741
742Always use {!Tomlt.decode} in production code:
743
744{[
745let load_config path =
746 match Tomlt_unix.decode_file config_codec path with
747 | Ok config -> config
748 | Error e ->
749 Printf.eprintf "Configuration error: %s\n"
750 (Toml.Error.to_string e);
751 exit 1
752]}
753
754{2 Decoding with Context}
755
756Errors include path information for nested structures:
757
758{[
759(* For deeply nested errors like:
760 [database]
761 port = "not an int"
762
763 The error will indicate:
764 "at database.port: expected int, got string" *)
765]}
766
767{2 Multiple Validation Errors}
768
769For collecting multiple errors, decode fields individually:
770
771{[
772let validate_config toml =
773 let errors = ref [] in
774 let get_field name codec =
775 match Tomlt.(decode (mem name codec) toml) with
776 | Ok v -> Some v
777 | Error e ->
778 errors := (name, e) :: !errors;
779 None
780 in
781 let host = get_field "host" Tomlt.string in
782 let port = get_field "port" Tomlt.int in
783 match !errors with
784 | [] -> Ok { host = Option.get host; port = Option.get port }
785 | errs -> Error errs
786]}
787
788{1:recursion Recursive Types}
789
790Use {!Tomlt.rec'} for self-referential types:
791
792{[
793type tree = Node of int * tree list
794
795let rec tree_codec = lazy Tomlt.(
796 Table.(
797 obj (fun value children -> Node (value, children))
798 |> mem "value" int ~enc:(function Node (v, _) -> v)
799 |> mem "children" (list (rec' tree_codec))
800 ~enc:(function Node (_, cs) -> cs)
801 ~dec_absent:[]
802 |> finish
803 ))
804
805let tree_codec = Lazy.force tree_codec
806]}
807
808{v
809value = 1
810
811[[children]]
812value = 2
813
814[[children]]
815value = 3
816
817[[children.children]]
818value = 4
819v}