···11+{0 Cookbook}
22+33+This cookbook provides patterns and recipes for common TOML tasks.
44+Each section includes both conceptual explanation and working code
55+examples. See {!module:Tomlt} for the full API reference.
66+77+{1:conventions Conventions}
88+99+Throughout this cookbook, we use the following conventions:
1010+1111+- Codec values are named after their OCaml type (e.g., [config_codec]
1212+ for a [config] type)
1313+- The [~enc] parameter always extracts the field from the record
1414+- Codecs are defined using the applicative-style {!Tomlt.Table} builder
1515+1616+{1:config_files Parsing Configuration Files}
1717+1818+The most common use case: parsing a TOML configuration file into an
1919+OCaml record.
2020+2121+{2 Basic Configuration}
2222+2323+{[
2424+type database_config = {
2525+ host : string;
2626+ port : int;
2727+ name : string;
2828+}
2929+3030+let database_config_codec =
3131+ Tomlt.(Table.(
3232+ obj (fun host port name -> { host; port; name })
3333+ |> mem "host" string ~enc:(fun c -> c.host)
3434+ |> mem "port" int ~enc:(fun c -> c.port)
3535+ |> mem "name" string ~enc:(fun c -> c.name)
3636+ |> finish
3737+ ))
3838+]}
3939+4040+This handles TOML like:
4141+4242+{v
4343+host = "localhost"
4444+port = 5432
4545+name = "myapp"
4646+v}
4747+4848+{2 Nested Configuration}
4949+5050+For nested tables, compose codecs:
5151+5252+{[
5353+type server_config = {
5454+ host : string;
5555+ port : int;
5656+}
5757+5858+type app_config = {
5959+ name : string;
6060+ server : server_config;
6161+ debug : bool;
6262+}
6363+6464+let server_config_codec =
6565+ Tomlt.(Table.(
6666+ obj (fun host port -> { host; port })
6767+ |> mem "host" string ~enc:(fun s -> s.host)
6868+ |> mem "port" int ~enc:(fun s -> s.port)
6969+ |> finish
7070+ ))
7171+7272+let app_config_codec =
7373+ Tomlt.(Table.(
7474+ obj (fun name server debug -> { name; server; debug })
7575+ |> mem "name" string ~enc:(fun c -> c.name)
7676+ |> mem "server" server_config_codec ~enc:(fun c -> c.server)
7777+ |> mem "debug" bool ~enc:(fun c -> c.debug)
7878+ |> finish
7979+ ))
8080+]}
8181+8282+This handles:
8383+8484+{v
8585+name = "My Application"
8686+debug = false
8787+8888+[server]
8989+host = "0.0.0.0"
9090+port = 8080
9191+v}
9292+9393+{2 Multi-Environment Configuration}
9494+9595+A pattern for dev/staging/prod configurations:
9696+9797+{[
9898+type env_config = {
9999+ database_url : string;
100100+ log_level : string;
101101+ cache_ttl : int;
102102+}
103103+104104+type config = {
105105+ app_name : string;
106106+ development : env_config;
107107+ production : env_config;
108108+}
109109+110110+let env_config_codec =
111111+ Tomlt.(Table.(
112112+ obj (fun database_url log_level cache_ttl ->
113113+ { database_url; log_level; cache_ttl })
114114+ |> mem "database_url" string ~enc:(fun e -> e.database_url)
115115+ |> mem "log_level" string ~enc:(fun e -> e.log_level)
116116+ |> mem "cache_ttl" int ~enc:(fun e -> e.cache_ttl)
117117+ |> finish
118118+ ))
119119+120120+let config_codec =
121121+ Tomlt.(Table.(
122122+ obj (fun app_name development production ->
123123+ { app_name; development; production })
124124+ |> mem "app_name" string ~enc:(fun c -> c.app_name)
125125+ |> mem "development" env_config_codec ~enc:(fun c -> c.development)
126126+ |> mem "production" env_config_codec ~enc:(fun c -> c.production)
127127+ |> finish
128128+ ))
129129+]}
130130+131131+{1:optional_values Optional and Absent Values}
132132+133133+TOML tables may have optional members. Tomlt provides several ways
134134+to handle missing values.
135135+136136+{2 Default Values with dec_absent}
137137+138138+Use [~dec_absent] to provide a default when a key is missing:
139139+140140+{[
141141+type settings = {
142142+ theme : string;
143143+ font_size : int;
144144+ show_line_numbers : bool;
145145+}
146146+147147+let settings_codec =
148148+ Tomlt.(Table.(
149149+ obj (fun theme font_size show_line_numbers ->
150150+ { theme; font_size; show_line_numbers })
151151+ |> mem "theme" string ~enc:(fun s -> s.theme)
152152+ ~dec_absent:"default"
153153+ |> mem "font_size" int ~enc:(fun s -> s.font_size)
154154+ ~dec_absent:12
155155+ |> mem "show_line_numbers" bool ~enc:(fun s -> s.show_line_numbers)
156156+ ~dec_absent:true
157157+ |> finish
158158+ ))
159159+]}
160160+161161+{v
162162+# All of these work:
163163+theme = "dark"
164164+165165+# Or with defaults:
166166+# (empty table uses all defaults)
167167+v}
168168+169169+{2 Option Types with opt_mem}
170170+171171+Use {!Tomlt.Table.opt_mem} when the absence of a value is meaningful:
172172+173173+{[
174174+type user = {
175175+ name : string;
176176+ email : string option;
177177+ phone : string option;
178178+}
179179+180180+let user_codec =
181181+ Tomlt.(Table.(
182182+ obj (fun name email phone -> { name; email; phone })
183183+ |> mem "name" string ~enc:(fun u -> u.name)
184184+ |> opt_mem "email" string ~enc:(fun u -> u.email)
185185+ |> opt_mem "phone" string ~enc:(fun u -> u.phone)
186186+ |> finish
187187+ ))
188188+]}
189189+190190+On encoding, [None] values are omitted from the output:
191191+192192+{[
193193+(* This user: *)
194194+let user = { name = "Alice"; email = Some "alice@example.com"; phone = None }
195195+196196+(* Encodes to: *)
197197+(* name = "Alice"
198198+ email = "alice@example.com"
199199+ # phone is omitted *)
200200+]}
201201+202202+{2 Conditional Omission with enc_omit}
203203+204204+Use [~enc_omit] to omit values that match a predicate:
205205+206206+{[
207207+type config = {
208208+ name : string;
209209+ retries : int; (* omit if 0 *)
210210+}
211211+212212+let config_codec =
213213+ Tomlt.(Table.(
214214+ obj (fun name retries -> { name; retries })
215215+ |> mem "name" string ~enc:(fun c -> c.name)
216216+ |> mem "retries" int ~enc:(fun c -> c.retries)
217217+ ~dec_absent:0
218218+ ~enc_omit:(fun r -> r = 0)
219219+ |> finish
220220+ ))
221221+]}
222222+223223+{1:datetimes Working with Datetimes}
224224+225225+TOML 1.1 supports four datetime formats. Tomlt provides Ptime-based
226226+codecs that handle all of them.
227227+228228+{2 TOML Datetime Formats}
229229+230230+{v
231231+# Offset datetime - full timestamp with timezone (unambiguous)
232232+published = 2024-01-15T10:30:00Z
233233+published = 2024-01-15T10:30:00-05:00
234234+235235+# Local datetime - no timezone (wall clock time)
236236+meeting = 2024-01-15T10:30:00
237237+238238+# Local date - date only
239239+birthday = 1979-05-27
240240+241241+# Local time - time only
242242+alarm = 07:30:00
243243+v}
244244+245245+{2 Basic Datetime Handling}
246246+247247+Use {!Tomlt.ptime} to accept any datetime format and normalize to
248248+[Ptime.t]:
249249+250250+{[
251251+type event = { name : string; timestamp : Ptime.t }
252252+253253+let event_codec =
254254+ Tomlt.(Table.(
255255+ obj (fun name timestamp -> { name; timestamp })
256256+ |> mem "name" string ~enc:(fun e -> e.name)
257257+ |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp)
258258+ |> finish
259259+ ))
260260+261261+(* All of these decode successfully: *)
262262+(* when = 2024-01-15T10:30:00Z -> offset datetime *)
263263+(* when = 2024-01-15T10:30:00 -> local datetime *)
264264+(* when = 2024-01-15 -> date only (midnight) *)
265265+(* when = 10:30:00 -> time only (today) *)
266266+]}
267267+268268+{2 Strict Timestamp Validation}
269269+270270+Use {!Tomlt.ptime_opt} when you require explicit timezone information:
271271+272272+{[
273273+type audit_log = { action : string; timestamp : Ptime.t }
274274+275275+let audit_codec =
276276+ Tomlt.(Table.(
277277+ obj (fun action timestamp -> { action; timestamp })
278278+ |> mem "action" string ~enc:(fun a -> a.action)
279279+ |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp)
280280+ |> finish
281281+ ))
282282+283283+(* Valid: timestamp = 2024-01-15T10:30:00Z *)
284284+(* Valid: timestamp = 2024-01-15T10:30:00+05:30 *)
285285+(* Invalid: timestamp = 2024-01-15T10:30:00 (no timezone) *)
286286+(* Invalid: timestamp = 2024-01-15 (date only) *)
287287+]}
288288+289289+{2 Date-Only Fields}
290290+291291+Use {!Tomlt.ptime_date} for fields that should only contain dates:
292292+293293+{[
294294+type person = { name : string; birthday : Ptime.date }
295295+296296+let person_codec =
297297+ Tomlt.(Table.(
298298+ obj (fun name birthday -> { name; birthday })
299299+ |> mem "name" string ~enc:(fun p -> p.name)
300300+ |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday)
301301+ |> finish
302302+ ))
303303+304304+(* birthday = 1979-05-27 -> (1979, 5, 27) *)
305305+]}
306306+307307+{2 Time-Only Fields}
308308+309309+Use {!Tomlt.ptime_span} for recurring times (as duration from midnight):
310310+311311+{[
312312+type alarm = { label : string; time : Ptime.Span.t }
313313+314314+let alarm_codec =
315315+ Tomlt.(Table.(
316316+ obj (fun label time -> { label; time })
317317+ |> mem "label" string ~enc:(fun a -> a.label)
318318+ |> mem "time" ptime_span ~enc:(fun a -> a.time)
319319+ |> finish
320320+ ))
321321+322322+(* time = 07:30:00 -> 27000 seconds (7.5 hours from midnight) *)
323323+]}
324324+325325+{2 Preserving Datetime Format}
326326+327327+Use {!Tomlt.ptime_full} to preserve the exact datetime variant for
328328+roundtripping:
329329+330330+{[
331331+type flexible_event = {
332332+ name : string;
333333+ when_ : Toml.ptime_datetime;
334334+}
335335+336336+let flexible_codec =
337337+ Tomlt.(Table.(
338338+ obj (fun name when_ -> { name; when_ })
339339+ |> mem "name" string ~enc:(fun e -> e.name)
340340+ |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
341341+ |> finish
342342+ ))
343343+344344+(* Decoding preserves the variant:
345345+ when = 2024-01-15T10:30:00Z -> `Datetime (ptime, Some 0)
346346+ when = 2024-01-15T10:30:00 -> `Datetime_local ptime
347347+ when = 2024-01-15 -> `Date (2024, 1, 15)
348348+ when = 10:30:00 -> `Time (10, 30, 0, 0)
349349+350350+ Encoding reproduces the original format. *)
351351+]}
352352+353353+{2 Timezone Handling}
354354+355355+For local datetimes without explicit timezone, you can specify how
356356+to interpret them:
357357+358358+{[
359359+(* Force UTC interpretation *)
360360+let utc_codec = Tomlt.ptime ~tz_offset_s:0 ()
361361+362362+(* Force Eastern Time (-05:00 = -18000 seconds) *)
363363+let eastern_codec = Tomlt.ptime ~tz_offset_s:(-18000) ()
364364+365365+(* Use system timezone (requires Tomlt_unix) *)
366366+let system_codec =
367367+ Tomlt.ptime ~get_tz:Tomlt_unix.current_tz_offset_s ()
368368+]}
369369+370370+{1:arrays Working with Arrays}
371371+372372+TOML 1.1 supports heterogeneous arrays, but most use cases involve
373373+homogeneous arrays of a single type.
374374+375375+{2 Basic Arrays}
376376+377377+{[
378378+type config = {
379379+ name : string;
380380+ ports : int list;
381381+ hosts : string list;
382382+}
383383+384384+let config_codec =
385385+ Tomlt.(Table.(
386386+ obj (fun name ports hosts -> { name; ports; hosts })
387387+ |> mem "name" string ~enc:(fun c -> c.name)
388388+ |> mem "ports" (list int) ~enc:(fun c -> c.ports)
389389+ |> mem "hosts" (list string) ~enc:(fun c -> c.hosts)
390390+ |> finish
391391+ ))
392392+]}
393393+394394+{v
395395+name = "load-balancer"
396396+ports = [80, 443, 8080]
397397+hosts = ["web1.example.com", "web2.example.com"]
398398+v}
399399+400400+{2 Arrays of Tables}
401401+402402+Use {!Tomlt.array_of_tables} for TOML's [[[name]]] syntax:
403403+404404+{[
405405+type product = { name : string; price : float }
406406+type catalog = { products : product list }
407407+408408+let product_codec =
409409+ Tomlt.(Table.(
410410+ obj (fun name price -> { name; price })
411411+ |> mem "name" string ~enc:(fun p -> p.name)
412412+ |> mem "price" float ~enc:(fun p -> p.price)
413413+ |> finish
414414+ ))
415415+416416+let catalog_codec =
417417+ Tomlt.(Table.(
418418+ obj (fun products -> { products })
419419+ |> mem "products" (array_of_tables product_codec)
420420+ ~enc:(fun c -> c.products)
421421+ |> finish
422422+ ))
423423+]}
424424+425425+{v
426426+[[products]]
427427+name = "Widget"
428428+price = 9.99
429429+430430+[[products]]
431431+name = "Gadget"
432432+price = 19.99
433433+v}
434434+435435+{2 Nested Arrays}
436436+437437+Arrays can contain other arrays:
438438+439439+{[
440440+type matrix = { rows : int list list }
441441+442442+let matrix_codec =
443443+ Tomlt.(Table.(
444444+ obj (fun rows -> { rows })
445445+ |> mem "rows" (list (list int)) ~enc:(fun m -> m.rows)
446446+ |> finish
447447+ ))
448448+]}
449449+450450+{v
451451+rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
452452+v}
453453+454454+{2 Custom Array Types}
455455+456456+Use {!val:Tomlt.Array.map} to decode into custom collection types:
457457+458458+{[
459459+module IntSet = Set.Make(Int)
460460+461461+let int_set_codec =
462462+ Tomlt.Array.(
463463+ map int
464464+ ~dec_empty:(fun () -> IntSet.empty)
465465+ ~dec_add:(fun x acc -> IntSet.add x acc)
466466+ ~dec_finish:(fun acc -> acc)
467467+ ~enc:{ fold = (fun f acc set -> IntSet.fold (fun x a -> f a x) set acc) }
468468+ |> finish
469469+ )
470470+]}
471471+472472+{1:tables Nested Tables and Objects}
473473+474474+{2 Inline Tables}
475475+476476+Use {!Tomlt.Table.inline} to encode as inline tables:
477477+478478+{[
479479+type point = { x : int; y : int }
480480+481481+let point_codec =
482482+ Tomlt.(Table.(
483483+ obj (fun x y -> { x; y })
484484+ |> mem "x" int ~enc:(fun p -> p.x)
485485+ |> mem "y" int ~enc:(fun p -> p.y)
486486+ |> inline (* <- produces inline table *)
487487+ ))
488488+489489+(* Encodes as: point = { x = 10, y = 20 } *)
490490+(* Instead of:
491491+ [point]
492492+ x = 10
493493+ y = 20 *)
494494+]}
495495+496496+{2 Deeply Nested Structures}
497497+498498+{[
499499+type address = { street : string; city : string }
500500+type company = { name : string; address : address }
501501+type employee = { name : string; company : company }
502502+503503+let address_codec =
504504+ Tomlt.(Table.(
505505+ obj (fun street city -> { street; city })
506506+ |> mem "street" string ~enc:(fun a -> a.street)
507507+ |> mem "city" string ~enc:(fun a -> a.city)
508508+ |> finish
509509+ ))
510510+511511+let company_codec =
512512+ Tomlt.(Table.(
513513+ obj (fun name address -> { name; address })
514514+ |> mem "name" string ~enc:(fun c -> c.name)
515515+ |> mem "address" address_codec ~enc:(fun c -> c.address)
516516+ |> finish
517517+ ))
518518+519519+let employee_codec =
520520+ Tomlt.(Table.(
521521+ obj (fun name company -> { name; company })
522522+ |> mem "name" string ~enc:(fun e -> e.name)
523523+ |> mem "company" company_codec ~enc:(fun e -> e.company)
524524+ |> finish
525525+ ))
526526+]}
527527+528528+{v
529529+name = "Alice"
530530+531531+[company]
532532+name = "Acme Corp"
533533+534534+[company.address]
535535+street = "123 Main St"
536536+city = "Springfield"
537537+v}
538538+539539+{1:unknown_members Unknown Member Handling}
540540+541541+By default, unknown members in TOML tables are ignored. You can
542542+change this behavior.
543543+544544+{2 Ignoring Unknown Members (Default)}
545545+546546+{[
547547+let config_codec =
548548+ Tomlt.(Table.(
549549+ obj (fun host -> host)
550550+ |> mem "host" string ~enc:Fun.id
551551+ |> skip_unknown (* default, can be omitted *)
552552+ |> finish
553553+ ))
554554+555555+(* This works even with extra keys: *)
556556+(* host = "localhost"
557557+ unknown_key = "ignored" *)
558558+]}
559559+560560+{2 Rejecting Unknown Members}
561561+562562+Use {!Tomlt.Table.error_unknown} for strict parsing:
563563+564564+{[
565565+let strict_config_codec =
566566+ Tomlt.(Table.(
567567+ obj (fun host port -> (host, port))
568568+ |> mem "host" string ~enc:fst
569569+ |> mem "port" int ~enc:snd
570570+ |> error_unknown (* <- rejects unknown keys *)
571571+ |> finish
572572+ ))
573573+574574+(* Error on: host = "localhost"
575575+ port = 8080
576576+ typo = "oops" <- causes error *)
577577+]}
578578+579579+{2 Collecting Unknown Members}
580580+581581+Use {!Tomlt.Table.keep_unknown} to preserve unknown members:
582582+583583+{[
584584+type config = {
585585+ name : string;
586586+ extra : (string * Toml.t) list;
587587+}
588588+589589+let config_codec =
590590+ Tomlt.(Table.(
591591+ obj (fun name extra -> { name; extra })
592592+ |> mem "name" string ~enc:(fun c -> c.name)
593593+ |> keep_unknown (Mems.assoc value) ~enc:(fun c -> c.extra)
594594+ |> finish
595595+ ))
596596+597597+(* Decoding:
598598+ name = "app"
599599+ foo = 42
600600+ bar = "hello"
601601+602602+ Results in:
603603+ { name = "app"; extra = [("foo", Int 42L); ("bar", String "hello")] }
604604+*)
605605+]}
606606+607607+{2 Typed Unknown Members}
608608+609609+Collect unknown members with a specific type:
610610+611611+{[
612612+module StringMap = Map.Make(String)
613613+614614+type translations = {
615615+ default_lang : string;
616616+ strings : string StringMap.t;
617617+}
618618+619619+let translations_codec =
620620+ Tomlt.(Table.(
621621+ obj (fun default_lang strings -> { default_lang; strings })
622622+ |> mem "default_lang" string ~enc:(fun t -> t.default_lang)
623623+ |> keep_unknown (Mems.string_map string) ~enc:(fun t -> t.strings)
624624+ |> finish
625625+ ))
626626+627627+(* Decoding:
628628+ default_lang = "en"
629629+ hello = "Hello"
630630+ goodbye = "Goodbye"
631631+ thanks = "Thank you"
632632+633633+ All string keys except default_lang go into the strings map.
634634+*)
635635+]}
636636+637637+{1:validation Validation and Constraints}
638638+639639+{2 Range Validation with iter}
640640+641641+Use {!Tomlt.iter} to add validation:
642642+643643+{[
644644+let port_codec =
645645+ Tomlt.(iter int
646646+ ~dec:(fun p ->
647647+ if p < 0 || p > 65535 then
648648+ failwith "port must be between 0 and 65535"))
649649+650650+let percentage_codec =
651651+ Tomlt.(iter float
652652+ ~dec:(fun p ->
653653+ if p < 0.0 || p > 100.0 then
654654+ failwith "percentage must be between 0 and 100"))
655655+]}
656656+657657+{2 String Enumerations}
658658+659659+Use {!Tomlt.enum} for fixed string values:
660660+661661+{[
662662+type log_level = Debug | Info | Warning | Error
663663+664664+let log_level_codec =
665665+ Tomlt.enum [
666666+ "debug", Debug;
667667+ "info", Info;
668668+ "warning", Warning;
669669+ "error", Error;
670670+ ]
671671+672672+type config = { level : log_level }
673673+674674+let config_codec =
675675+ Tomlt.(Table.(
676676+ obj (fun level -> { level })
677677+ |> mem "level" log_level_codec ~enc:(fun c -> c.level)
678678+ |> finish
679679+ ))
680680+]}
681681+682682+{2 Custom Transformations with map}
683683+684684+Use {!Tomlt.map} to transform between representations:
685685+686686+{[
687687+(* Store URI as string in TOML *)
688688+let uri_codec =
689689+ Tomlt.(map string
690690+ ~dec:Uri.of_string
691691+ ~enc:Uri.to_string)
692692+693693+(* Parse comma-separated tags *)
694694+let tags_codec =
695695+ Tomlt.(map string
696696+ ~dec:(String.split_on_char ',')
697697+ ~enc:(String.concat ","))
698698+]}
699699+700700+{1:roundtripping Roundtripping TOML}
701701+702702+{2 Preserving Raw Values}
703703+704704+Use {!Tomlt.value} to preserve parts of a document unchanged:
705705+706706+{[
707707+type partial_config = {
708708+ version : int;
709709+ rest : Toml.t; (* preserve everything else *)
710710+}
711711+712712+(* This requires a different approach - extract version,
713713+ keep the rest as raw TOML *)
714714+]}
715715+716716+{2 Preserving Datetime Variants}
717717+718718+Use {!Tomlt.ptime_full} to roundtrip datetime formats:
719719+720720+{[
721721+type event = {
722722+ name : string;
723723+ when_ : Toml.ptime_datetime;
724724+}
725725+726726+let event_codec =
727727+ Tomlt.(Table.(
728728+ obj (fun name when_ -> { name; when_ })
729729+ |> mem "name" string ~enc:(fun e -> e.name)
730730+ |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
731731+ |> finish
732732+ ))
733733+734734+(* Input: when = 2024-01-15
735735+ Output: when = 2024-01-15 (not 2024-01-15T00:00:00Z) *)
736736+]}
737737+738738+{1:error_handling Error Handling}
739739+740740+{2 Result-Based Decoding}
741741+742742+Always use {!Tomlt.decode} in production code:
743743+744744+{[
745745+let load_config path =
746746+ match Tomlt_unix.decode_file config_codec path with
747747+ | Ok config -> config
748748+ | Error e ->
749749+ Printf.eprintf "Configuration error: %s\n"
750750+ (Toml.Error.to_string e);
751751+ exit 1
752752+]}
753753+754754+{2 Decoding with Context}
755755+756756+Errors include path information for nested structures:
757757+758758+{[
759759+(* For deeply nested errors like:
760760+ [database]
761761+ port = "not an int"
762762+763763+ The error will indicate:
764764+ "at database.port: expected int, got string" *)
765765+]}
766766+767767+{2 Multiple Validation Errors}
768768+769769+For collecting multiple errors, decode fields individually:
770770+771771+{[
772772+let validate_config toml =
773773+ let errors = ref [] in
774774+ let get_field name codec =
775775+ match Tomlt.(decode (mem name codec) toml) with
776776+ | Ok v -> Some v
777777+ | Error e ->
778778+ errors := (name, e) :: !errors;
779779+ None
780780+ in
781781+ let host = get_field "host" Tomlt.string in
782782+ let port = get_field "port" Tomlt.int in
783783+ match !errors with
784784+ | [] -> Ok { host = Option.get host; port = Option.get port }
785785+ | errs -> Error errs
786786+]}
787787+788788+{1:recursion Recursive Types}
789789+790790+Use {!Tomlt.rec'} for self-referential types:
791791+792792+{[
793793+type tree = Node of int * tree list
794794+795795+let rec tree_codec = lazy Tomlt.(
796796+ Table.(
797797+ obj (fun value children -> Node (value, children))
798798+ |> mem "value" int ~enc:(function Node (v, _) -> v)
799799+ |> mem "children" (list (rec' tree_codec))
800800+ ~enc:(function Node (_, cs) -> cs)
801801+ ~dec_absent:[]
802802+ |> finish
803803+ ))
804804+805805+let tree_codec = Lazy.force tree_codec
806806+]}
807807+808808+{v
809809+value = 1
810810+811811+[[children]]
812812+value = 2
813813+814814+[[children]]
815815+value = 3
816816+817817+[[children.children]]
818818+value = 4
819819+v}
+3
doc/dune
···11+(documentation
22+ (package tomlt)
33+ (mld_files index cookbook))
+71
doc/index.mld
···11+{0 Tomlt}
22+33+{1 TOML 1.1 Codec Library}
44+55+Tomlt is a bidirectional codec library for {{:https://toml.io/en/v1.1.0}TOML 1.1}
66+configuration files. It provides type-safe encoding and decoding between
77+OCaml types and TOML values.
88+99+{2 Quick Start}
1010+1111+Define a codec for your configuration type:
1212+1313+{[
1414+type config = { host : string; port : int; debug : bool }
1515+1616+let config_codec =
1717+ Tomlt.(Table.(
1818+ obj (fun host port debug -> { host; port; debug })
1919+ |> mem "host" string ~enc:(fun c -> c.host)
2020+ |> mem "port" int ~enc:(fun c -> c.port)
2121+ |> mem "debug" bool ~enc:(fun c -> c.debug) ~dec_absent:false
2222+ |> finish
2323+ ))
2424+]}
2525+2626+Parse and use it:
2727+2828+{[
2929+let () =
3030+ match Tomlt_bytesrw.decode_string config_codec {|
3131+ host = "localhost"
3232+ port = 8080
3333+ |} with
3434+ | Ok config -> Printf.printf "Host: %s\n" config.host
3535+ | Error e -> prerr_endline (Toml.Error.to_string e)
3636+]}
3737+3838+{2 Library Structure}
3939+4040+- {!Tomlt.Toml} - Core TOML value types and operations
4141+- {!Tomlt} - Codec combinators for bidirectional TOML encoding/decoding
4242+- {!Tomlt_bytesrw} - Streaming parser and encoder
4343+- {!Tomlt_eio} - Eio-native I/O integration
4444+- {!Tomlt_unix} - Unix I/O integration
4545+- {!Tomlt_jsont} - JSON codec for toml-test format
4646+4747+{2 Cookbook}
4848+4949+The {{!page-cookbook}cookbook} provides patterns and recipes for common
5050+TOML scenarios:
5151+5252+- {{!page-cookbook.config_files}Parsing configuration files}
5353+- {{!page-cookbook.optional_values}Optional and absent values}
5454+- {{!page-cookbook.datetimes}Working with datetimes}
5555+- {{!page-cookbook.arrays}Working with arrays}
5656+- {{!page-cookbook.tables}Nested tables and objects}
5757+- {{!page-cookbook.unknown_members}Unknown member handling}
5858+- {{!page-cookbook.validation}Validation and constraints}
5959+- {{!page-cookbook.roundtripping}Roundtripping TOML}
6060+- {{!page-cookbook.error_handling}Error handling}
6161+6262+{2 Design}
6363+6464+Tomlt is inspired by {{:https://erratique.ch/software/jsont}Jsont}'s approach
6565+to JSON codecs. Each codec ['a Tomlt.t] defines both:
6666+6767+- A decoder: [Toml.t -> ('a, error) result]
6868+- An encoder: ['a -> Toml.t]
6969+7070+Codecs compose through combinators, allowing complex types to be built
7171+from simple primitives while maintaining bidirectionality.
+9-7
lib/toml.mli
···7788 This module provides the core TOML value type and operations for
99 constructing, accessing, and manipulating TOML data. For parsing and
1010- encoding, see {!Tomlt_bytesrw}.
1010+ encoding, see {!Tomlt_bytesrw}. For codec-based bidirectional encoding,
1111+ see {!Tomlt}.
11121213 {2 Quick Start}
1314···24252526 Access values:
2627 {[
2727- let host = Toml.(config.%{["database"; "host"]} |> to_string)
2828- let port = Toml.(config.%{["database"; "ports"]} |> to_array |> List.hd |> to_int)
2828+ let host = Toml.to_string (Toml.find "host" (Toml.find "database" config))
2929+ let ports = Toml.to_array (Toml.find "ports" (Toml.find "database" config))
3030+ let port = Toml.to_int (List.hd ports)
2931 ]}
3232+3333+ See the {{!page-cookbook}cookbook} for common patterns and recipes.
30343135 {2 Module Overview}
3236···370374371375val pp_value : Format.formatter -> t -> unit
372376(** [pp_value fmt t] pretty-prints a single TOML value.
373373- Same as {!pp}. *)
377377+ Same as {!val:pp}. *)
374378375379val equal : t -> t -> bool
376380(** [equal a b] is structural equality on TOML values.
···382386(** {1:errors Error Handling} *)
383387384388module Error = Toml_error
385385-(** Structured error types for TOML parsing and encoding.
386386-387387- See {!Toml_error} for detailed documentation. *)
389389+(** Structured error types for TOML parsing and encoding. *)
+54-378
lib/tomlt.mli
···4444 Codecs compose through combinators to build complex types from
4545 simple primitives.
46464747- {2 Datetime Handling}
4848-4949- Tomlt uses {{:https://erratique.ch/software/ptime}Ptime} for all datetime
5050- operations, providing a unified approach to TOML's four datetime formats:
4747+ {2 Cookbook}
51485252- {v
5353- (* Accept any TOML datetime format, normalize to Ptime.t *)
5454- type event = { name : string; when_ : Ptime.t }
4949+ See the {{!page-cookbook}cookbook} for patterns and recipes:
55505656- let event_codec = Tomlt.(Table.(
5757- obj (fun name when_ -> { name; when_ })
5858- |> mem "name" string ~enc:(fun e -> e.name)
5959- |> mem "when" (ptime ()) ~enc:(fun e -> e.when_)
6060- |> finish
6161- ))
6262-6363- (* All of these work: *)
6464- (* when = 2024-01-15T10:30:00Z -> offset datetime *)
6565- (* when = 2024-01-15T10:30:00 -> local datetime (uses system tz) *)
6666- (* when = 2024-01-15 -> date only (assumes midnight) *)
6767- (* when = 10:30:00 -> time only (uses today's date) *)
6868- v}
6969-7070- See {!section:ptime_codecs} for the complete datetime codec API.
5151+ - {{!page-cookbook.config_files}Parsing configuration files}
5252+ - {{!page-cookbook.optional_values}Optional and absent values}
5353+ - {{!page-cookbook.datetimes}Working with datetimes}
5454+ - {{!page-cookbook.arrays}Working with arrays}
5555+ - {{!page-cookbook.tables}Nested tables and objects}
5656+ - {{!page-cookbook.unknown_members}Unknown member handling}
5757+ - {{!page-cookbook.validation}Validation and constraints}
71587259 {2 Module Overview}
7360···340327341328(** {1:ptime_codecs Ptime Datetime Codecs}
342329343343- Tomlt provides a unified datetime handling system built on
344344- {{:https://erratique.ch/software/ptime}Ptime}. All
345345- {{:https://toml.io/en/v1.1.0#offset-date-time}TOML datetime formats}
346346- can be decoded to [Ptime.t] timestamps with sensible defaults for
347347- incomplete information.
348348-349349- {2 TOML Datetime Formats}
350350-351351- {{:https://toml.io/en/v1.1.0}TOML 1.1} supports four datetime formats
352352- with varying levels of precision:
353353-354354- {v
355355- # Offset datetime - full timestamp with timezone (unambiguous)
356356- # See: https://toml.io/en/v1.1.0#offset-date-time
357357- published = 2024-01-15T10:30:00Z
358358- published = 2024-01-15T10:30:00-05:00
330330+ Tomlt provides unified datetime handling using
331331+ {{:https://erratique.ch/software/ptime}Ptime}. All TOML datetime formats
332332+ can be decoded to [Ptime.t] timestamps.
359333360360- # Local datetime - no timezone (wall clock time)
361361- # See: https://toml.io/en/v1.1.0#local-date-time
362362- meeting = 2024-01-15T10:30:00
363363-364364- # Local date - date only
365365- # See: https://toml.io/en/v1.1.0#local-date
366366- birthday = 1979-05-27
367367-368368- # Local time - time only
369369- # See: https://toml.io/en/v1.1.0#local-time
370370- alarm = 07:30:00
371371- v}
334334+ See the {{!page-cookbook.datetimes}cookbook} for detailed patterns
335335+ and examples.
372336373337 {2 Choosing a Codec}
374338375375- - {!val:ptime} - {b Recommended for most cases.} Accepts any datetime format
376376- and normalizes to [Ptime.t] by filling in sensible defaults.
377377-378378- - {!val:ptime_opt} - {b For strict validation.} Only accepts offset datetimes
379379- with explicit timezone. Rejects ambiguous local formats.
380380-381381- - {!val:ptime_date} - For fields that should only contain dates.
382382-383383- - {!val:ptime_span} - For fields that should only contain times (as duration).
384384-385385- - {!val:ptime_full} - {b For roundtripping.} Preserves the exact datetime
386386- variant from the source, allowing faithful re-encoding.
387387-388388- {2 Timezone Handling}
389389-390390- For local datetimes without explicit timezone, Tomlt uses
391391- [Ptime_clock.current_tz_offset_s ()] to get the system timezone.
392392- You can override this by passing [~tz_offset_s]:
393393-394394- {v
395395- (* Force UTC interpretation for local datetimes *)
396396- let codec = ptime ~tz_offset_s:0 ()
397397-398398- (* Force Eastern Time (-05:00 = -18000 seconds) *)
399399- let codec = ptime ~tz_offset_s:(-18000) ()
400400- v}
401401-402402- {2 Examples}
403403-404404- {3 Basic Event Tracking}
405405- {v
406406- type event = { name : string; timestamp : Ptime.t }
407407-408408- let event_codec = Tomlt.(Table.(
409409- obj (fun name timestamp -> { name; timestamp })
410410- |> mem "name" string ~enc:(fun e -> e.name)
411411- |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp)
412412- |> finish
413413- ))
414414-415415- (* All of these decode successfully: *)
416416- (* when = 2024-01-15T10:30:00Z *)
417417- (* when = 2024-01-15T10:30:00 *)
418418- (* when = 2024-01-15 *)
419419- (* when = 10:30:00 *)
420420- v}
421421-422422- {3 Strict Timestamp Validation}
423423- {v
424424- type log_entry = { message : string; timestamp : Ptime.t }
425425-426426- let log_codec = Tomlt.(Table.(
427427- obj (fun message timestamp -> { message; timestamp })
428428- |> mem "message" string ~enc:(fun e -> e.message)
429429- |> mem "timestamp" (ptime_opt ()) ~enc:(fun e -> e.timestamp)
430430- |> finish
431431- ))
432432-433433- (* Only accepts: timestamp = 2024-01-15T10:30:00Z *)
434434- (* Rejects: timestamp = 2024-01-15T10:30:00 *)
435435- v}
436436-437437- {3 Birthday (Date Only)}
438438- {v
439439- type person = { name : string; birthday : Ptime.date }
440440-441441- let person_codec = Tomlt.(Table.(
442442- obj (fun name birthday -> { name; birthday })
443443- |> mem "name" string ~enc:(fun p -> p.name)
444444- |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday)
445445- |> finish
446446- ))
447447-448448- (* birthday = 1979-05-27 -> (1979, 5, 27) *)
449449- v}
450450-451451- {3 Daily Alarm (Time Only)}
452452- {v
453453- type alarm = { label : string; time : Ptime.Span.t }
454454-455455- let alarm_codec = Tomlt.(Table.(
456456- obj (fun label time -> { label; time })
457457- |> mem "label" string ~enc:(fun a -> a.label)
458458- |> mem "time" ptime_span ~enc:(fun a -> a.time)
459459- |> finish
460460- ))
461461-462462- (* time = 07:30:00 -> 27000 seconds (7.5 hours from midnight) *)
463463- v}
464464-465465- {3 Preserving Datetime Format}
466466- {v
467467- type flexible_event = {
468468- name : string;
469469- when_ : Toml.ptime_datetime;
470470- }
471471-472472- let flexible_codec = Tomlt.(Table.(
473473- obj (fun name when_ -> { name; when_ })
474474- |> mem "name" string ~enc:(fun e -> e.name)
475475- |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
476476- |> finish
477477- ))
478478-479479- (* Decoding preserves the variant:
480480- when = 2024-01-15T10:30:00Z -> `Datetime (ptime, Some 0)
481481- when = 2024-01-15T10:30:00 -> `Datetime_local ptime
482482- when = 2024-01-15 -> `Date (2024, 1, 15)
483483- when = 10:30:00 -> `Time (10, 30, 0, 0)
484484-485485- Encoding reproduces the original format. *)
486486- v} *)
339339+ - {!val:ptime} - Accepts any datetime format, normalizes to [Ptime.t]
340340+ - {!val:ptime_opt} - Strict: only accepts offset datetimes with timezone
341341+ - {!val:ptime_date} - For date-only fields
342342+ - {!val:ptime_span} - For time-only fields (as duration from midnight)
343343+ - {!val:ptime_full} - Preserves exact variant for roundtripping *)
487344488345val ptime :
489346 ?tz_offset_s:int ->
···493350 unit -> Ptime.t t
494351(** Datetime codec that converts any TOML datetime to {!Ptime.t}.
495352496496- This is the recommended codec for most datetime use cases. It handles
497497- all TOML datetime variants by filling in sensible defaults:
498498-499499- - {b Offset datetime} ([2024-01-15T10:30:00Z]): Parsed directly to [Ptime.t]
500500- - {b Local datetime} ([2024-01-15T10:30:00]): Converted using the timezone
501501- - {b Local date} ([2024-01-15]): Assumed to be midnight (00:00:00) in the
502502- given timezone
503503- - {b Local time} ([10:30:00]): Combined with today's date using [now]
504504-505505- Encoding always produces an RFC 3339 offset datetime string.
506506-507507- {4 Parameters}
508508-509509- @param tz_offset_s Explicit timezone offset in seconds, used for:
510510- - Converting local datetimes to [Ptime.t]
511511- - Converting local dates to [Ptime.t] (at midnight)
512512- - Converting local times to [Ptime.t] (on today's date)
513513- - Formatting the timezone when encoding
514514-515515- Common values:
516516- - [0] = UTC
517517- - [3600] = +01:00 (Central European Time)
518518- - [-18000] = -05:00 (Eastern Standard Time)
519519- - [-28800] = -08:00 (Pacific Standard Time)
520520-521521- If not provided, [get_tz] is called. If neither is provided, defaults
522522- to UTC (0).
523523-524524- @param get_tz Function to get the current timezone offset. Called when
525525- [tz_offset_s] is not provided. Pass [Tomlt_unix.current_tz_offset_s]
526526- for OS-specific timezone support:
527527- {[let codec = ptime ~get_tz:Tomlt_unix.current_tz_offset_s ()]}
528528-529529- @param now Function to get the current time. Used when decoding local
530530- times (e.g., [10:30:00]) to combine with today's date. Pass
531531- [Tomlt_unix.now] for OS-specific time support. If not provided,
532532- defaults to [Ptime.epoch] (1970-01-01).
533533-534534- @param frac_s Number of fractional second digits to include when encoding.
535535- Range: 0-12. Default: 0 (whole seconds only). For example, [~frac_s:3]
536536- produces [2024-01-15T10:30:00.123Z].
353353+ Handles all TOML datetime variants by filling in sensible defaults.
354354+ Encoding produces RFC 3339 offset datetime strings.
537355538538- {4 Example}
539539- {[
540540- type event = { name : string; timestamp : Ptime.t }
356356+ See {{!page-cookbook.datetimes}Working with datetimes} for examples.
541357542542- let event_codec = Tomlt.(Table.(
543543- obj (fun name timestamp -> { name; timestamp })
544544- |> mem "name" string ~enc:(fun e -> e.name)
545545- |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp)
546546- |> finish
547547- ))
548548-549549- (* All of these decode to a Ptime.t: *)
550550- let e1 = decode_string_exn event_codec {|name="a" when=2024-01-15T10:30:00Z|}
551551- let e2 = decode_string_exn event_codec {|name="b" when=2024-01-15T10:30:00|}
552552- let e3 = decode_string_exn event_codec {|name="c" when=2024-01-15|}
553553- let e4 = decode_string_exn event_codec {|name="d" when=10:30:00|}
554554- ]} *)
358358+ @param tz_offset_s Timezone offset in seconds for local datetimes.
359359+ Common: [0] (UTC), [3600] (+01:00), [-18000] (-05:00).
360360+ @param get_tz Function to get timezone offset when [tz_offset_s]
361361+ not provided. Use [Tomlt_unix.current_tz_offset_s] for system timezone.
362362+ @param now Function for current time, used for time-only values.
363363+ Use [Tomlt_unix.now] for system time.
364364+ @param frac_s Fractional second digits (0-12) for encoding. *)
555365556366val ptime_opt : ?tz_offset_s:int -> ?frac_s:int -> unit -> Ptime.t t
557367(** Strict datetime codec that only accepts offset datetimes.
558368559559- Unlike {!ptime} which accepts any datetime format, this codec requires
560560- an explicit timezone and rejects local datetime variants. Use this when
561561- you need unambiguous timestamps and want to reject values that would
562562- require timezone assumptions.
369369+ Requires explicit timezone; rejects local datetimes, dates, and times.
370370+ Use when you need unambiguous timestamps.
563371564564- {4 Accepted}
565565- - [2024-01-15T10:30:00Z] (UTC)
566566- - [2024-01-15T10:30:00+05:30] (explicit offset)
567567- - [2024-01-15T10:30:00-08:00] (explicit offset)
568568-569569- {4 Rejected}
570570-571571- These raise [Value_error]:
572572-573573- - [2024-01-15T10:30:00] (local datetime - no timezone)
574574- - [2024-01-15] (local date)
575575- - [10:30:00] (local time)
372372+ See {{!page-cookbook.datetimes}Working with datetimes} for examples.
576373577374 @param tz_offset_s Timezone offset for encoding. Default: 0 (UTC).
578578- @param frac_s Fractional second digits for encoding. Default: 0.
579579-580580- {4 Example}
581581- {[
582582- type audit_log = { action : string; timestamp : Ptime.t }
583583-584584- let audit_codec = Tomlt.(Table.(
585585- obj (fun action timestamp -> { action; timestamp })
586586- |> mem "action" string ~enc:(fun a -> a.action)
587587- |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp)
588588- |> finish
589589- ))
590590-591591- (* Valid: timestamp = 2024-01-15T10:30:00Z *)
592592- (* Error: timestamp = 2024-01-15T10:30:00 (no timezone) *)
593593- ]} *)
375375+ @param frac_s Fractional second digits for encoding. Default: 0. *)
594376595377val ptime_span : Ptime.Span.t t
596378(** Codec for TOML local times as [Ptime.Span.t] (duration from midnight).
597379598598- Decodes a local time like [07:32:00] or [14:30:45.123] to a [Ptime.Span.t]
599599- representing the time elapsed since midnight (00:00:00).
600600-601601- When encoding, the span is formatted as a local time string. Values are
602602- clamped to the range [00:00:00] to [23:59:59.999999999].
603603-604604- {4 Decoding}
605605- - [07:32:00] -> 27120 seconds (7 hours, 32 minutes)
606606- - [14:30:45.5] -> 52245.5 seconds
607607- - [00:00:00] -> 0 seconds
608608-609609- {4 Encoding}
610610- - 27120 seconds -> [07:32:00]
611611- - 52245.5 seconds -> [14:30:45.5]
612612-613613- {4 Example}
614614- {[
615615- type daily_schedule = { name : string; start_time : Ptime.Span.t }
380380+ Decodes [07:32:00] to a span representing time since midnight.
381381+ Values are clamped to [00:00:00] to [23:59:59.999999999].
616382617617- let schedule_codec = Tomlt.(Table.(
618618- obj (fun name start_time -> { name; start_time })
619619- |> mem "name" string ~enc:(fun s -> s.name)
620620- |> mem "start_time" ptime_span ~enc:(fun s -> s.start_time)
621621- |> finish
622622- ))
623623-624624- (* start_time = 09:00:00 -> 32400 seconds *)
625625- ]} *)
383383+ See {{!page-cookbook.datetimes}Working with datetimes} for examples. *)
626384627385val ptime_date : Ptime.date t
628628-(** Codec for TOML local dates as [Ptime.date] (a [(year, month, day)] tuple).
629629-630630- Decodes a local date like [1979-05-27] to an [(int * int * int)] tuple.
631631- Only accepts [Date_local] TOML values; rejects datetimes and times.
632632-633633- {4 Example}
634634- {[
635635- type person = { name : string; birthday : Ptime.date }
636636-637637- let person_codec = Tomlt.(Table.(
638638- obj (fun name birthday -> { name; birthday })
639639- |> mem "name" string ~enc:(fun p -> p.name)
640640- |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday)
641641- |> finish
642642- ))
386386+(** Codec for TOML local dates as [Ptime.date] ([(year, month, day)] tuple).
643387644644- (* birthday = 1979-05-27 -> (1979, 5, 27) *)
645645- ]}
388388+ Decodes [1979-05-27] to [(1979, 5, 27)]. Only accepts local dates.
389389+ To work with dates as [Ptime.t] (at midnight), use {!ptime} instead.
646390647647- To work with dates as [Ptime.t] (at midnight), use {!ptime} instead. *)
391391+ See {{!page-cookbook.datetimes}Working with datetimes} for examples. *)
648392649393val ptime_full :
650394 ?tz_offset_s:int ->
···652396 unit -> Toml.ptime_datetime t
653397(** Codec that preserves full datetime variant information.
654398655655- Unlike {!ptime} which normalizes all datetime formats to [Ptime.t],
656656- this codec returns a polymorphic variant that indicates exactly what
657657- was present in the TOML source. This is essential for:
658658-659659- - Distinguishing between datetime formats during decoding
660660- - Roundtripping TOML files while preserving the original format
661661- - Applications that treat different datetime formats differently
662662-663663- {4 Decoded Variants}
664664-665665- The [Toml.ptime_datetime] type is:
666666- {[
667667- type ptime_datetime = [
668668- | `Datetime of Ptime.t * Ptime.tz_offset_s option
669669- | `Datetime_local of Ptime.t
670670- | `Date of Ptime.date
671671- | `Time of int * int * int * int (* hour, minute, second, nanoseconds *)
672672- ]
673673- ]}
674674-675675- {4 Mapping from TOML}
676676-677677- - [2024-01-15T10:30:00Z] -> [`Datetime (ptime, Some 0)]
678678- - [2024-01-15T10:30:00-05:00] -> [`Datetime (ptime, Some (-18000))]
679679- - [2024-01-15T10:30:00] -> [`Datetime_local ptime]
680680- - [2024-01-15] -> [`Date (2024, 1, 15)]
681681- - [10:30:45.123] -> [`Time (10, 30, 45, 123_000_000)]
682682-683683- {4 Encoding}
399399+ Returns a {!Toml.ptime_datetime} variant indicating exactly what was
400400+ present in the TOML source. Essential for roundtripping TOML files
401401+ while preserving the original format.
684402685685- When encoding, the variant determines the output format:
686686- - [`Datetime] -> offset datetime with timezone
687687- - [`Datetime_local] -> local datetime (no timezone)
688688- - [`Date] -> local date
689689- - [`Time] -> local time
403403+ See {{!page-cookbook.datetimes}Working with datetimes} and
404404+ {{!page-cookbook.roundtripping}Roundtripping TOML} for examples.
690405691691- @param tz_offset_s Explicit timezone offset for converting
692692- [`Datetime_local] to [Ptime.t].
693693-694694- @param get_tz Function to get the current timezone offset. Called when
695695- [tz_offset_s] is not provided. Pass [Tomlt_unix.current_tz_offset_s]
696696- for OS-specific timezone support. If neither is provided, defaults to
697697- UTC (0).
698698-699699- {4 Example}
700700- {[
701701- type schedule_item = {
702702- description : string;
703703- when_ : Toml.ptime_datetime;
704704- }
705705-706706- let item_codec = Tomlt.(Table.(
707707- obj (fun description when_ -> { description; when_ })
708708- |> mem "description" string ~enc:(fun i -> i.description)
709709- |> mem "when" (ptime_full ()) ~enc:(fun i -> i.when_)
710710- |> finish
711711- ))
712712-713713- (* Can distinguish between:
714714- - when = 2024-01-15T10:00:00Z (specific instant)
715715- - when = 2024-01-15T10:00:00 (wall clock time)
716716- - when = 2024-01-15 (all day)
717717- - when = 10:00:00 (daily recurring)
718718- *)
719719- ]} *)
406406+ @param tz_offset_s Timezone offset for converting [`Datetime_local].
407407+ @param get_tz Function for timezone when [tz_offset_s] not provided. *)
720408721409(** {1:combinators Codec Combinators} *)
722410···904592905593(** {1:arrays Array Codecs}
906594907907- Build codecs for {{:https://toml.io/en/v1.1.0#array}TOML arrays}. *)
595595+ Build codecs for {{:https://toml.io/en/v1.1.0#array}TOML arrays}.
596596+597597+ See {{!page-cookbook.arrays}Working with arrays} for patterns. *)
908598909599module Array : sig
910600 type 'a codec = 'a t
···947637(** {1:tables Table Codecs}
948638949639 Build codecs for {{:https://toml.io/en/v1.1.0#table}TOML tables}
950950- (key-value mappings). The applicative-style builder pattern allows
951951- defining bidirectional codecs declaratively.
952952-953953- Tables can be defined using standard headers or as
954954- {{:https://toml.io/en/v1.1.0#inline-table}inline tables}.
955955- {{:https://toml.io/en/v1.1.0#keys}Keys} can be bare, quoted, or dotted.
956956-957957- {2 Basic Usage}
958958-959959- {v
960960- type person = { name : string; age : int }
640640+ using an applicative-style builder pattern.
961641962962- let person_codec = Tomlt.Table.(
963963- obj (fun name age -> { name; age })
964964- |> mem "name" Tomlt.string ~enc:(fun p -> p.name)
965965- |> mem "age" Tomlt.int ~enc:(fun p -> p.age)
966966- |> finish
967967- )
968968- v} *)
642642+ See the {{!page-cookbook.config_files}cookbook} for configuration patterns,
643643+ {{!page-cookbook.optional_values}optional values}, and
644644+ {{!page-cookbook.unknown_members}unknown member handling}. *)
969645970646module Table : sig
971647 type 'a codec = 'a t
+3-2
lib_bytesrw/tomlt_bytesrw.mli
···2020 |} in
2121 match config with
2222 | Ok t ->
2323- let host = Tomlt.Toml.(t.%{["server"; "host"]} |> to_string) in
2424- let port = Tomlt.Toml.(t.%{["server"; "port"]} |> to_int) in
2323+ let server = Tomlt.Toml.find "server" t in
2424+ let host = Tomlt.Toml.to_string (Tomlt.Toml.find "host" server) in
2525+ let port = Tomlt.Toml.to_int (Tomlt.Toml.find "port" server) in
2526 Printf.printf "Server: %s:%Ld\n" host port
2627 | Error e -> prerr_endline (Tomlt.Toml.Error.to_string e)
2728 ]}
+646
test/cookbook.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+[@@@warning "-32"]
77+88+(** Cookbook examples - runnable implementations matching doc/cookbook.mld *)
99+1010+(* ============================================
1111+ Configuration Files
1212+ ============================================ *)
1313+1414+module Config_files = struct
1515+ (* Basic Configuration *)
1616+ type database_config = {
1717+ host : string;
1818+ port : int;
1919+ name : string;
2020+ }
2121+2222+ let database_config_codec =
2323+ Tomlt.(Table.(
2424+ obj (fun host port name -> { host; port; name })
2525+ |> mem "host" string ~enc:(fun c -> c.host)
2626+ |> mem "port" int ~enc:(fun c -> c.port)
2727+ |> mem "name" string ~enc:(fun c -> c.name)
2828+ |> finish
2929+ ))
3030+3131+ let example_database_toml = {|
3232+host = "localhost"
3333+port = 5432
3434+name = "myapp"
3535+|}
3636+3737+ (* Nested Configuration *)
3838+ type server_config = {
3939+ host : string;
4040+ port : int;
4141+ }
4242+4343+ type app_config = {
4444+ name : string;
4545+ server : server_config;
4646+ debug : bool;
4747+ }
4848+4949+ let server_config_codec =
5050+ Tomlt.(Table.(
5151+ obj (fun host port -> { host; port })
5252+ |> mem "host" string ~enc:(fun s -> s.host)
5353+ |> mem "port" int ~enc:(fun s -> s.port)
5454+ |> finish
5555+ ))
5656+5757+ let app_config_codec =
5858+ Tomlt.(Table.(
5959+ obj (fun name server debug -> { name; server; debug })
6060+ |> mem "name" string ~enc:(fun c -> c.name)
6161+ |> mem "server" server_config_codec ~enc:(fun c -> c.server)
6262+ |> mem "debug" bool ~enc:(fun c -> c.debug)
6363+ |> finish
6464+ ))
6565+6666+ let example_app_toml = {|
6767+name = "My Application"
6868+debug = false
6969+7070+[server]
7171+host = "0.0.0.0"
7272+port = 8080
7373+|}
7474+7575+ (* Multi-Environment Configuration *)
7676+ type env_config = {
7777+ database_url : string;
7878+ log_level : string;
7979+ cache_ttl : int;
8080+ }
8181+8282+ type config = {
8383+ app_name : string;
8484+ development : env_config;
8585+ production : env_config;
8686+ }
8787+8888+ let env_config_codec =
8989+ Tomlt.(Table.(
9090+ obj (fun database_url log_level cache_ttl ->
9191+ { database_url; log_level; cache_ttl })
9292+ |> mem "database_url" string ~enc:(fun e -> e.database_url)
9393+ |> mem "log_level" string ~enc:(fun e -> e.log_level)
9494+ |> mem "cache_ttl" int ~enc:(fun e -> e.cache_ttl)
9595+ |> finish
9696+ ))
9797+9898+ let config_codec =
9999+ Tomlt.(Table.(
100100+ obj (fun app_name development production ->
101101+ { app_name; development; production })
102102+ |> mem "app_name" string ~enc:(fun c -> c.app_name)
103103+ |> mem "development" env_config_codec ~enc:(fun c -> c.development)
104104+ |> mem "production" env_config_codec ~enc:(fun c -> c.production)
105105+ |> finish
106106+ ))
107107+108108+ let example_multi_env_toml = {|
109109+app_name = "MyApp"
110110+111111+[development]
112112+database_url = "postgres://localhost/dev"
113113+log_level = "debug"
114114+cache_ttl = 60
115115+116116+[production]
117117+database_url = "postgres://prod-db/app"
118118+log_level = "error"
119119+cache_ttl = 3600
120120+|}
121121+end
122122+123123+(* ============================================
124124+ Optional and Absent Values
125125+ ============================================ *)
126126+127127+module Optional_values = struct
128128+ (* Default Values with dec_absent *)
129129+ type settings = {
130130+ theme : string;
131131+ font_size : int;
132132+ show_line_numbers : bool;
133133+ }
134134+135135+ let settings_codec =
136136+ Tomlt.(Table.(
137137+ obj (fun theme font_size show_line_numbers ->
138138+ { theme; font_size; show_line_numbers })
139139+ |> mem "theme" string ~enc:(fun s -> s.theme)
140140+ ~dec_absent:"default"
141141+ |> mem "font_size" int ~enc:(fun s -> s.font_size)
142142+ ~dec_absent:12
143143+ |> mem "show_line_numbers" bool ~enc:(fun s -> s.show_line_numbers)
144144+ ~dec_absent:true
145145+ |> finish
146146+ ))
147147+148148+ let example_settings_toml = {|
149149+theme = "dark"
150150+|}
151151+152152+ (* Option Types with opt_mem *)
153153+ type user = {
154154+ name : string;
155155+ email : string option;
156156+ phone : string option;
157157+ }
158158+159159+ let user_codec =
160160+ Tomlt.(Table.(
161161+ obj (fun name email phone -> { name; email; phone })
162162+ |> mem "name" string ~enc:(fun u -> u.name)
163163+ |> opt_mem "email" string ~enc:(fun u -> u.email)
164164+ |> opt_mem "phone" string ~enc:(fun u -> u.phone)
165165+ |> finish
166166+ ))
167167+168168+ let example_user_toml = {|
169169+name = "Alice"
170170+email = "alice@example.com"
171171+|}
172172+173173+ (* Conditional Omission with enc_omit *)
174174+ type retry_config = {
175175+ name : string;
176176+ retries : int;
177177+ }
178178+179179+ let retry_config_codec =
180180+ Tomlt.(Table.(
181181+ obj (fun name retries -> { name; retries })
182182+ |> mem "name" string ~enc:(fun c -> c.name)
183183+ |> mem "retries" int ~enc:(fun c -> c.retries)
184184+ ~dec_absent:0
185185+ ~enc_omit:(fun r -> r = 0)
186186+ |> finish
187187+ ))
188188+end
189189+190190+(* ============================================
191191+ Datetimes
192192+ ============================================ *)
193193+194194+module Datetimes = struct
195195+ (* Basic Datetime Handling *)
196196+ type event = { name : string; timestamp : Ptime.t }
197197+198198+ let event_codec =
199199+ Tomlt.(Table.(
200200+ obj (fun name timestamp -> { name; timestamp })
201201+ |> mem "name" string ~enc:(fun e -> e.name)
202202+ |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp)
203203+ |> finish
204204+ ))
205205+206206+ let example_event_toml = {|
207207+name = "Meeting"
208208+when = 2024-01-15T10:30:00Z
209209+|}
210210+211211+ (* Strict Timestamp Validation *)
212212+ type audit_log = { action : string; timestamp : Ptime.t }
213213+214214+ let audit_codec =
215215+ Tomlt.(Table.(
216216+ obj (fun action timestamp -> { action; timestamp })
217217+ |> mem "action" string ~enc:(fun a -> a.action)
218218+ |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp)
219219+ |> finish
220220+ ))
221221+222222+ let example_audit_toml = {|
223223+action = "user_login"
224224+timestamp = 2024-01-15T10:30:00Z
225225+|}
226226+227227+ (* Date-Only Fields *)
228228+ type person = { name : string; birthday : Ptime.date }
229229+230230+ let person_codec =
231231+ Tomlt.(Table.(
232232+ obj (fun name birthday -> { name; birthday })
233233+ |> mem "name" string ~enc:(fun p -> p.name)
234234+ |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday)
235235+ |> finish
236236+ ))
237237+238238+ let example_person_toml = {|
239239+name = "Bob"
240240+birthday = 1985-03-15
241241+|}
242242+243243+ (* Time-Only Fields *)
244244+ type alarm = { label : string; time : Ptime.Span.t }
245245+246246+ let alarm_codec =
247247+ Tomlt.(Table.(
248248+ obj (fun label time -> { label; time })
249249+ |> mem "label" string ~enc:(fun a -> a.label)
250250+ |> mem "time" ptime_span ~enc:(fun a -> a.time)
251251+ |> finish
252252+ ))
253253+254254+ let example_alarm_toml = {|
255255+label = "Wake up"
256256+time = 07:30:00
257257+|}
258258+259259+ (* Preserving Datetime Format *)
260260+ type flexible_event = {
261261+ name : string;
262262+ when_ : Tomlt.Toml.ptime_datetime;
263263+ }
264264+265265+ let flexible_codec =
266266+ Tomlt.(Table.(
267267+ obj (fun name when_ -> { name; when_ })
268268+ |> mem "name" string ~enc:(fun e -> e.name)
269269+ |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_)
270270+ |> finish
271271+ ))
272272+273273+ let example_flexible_toml = {|
274274+name = "Birthday"
275275+when = 1985-03-15
276276+|}
277277+end
278278+279279+(* ============================================
280280+ Arrays
281281+ ============================================ *)
282282+283283+module Arrays = struct
284284+ (* Basic Arrays *)
285285+ type network_config = {
286286+ name : string;
287287+ ports : int list;
288288+ hosts : string list;
289289+ }
290290+291291+ let network_config_codec =
292292+ Tomlt.(Table.(
293293+ obj (fun name ports hosts -> { name; ports; hosts })
294294+ |> mem "name" string ~enc:(fun c -> c.name)
295295+ |> mem "ports" (list int) ~enc:(fun c -> c.ports)
296296+ |> mem "hosts" (list string) ~enc:(fun c -> c.hosts)
297297+ |> finish
298298+ ))
299299+300300+ let example_network_toml = {|
301301+name = "load-balancer"
302302+ports = [80, 443, 8080]
303303+hosts = ["web1.example.com", "web2.example.com"]
304304+|}
305305+306306+ (* Arrays of Tables *)
307307+ type product = { name : string; price : float }
308308+ type catalog = { products : product list }
309309+310310+ let product_codec =
311311+ Tomlt.(Table.(
312312+ obj (fun name price -> { name; price })
313313+ |> mem "name" string ~enc:(fun p -> p.name)
314314+ |> mem "price" float ~enc:(fun p -> p.price)
315315+ |> finish
316316+ ))
317317+318318+ let catalog_codec =
319319+ Tomlt.(Table.(
320320+ obj (fun products -> { products })
321321+ |> mem "products" (array_of_tables product_codec)
322322+ ~enc:(fun c -> c.products)
323323+ |> finish
324324+ ))
325325+326326+ let example_catalog_toml = {|
327327+[[products]]
328328+name = "Widget"
329329+price = 9.99
330330+331331+[[products]]
332332+name = "Gadget"
333333+price = 19.99
334334+|}
335335+336336+ (* Nested Arrays *)
337337+ type matrix = { rows : int list list }
338338+339339+ let matrix_codec =
340340+ Tomlt.(Table.(
341341+ obj (fun rows -> { rows })
342342+ |> mem "rows" (list (list int)) ~enc:(fun m -> m.rows)
343343+ |> finish
344344+ ))
345345+346346+ let example_matrix_toml = {|
347347+rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
348348+|}
349349+end
350350+351351+(* ============================================
352352+ Tables
353353+ ============================================ *)
354354+355355+module Tables = struct
356356+ (* Inline Tables *)
357357+ type point = { x : int; y : int }
358358+359359+ let point_codec =
360360+ Tomlt.(Table.(
361361+ obj (fun x y -> { x; y })
362362+ |> mem "x" int ~enc:(fun p -> p.x)
363363+ |> mem "y" int ~enc:(fun p -> p.y)
364364+ |> inline
365365+ ))
366366+367367+ (* Deeply Nested Structures *)
368368+ type address = { street : string; city : string }
369369+ type company = { name : string; address : address }
370370+ type employee = { name : string; company : company }
371371+372372+ let address_codec =
373373+ Tomlt.(Table.(
374374+ obj (fun street city -> { street; city })
375375+ |> mem "street" string ~enc:(fun (a : address) -> a.street)
376376+ |> mem "city" string ~enc:(fun a -> a.city)
377377+ |> finish
378378+ ))
379379+380380+ let company_codec =
381381+ Tomlt.(Table.(
382382+ obj (fun name address -> { name; address })
383383+ |> mem "name" string ~enc:(fun (c : company) -> c.name)
384384+ |> mem "address" address_codec ~enc:(fun c -> c.address)
385385+ |> finish
386386+ ))
387387+388388+ let employee_codec =
389389+ Tomlt.(Table.(
390390+ obj (fun name company -> { name; company })
391391+ |> mem "name" string ~enc:(fun (e : employee) -> e.name)
392392+ |> mem "company" company_codec ~enc:(fun e -> e.company)
393393+ |> finish
394394+ ))
395395+396396+ let example_employee_toml = {|
397397+name = "Alice"
398398+399399+[company]
400400+name = "Acme Corp"
401401+402402+[company.address]
403403+street = "123 Main St"
404404+city = "Springfield"
405405+|}
406406+end
407407+408408+(* ============================================
409409+ Unknown Members
410410+ ============================================ *)
411411+412412+module Unknown_members = struct
413413+ (* Ignoring Unknown Members (Default) *)
414414+ let host_only_codec =
415415+ Tomlt.(Table.(
416416+ obj (fun host -> host)
417417+ |> mem "host" string ~enc:Fun.id
418418+ |> skip_unknown
419419+ |> finish
420420+ ))
421421+422422+ (* Rejecting Unknown Members *)
423423+ let strict_config_codec =
424424+ Tomlt.(Table.(
425425+ obj (fun host port -> (host, port))
426426+ |> mem "host" string ~enc:fst
427427+ |> mem "port" int ~enc:snd
428428+ |> error_unknown
429429+ |> finish
430430+ ))
431431+432432+ (* Collecting Unknown Members *)
433433+ type extensible_config = {
434434+ name : string;
435435+ extra : (string * Tomlt.Toml.t) list;
436436+ }
437437+438438+ let extensible_config_codec =
439439+ Tomlt.(Table.(
440440+ obj (fun name extra -> { name; extra })
441441+ |> mem "name" string ~enc:(fun c -> c.name)
442442+ |> keep_unknown (Mems.assoc value) ~enc:(fun c -> c.extra)
443443+ |> finish
444444+ ))
445445+446446+ let example_extensible_toml = {|
447447+name = "app"
448448+foo = 42
449449+bar = "hello"
450450+|}
451451+452452+ (* Typed Unknown Members *)
453453+ module StringMap = Map.Make(String)
454454+455455+ type translations = {
456456+ default_lang : string;
457457+ strings : string StringMap.t;
458458+ }
459459+460460+ let translations_codec =
461461+ Tomlt.(Table.(
462462+ obj (fun default_lang strings -> { default_lang; strings })
463463+ |> mem "default_lang" string ~enc:(fun t -> t.default_lang)
464464+ |> keep_unknown (Mems.string_map string) ~enc:(fun t -> t.strings)
465465+ |> finish
466466+ ))
467467+468468+ let example_translations_toml = {|
469469+default_lang = "en"
470470+hello = "Hello"
471471+goodbye = "Goodbye"
472472+thanks = "Thank you"
473473+|}
474474+end
475475+476476+(* ============================================
477477+ Validation
478478+ ============================================ *)
479479+480480+module Validation = struct
481481+ (* Range Validation with iter *)
482482+ let port_codec =
483483+ Tomlt.(iter int
484484+ ~dec:(fun p ->
485485+ if p < 0 || p > 65535 then
486486+ failwith "port must be between 0 and 65535"))
487487+488488+ let percentage_codec =
489489+ Tomlt.(iter float
490490+ ~dec:(fun p ->
491491+ if p < 0.0 || p > 100.0 then
492492+ failwith "percentage must be between 0 and 100"))
493493+494494+ (* String Enumerations *)
495495+ type log_level = Debug | Info | Warning | Error
496496+497497+ let log_level_codec =
498498+ Tomlt.enum [
499499+ "debug", Debug;
500500+ "info", Info;
501501+ "warning", Warning;
502502+ "error", Error;
503503+ ]
504504+505505+ type log_config = { level : log_level }
506506+507507+ let log_config_codec =
508508+ Tomlt.(Table.(
509509+ obj (fun level -> { level })
510510+ |> mem "level" log_level_codec ~enc:(fun c -> c.level)
511511+ |> finish
512512+ ))
513513+514514+ let example_log_toml = {|
515515+level = "info"
516516+|}
517517+end
518518+519519+(* ============================================
520520+ Recursion
521521+ ============================================ *)
522522+523523+module Recursion = struct
524524+ type tree = Node of int * tree list
525525+526526+ let rec tree_codec = lazy Tomlt.(
527527+ Table.(
528528+ obj (fun value children -> Node (value, children))
529529+ |> mem "value" int ~enc:(function Node (v, _) -> v)
530530+ |> mem "children" (list (rec' tree_codec))
531531+ ~enc:(function Node (_, cs) -> cs)
532532+ ~dec_absent:[]
533533+ |> finish
534534+ ))
535535+536536+ let tree_codec = Lazy.force tree_codec
537537+538538+ let example_tree_toml = {|
539539+value = 1
540540+541541+[[children]]
542542+value = 2
543543+544544+[[children]]
545545+value = 3
546546+547547+[[children.children]]
548548+value = 4
549549+|}
550550+end
551551+552552+(* ============================================
553553+ Main - Run examples
554554+ ============================================ *)
555555+556556+let decode_and_print name codec toml =
557557+ Printf.printf "=== %s ===\n" name;
558558+ match Tomlt_bytesrw.decode_string codec toml with
559559+ | Ok _ -> Printf.printf "OK: Decoded successfully\n\n"
560560+ | Error e -> Printf.printf "ERROR: %s\n\n" (Tomlt.Toml.Error.to_string e)
561561+562562+let () =
563563+ Printf.printf "Tomlt Cookbook Examples\n";
564564+ Printf.printf "=======================\n\n";
565565+566566+ (* Config files *)
567567+ decode_and_print "Database config"
568568+ Config_files.database_config_codec
569569+ Config_files.example_database_toml;
570570+571571+ decode_and_print "App config"
572572+ Config_files.app_config_codec
573573+ Config_files.example_app_toml;
574574+575575+ decode_and_print "Multi-env config"
576576+ Config_files.config_codec
577577+ Config_files.example_multi_env_toml;
578578+579579+ (* Optional values *)
580580+ decode_and_print "Settings with defaults"
581581+ Optional_values.settings_codec
582582+ Optional_values.example_settings_toml;
583583+584584+ decode_and_print "User with optional fields"
585585+ Optional_values.user_codec
586586+ Optional_values.example_user_toml;
587587+588588+ (* Datetimes *)
589589+ decode_and_print "Event with datetime"
590590+ Datetimes.event_codec
591591+ Datetimes.example_event_toml;
592592+593593+ decode_and_print "Audit log (strict)"
594594+ Datetimes.audit_codec
595595+ Datetimes.example_audit_toml;
596596+597597+ decode_and_print "Person with birthday"
598598+ Datetimes.person_codec
599599+ Datetimes.example_person_toml;
600600+601601+ decode_and_print "Alarm with time"
602602+ Datetimes.alarm_codec
603603+ Datetimes.example_alarm_toml;
604604+605605+ decode_and_print "Flexible event"
606606+ Datetimes.flexible_codec
607607+ Datetimes.example_flexible_toml;
608608+609609+ (* Arrays *)
610610+ decode_and_print "Network config"
611611+ Arrays.network_config_codec
612612+ Arrays.example_network_toml;
613613+614614+ decode_and_print "Product catalog"
615615+ Arrays.catalog_codec
616616+ Arrays.example_catalog_toml;
617617+618618+ decode_and_print "Matrix"
619619+ Arrays.matrix_codec
620620+ Arrays.example_matrix_toml;
621621+622622+ (* Tables *)
623623+ decode_and_print "Employee (nested)"
624624+ Tables.employee_codec
625625+ Tables.example_employee_toml;
626626+627627+ (* Unknown members *)
628628+ decode_and_print "Extensible config"
629629+ Unknown_members.extensible_config_codec
630630+ Unknown_members.example_extensible_toml;
631631+632632+ decode_and_print "Translations"
633633+ Unknown_members.translations_codec
634634+ Unknown_members.example_translations_toml;
635635+636636+ (* Validation *)
637637+ decode_and_print "Log config"
638638+ Validation.log_config_codec
639639+ Validation.example_log_toml;
640640+641641+ (* Recursion *)
642642+ decode_and_print "Tree"
643643+ Recursion.tree_codec
644644+ Recursion.example_tree_toml;
645645+646646+ Printf.printf "All examples completed.\n"