My aggregated monorepo of OCaml code, automaintained
at doc-fixes 819 lines 18 kB view raw
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}