The unpac monorepo manager self-hosting as a monorepo using unpac

cook

+1609 -387
+819
doc/cookbook.mld
··· 1 + {0 Cookbook} 2 + 3 + This cookbook provides patterns and recipes for common TOML tasks. 4 + Each section includes both conceptual explanation and working code 5 + examples. See {!module:Tomlt} for the full API reference. 6 + 7 + {1:conventions Conventions} 8 + 9 + Throughout 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 + 18 + The most common use case: parsing a TOML configuration file into an 19 + OCaml record. 20 + 21 + {2 Basic Configuration} 22 + 23 + {[ 24 + type database_config = { 25 + host : string; 26 + port : int; 27 + name : string; 28 + } 29 + 30 + let 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 + 40 + This handles TOML like: 41 + 42 + {v 43 + host = "localhost" 44 + port = 5432 45 + name = "myapp" 46 + v} 47 + 48 + {2 Nested Configuration} 49 + 50 + For nested tables, compose codecs: 51 + 52 + {[ 53 + type server_config = { 54 + host : string; 55 + port : int; 56 + } 57 + 58 + type app_config = { 59 + name : string; 60 + server : server_config; 61 + debug : bool; 62 + } 63 + 64 + let 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 + 72 + let 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 + 82 + This handles: 83 + 84 + {v 85 + name = "My Application" 86 + debug = false 87 + 88 + [server] 89 + host = "0.0.0.0" 90 + port = 8080 91 + v} 92 + 93 + {2 Multi-Environment Configuration} 94 + 95 + A pattern for dev/staging/prod configurations: 96 + 97 + {[ 98 + type env_config = { 99 + database_url : string; 100 + log_level : string; 101 + cache_ttl : int; 102 + } 103 + 104 + type config = { 105 + app_name : string; 106 + development : env_config; 107 + production : env_config; 108 + } 109 + 110 + let 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 + 120 + let 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 + 133 + TOML tables may have optional members. Tomlt provides several ways 134 + to handle missing values. 135 + 136 + {2 Default Values with dec_absent} 137 + 138 + Use [~dec_absent] to provide a default when a key is missing: 139 + 140 + {[ 141 + type settings = { 142 + theme : string; 143 + font_size : int; 144 + show_line_numbers : bool; 145 + } 146 + 147 + let 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: 163 + theme = "dark" 164 + 165 + # Or with defaults: 166 + # (empty table uses all defaults) 167 + v} 168 + 169 + {2 Option Types with opt_mem} 170 + 171 + Use {!Tomlt.Table.opt_mem} when the absence of a value is meaningful: 172 + 173 + {[ 174 + type user = { 175 + name : string; 176 + email : string option; 177 + phone : string option; 178 + } 179 + 180 + let 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 + 190 + On encoding, [None] values are omitted from the output: 191 + 192 + {[ 193 + (* This user: *) 194 + let 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 + 204 + Use [~enc_omit] to omit values that match a predicate: 205 + 206 + {[ 207 + type config = { 208 + name : string; 209 + retries : int; (* omit if 0 *) 210 + } 211 + 212 + let 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 + 225 + TOML 1.1 supports four datetime formats. Tomlt provides Ptime-based 226 + codecs that handle all of them. 227 + 228 + {2 TOML Datetime Formats} 229 + 230 + {v 231 + # Offset datetime - full timestamp with timezone (unambiguous) 232 + published = 2024-01-15T10:30:00Z 233 + published = 2024-01-15T10:30:00-05:00 234 + 235 + # Local datetime - no timezone (wall clock time) 236 + meeting = 2024-01-15T10:30:00 237 + 238 + # Local date - date only 239 + birthday = 1979-05-27 240 + 241 + # Local time - time only 242 + alarm = 07:30:00 243 + v} 244 + 245 + {2 Basic Datetime Handling} 246 + 247 + Use {!Tomlt.ptime} to accept any datetime format and normalize to 248 + [Ptime.t]: 249 + 250 + {[ 251 + type event = { name : string; timestamp : Ptime.t } 252 + 253 + let 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 + 270 + Use {!Tomlt.ptime_opt} when you require explicit timezone information: 271 + 272 + {[ 273 + type audit_log = { action : string; timestamp : Ptime.t } 274 + 275 + let 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 + 291 + Use {!Tomlt.ptime_date} for fields that should only contain dates: 292 + 293 + {[ 294 + type person = { name : string; birthday : Ptime.date } 295 + 296 + let 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 + 309 + Use {!Tomlt.ptime_span} for recurring times (as duration from midnight): 310 + 311 + {[ 312 + type alarm = { label : string; time : Ptime.Span.t } 313 + 314 + let 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 + 327 + Use {!Tomlt.ptime_full} to preserve the exact datetime variant for 328 + roundtripping: 329 + 330 + {[ 331 + type flexible_event = { 332 + name : string; 333 + when_ : Toml.ptime_datetime; 334 + } 335 + 336 + let 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 + 355 + For local datetimes without explicit timezone, you can specify how 356 + to interpret them: 357 + 358 + {[ 359 + (* Force UTC interpretation *) 360 + let utc_codec = Tomlt.ptime ~tz_offset_s:0 () 361 + 362 + (* Force Eastern Time (-05:00 = -18000 seconds) *) 363 + let eastern_codec = Tomlt.ptime ~tz_offset_s:(-18000) () 364 + 365 + (* Use system timezone (requires Tomlt_unix) *) 366 + let system_codec = 367 + Tomlt.ptime ~get_tz:Tomlt_unix.current_tz_offset_s () 368 + ]} 369 + 370 + {1:arrays Working with Arrays} 371 + 372 + TOML 1.1 supports heterogeneous arrays, but most use cases involve 373 + homogeneous arrays of a single type. 374 + 375 + {2 Basic Arrays} 376 + 377 + {[ 378 + type config = { 379 + name : string; 380 + ports : int list; 381 + hosts : string list; 382 + } 383 + 384 + let 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 395 + name = "load-balancer" 396 + ports = [80, 443, 8080] 397 + hosts = ["web1.example.com", "web2.example.com"] 398 + v} 399 + 400 + {2 Arrays of Tables} 401 + 402 + Use {!Tomlt.array_of_tables} for TOML's [[[name]]] syntax: 403 + 404 + {[ 405 + type product = { name : string; price : float } 406 + type catalog = { products : product list } 407 + 408 + let 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 + 416 + let 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]] 427 + name = "Widget" 428 + price = 9.99 429 + 430 + [[products]] 431 + name = "Gadget" 432 + price = 19.99 433 + v} 434 + 435 + {2 Nested Arrays} 436 + 437 + Arrays can contain other arrays: 438 + 439 + {[ 440 + type matrix = { rows : int list list } 441 + 442 + let 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 451 + rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 452 + v} 453 + 454 + {2 Custom Array Types} 455 + 456 + Use {!val:Tomlt.Array.map} to decode into custom collection types: 457 + 458 + {[ 459 + module IntSet = Set.Make(Int) 460 + 461 + let 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 + 476 + Use {!Tomlt.Table.inline} to encode as inline tables: 477 + 478 + {[ 479 + type point = { x : int; y : int } 480 + 481 + let 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 + {[ 499 + type address = { street : string; city : string } 500 + type company = { name : string; address : address } 501 + type employee = { name : string; company : company } 502 + 503 + let 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 + 511 + let 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 + 519 + let 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 529 + name = "Alice" 530 + 531 + [company] 532 + name = "Acme Corp" 533 + 534 + [company.address] 535 + street = "123 Main St" 536 + city = "Springfield" 537 + v} 538 + 539 + {1:unknown_members Unknown Member Handling} 540 + 541 + By default, unknown members in TOML tables are ignored. You can 542 + change this behavior. 543 + 544 + {2 Ignoring Unknown Members (Default)} 545 + 546 + {[ 547 + let 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 + 562 + Use {!Tomlt.Table.error_unknown} for strict parsing: 563 + 564 + {[ 565 + let 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 + 581 + Use {!Tomlt.Table.keep_unknown} to preserve unknown members: 582 + 583 + {[ 584 + type config = { 585 + name : string; 586 + extra : (string * Toml.t) list; 587 + } 588 + 589 + let 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 + 609 + Collect unknown members with a specific type: 610 + 611 + {[ 612 + module StringMap = Map.Make(String) 613 + 614 + type translations = { 615 + default_lang : string; 616 + strings : string StringMap.t; 617 + } 618 + 619 + let 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 + 641 + Use {!Tomlt.iter} to add validation: 642 + 643 + {[ 644 + let 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 + 650 + let 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 + 659 + Use {!Tomlt.enum} for fixed string values: 660 + 661 + {[ 662 + type log_level = Debug | Info | Warning | Error 663 + 664 + let log_level_codec = 665 + Tomlt.enum [ 666 + "debug", Debug; 667 + "info", Info; 668 + "warning", Warning; 669 + "error", Error; 670 + ] 671 + 672 + type config = { level : log_level } 673 + 674 + let 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 + 684 + Use {!Tomlt.map} to transform between representations: 685 + 686 + {[ 687 + (* Store URI as string in TOML *) 688 + let uri_codec = 689 + Tomlt.(map string 690 + ~dec:Uri.of_string 691 + ~enc:Uri.to_string) 692 + 693 + (* Parse comma-separated tags *) 694 + let 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 + 704 + Use {!Tomlt.value} to preserve parts of a document unchanged: 705 + 706 + {[ 707 + type 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 + 718 + Use {!Tomlt.ptime_full} to roundtrip datetime formats: 719 + 720 + {[ 721 + type event = { 722 + name : string; 723 + when_ : Toml.ptime_datetime; 724 + } 725 + 726 + let 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 + 742 + Always use {!Tomlt.decode} in production code: 743 + 744 + {[ 745 + let 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 + 756 + Errors 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 + 769 + For collecting multiple errors, decode fields individually: 770 + 771 + {[ 772 + let 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 + 790 + Use {!Tomlt.rec'} for self-referential types: 791 + 792 + {[ 793 + type tree = Node of int * tree list 794 + 795 + let 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 + 805 + let tree_codec = Lazy.force tree_codec 806 + ]} 807 + 808 + {v 809 + value = 1 810 + 811 + [[children]] 812 + value = 2 813 + 814 + [[children]] 815 + value = 3 816 + 817 + [[children.children]] 818 + value = 4 819 + v}
+3
doc/dune
··· 1 + (documentation 2 + (package tomlt) 3 + (mld_files index cookbook))
+71
doc/index.mld
··· 1 + {0 Tomlt} 2 + 3 + {1 TOML 1.1 Codec Library} 4 + 5 + Tomlt is a bidirectional codec library for {{:https://toml.io/en/v1.1.0}TOML 1.1} 6 + configuration files. It provides type-safe encoding and decoding between 7 + OCaml types and TOML values. 8 + 9 + {2 Quick Start} 10 + 11 + Define a codec for your configuration type: 12 + 13 + {[ 14 + type config = { host : string; port : int; debug : bool } 15 + 16 + let config_codec = 17 + Tomlt.(Table.( 18 + obj (fun host port debug -> { host; port; debug }) 19 + |> mem "host" string ~enc:(fun c -> c.host) 20 + |> mem "port" int ~enc:(fun c -> c.port) 21 + |> mem "debug" bool ~enc:(fun c -> c.debug) ~dec_absent:false 22 + |> finish 23 + )) 24 + ]} 25 + 26 + Parse and use it: 27 + 28 + {[ 29 + let () = 30 + match Tomlt_bytesrw.decode_string config_codec {| 31 + host = "localhost" 32 + port = 8080 33 + |} with 34 + | Ok config -> Printf.printf "Host: %s\n" config.host 35 + | Error e -> prerr_endline (Toml.Error.to_string e) 36 + ]} 37 + 38 + {2 Library Structure} 39 + 40 + - {!Tomlt.Toml} - Core TOML value types and operations 41 + - {!Tomlt} - Codec combinators for bidirectional TOML encoding/decoding 42 + - {!Tomlt_bytesrw} - Streaming parser and encoder 43 + - {!Tomlt_eio} - Eio-native I/O integration 44 + - {!Tomlt_unix} - Unix I/O integration 45 + - {!Tomlt_jsont} - JSON codec for toml-test format 46 + 47 + {2 Cookbook} 48 + 49 + The {{!page-cookbook}cookbook} provides patterns and recipes for common 50 + TOML scenarios: 51 + 52 + - {{!page-cookbook.config_files}Parsing configuration files} 53 + - {{!page-cookbook.optional_values}Optional and absent values} 54 + - {{!page-cookbook.datetimes}Working with datetimes} 55 + - {{!page-cookbook.arrays}Working with arrays} 56 + - {{!page-cookbook.tables}Nested tables and objects} 57 + - {{!page-cookbook.unknown_members}Unknown member handling} 58 + - {{!page-cookbook.validation}Validation and constraints} 59 + - {{!page-cookbook.roundtripping}Roundtripping TOML} 60 + - {{!page-cookbook.error_handling}Error handling} 61 + 62 + {2 Design} 63 + 64 + Tomlt is inspired by {{:https://erratique.ch/software/jsont}Jsont}'s approach 65 + to JSON codecs. Each codec ['a Tomlt.t] defines both: 66 + 67 + - A decoder: [Toml.t -> ('a, error) result] 68 + - An encoder: ['a -> Toml.t] 69 + 70 + Codecs compose through combinators, allowing complex types to be built 71 + from simple primitives while maintaining bidirectionality.
+9 -7
lib/toml.mli
··· 7 7 8 8 This module provides the core TOML value type and operations for 9 9 constructing, accessing, and manipulating TOML data. For parsing and 10 - encoding, see {!Tomlt_bytesrw}. 10 + encoding, see {!Tomlt_bytesrw}. For codec-based bidirectional encoding, 11 + see {!Tomlt}. 11 12 12 13 {2 Quick Start} 13 14 ··· 24 25 25 26 Access values: 26 27 {[ 27 - let host = Toml.(config.%{["database"; "host"]} |> to_string) 28 - let port = Toml.(config.%{["database"; "ports"]} |> to_array |> List.hd |> to_int) 28 + let host = Toml.to_string (Toml.find "host" (Toml.find "database" config)) 29 + let ports = Toml.to_array (Toml.find "ports" (Toml.find "database" config)) 30 + let port = Toml.to_int (List.hd ports) 29 31 ]} 32 + 33 + See the {{!page-cookbook}cookbook} for common patterns and recipes. 30 34 31 35 {2 Module Overview} 32 36 ··· 370 374 371 375 val pp_value : Format.formatter -> t -> unit 372 376 (** [pp_value fmt t] pretty-prints a single TOML value. 373 - Same as {!pp}. *) 377 + Same as {!val:pp}. *) 374 378 375 379 val equal : t -> t -> bool 376 380 (** [equal a b] is structural equality on TOML values. ··· 382 386 (** {1:errors Error Handling} *) 383 387 384 388 module Error = Toml_error 385 - (** Structured error types for TOML parsing and encoding. 386 - 387 - See {!Toml_error} for detailed documentation. *) 389 + (** Structured error types for TOML parsing and encoding. *)
+54 -378
lib/tomlt.mli
··· 44 44 Codecs compose through combinators to build complex types from 45 45 simple primitives. 46 46 47 - {2 Datetime Handling} 48 - 49 - Tomlt uses {{:https://erratique.ch/software/ptime}Ptime} for all datetime 50 - operations, providing a unified approach to TOML's four datetime formats: 47 + {2 Cookbook} 51 48 52 - {v 53 - (* Accept any TOML datetime format, normalize to Ptime.t *) 54 - type event = { name : string; when_ : Ptime.t } 49 + See the {{!page-cookbook}cookbook} for patterns and recipes: 55 50 56 - let event_codec = Tomlt.(Table.( 57 - obj (fun name when_ -> { name; when_ }) 58 - |> mem "name" string ~enc:(fun e -> e.name) 59 - |> mem "when" (ptime ()) ~enc:(fun e -> e.when_) 60 - |> finish 61 - )) 62 - 63 - (* All of these work: *) 64 - (* when = 2024-01-15T10:30:00Z -> offset datetime *) 65 - (* when = 2024-01-15T10:30:00 -> local datetime (uses system tz) *) 66 - (* when = 2024-01-15 -> date only (assumes midnight) *) 67 - (* when = 10:30:00 -> time only (uses today's date) *) 68 - v} 69 - 70 - See {!section:ptime_codecs} for the complete datetime codec API. 51 + - {{!page-cookbook.config_files}Parsing configuration files} 52 + - {{!page-cookbook.optional_values}Optional and absent values} 53 + - {{!page-cookbook.datetimes}Working with datetimes} 54 + - {{!page-cookbook.arrays}Working with arrays} 55 + - {{!page-cookbook.tables}Nested tables and objects} 56 + - {{!page-cookbook.unknown_members}Unknown member handling} 57 + - {{!page-cookbook.validation}Validation and constraints} 71 58 72 59 {2 Module Overview} 73 60 ··· 340 327 341 328 (** {1:ptime_codecs Ptime Datetime Codecs} 342 329 343 - Tomlt provides a unified datetime handling system built on 344 - {{:https://erratique.ch/software/ptime}Ptime}. All 345 - {{:https://toml.io/en/v1.1.0#offset-date-time}TOML datetime formats} 346 - can be decoded to [Ptime.t] timestamps with sensible defaults for 347 - incomplete information. 348 - 349 - {2 TOML Datetime Formats} 350 - 351 - {{:https://toml.io/en/v1.1.0}TOML 1.1} supports four datetime formats 352 - with varying levels of precision: 353 - 354 - {v 355 - # Offset datetime - full timestamp with timezone (unambiguous) 356 - # See: https://toml.io/en/v1.1.0#offset-date-time 357 - published = 2024-01-15T10:30:00Z 358 - published = 2024-01-15T10:30:00-05:00 330 + Tomlt provides unified datetime handling using 331 + {{:https://erratique.ch/software/ptime}Ptime}. All TOML datetime formats 332 + can be decoded to [Ptime.t] timestamps. 359 333 360 - # Local datetime - no timezone (wall clock time) 361 - # See: https://toml.io/en/v1.1.0#local-date-time 362 - meeting = 2024-01-15T10:30:00 363 - 364 - # Local date - date only 365 - # See: https://toml.io/en/v1.1.0#local-date 366 - birthday = 1979-05-27 367 - 368 - # Local time - time only 369 - # See: https://toml.io/en/v1.1.0#local-time 370 - alarm = 07:30:00 371 - v} 334 + See the {{!page-cookbook.datetimes}cookbook} for detailed patterns 335 + and examples. 372 336 373 337 {2 Choosing a Codec} 374 338 375 - - {!val:ptime} - {b Recommended for most cases.} Accepts any datetime format 376 - and normalizes to [Ptime.t] by filling in sensible defaults. 377 - 378 - - {!val:ptime_opt} - {b For strict validation.} Only accepts offset datetimes 379 - with explicit timezone. Rejects ambiguous local formats. 380 - 381 - - {!val:ptime_date} - For fields that should only contain dates. 382 - 383 - - {!val:ptime_span} - For fields that should only contain times (as duration). 384 - 385 - - {!val:ptime_full} - {b For roundtripping.} Preserves the exact datetime 386 - variant from the source, allowing faithful re-encoding. 387 - 388 - {2 Timezone Handling} 389 - 390 - For local datetimes without explicit timezone, Tomlt uses 391 - [Ptime_clock.current_tz_offset_s ()] to get the system timezone. 392 - You can override this by passing [~tz_offset_s]: 393 - 394 - {v 395 - (* Force UTC interpretation for local datetimes *) 396 - let codec = ptime ~tz_offset_s:0 () 397 - 398 - (* Force Eastern Time (-05:00 = -18000 seconds) *) 399 - let codec = ptime ~tz_offset_s:(-18000) () 400 - v} 401 - 402 - {2 Examples} 403 - 404 - {3 Basic Event Tracking} 405 - {v 406 - type event = { name : string; timestamp : Ptime.t } 407 - 408 - let event_codec = Tomlt.(Table.( 409 - obj (fun name timestamp -> { name; timestamp }) 410 - |> mem "name" string ~enc:(fun e -> e.name) 411 - |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp) 412 - |> finish 413 - )) 414 - 415 - (* All of these decode successfully: *) 416 - (* when = 2024-01-15T10:30:00Z *) 417 - (* when = 2024-01-15T10:30:00 *) 418 - (* when = 2024-01-15 *) 419 - (* when = 10:30:00 *) 420 - v} 421 - 422 - {3 Strict Timestamp Validation} 423 - {v 424 - type log_entry = { message : string; timestamp : Ptime.t } 425 - 426 - let log_codec = Tomlt.(Table.( 427 - obj (fun message timestamp -> { message; timestamp }) 428 - |> mem "message" string ~enc:(fun e -> e.message) 429 - |> mem "timestamp" (ptime_opt ()) ~enc:(fun e -> e.timestamp) 430 - |> finish 431 - )) 432 - 433 - (* Only accepts: timestamp = 2024-01-15T10:30:00Z *) 434 - (* Rejects: timestamp = 2024-01-15T10:30:00 *) 435 - v} 436 - 437 - {3 Birthday (Date Only)} 438 - {v 439 - type person = { name : string; birthday : Ptime.date } 440 - 441 - let person_codec = Tomlt.(Table.( 442 - obj (fun name birthday -> { name; birthday }) 443 - |> mem "name" string ~enc:(fun p -> p.name) 444 - |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday) 445 - |> finish 446 - )) 447 - 448 - (* birthday = 1979-05-27 -> (1979, 5, 27) *) 449 - v} 450 - 451 - {3 Daily Alarm (Time Only)} 452 - {v 453 - type alarm = { label : string; time : Ptime.Span.t } 454 - 455 - let alarm_codec = Tomlt.(Table.( 456 - obj (fun label time -> { label; time }) 457 - |> mem "label" string ~enc:(fun a -> a.label) 458 - |> mem "time" ptime_span ~enc:(fun a -> a.time) 459 - |> finish 460 - )) 461 - 462 - (* time = 07:30:00 -> 27000 seconds (7.5 hours from midnight) *) 463 - v} 464 - 465 - {3 Preserving Datetime Format} 466 - {v 467 - type flexible_event = { 468 - name : string; 469 - when_ : Toml.ptime_datetime; 470 - } 471 - 472 - let flexible_codec = Tomlt.(Table.( 473 - obj (fun name when_ -> { name; when_ }) 474 - |> mem "name" string ~enc:(fun e -> e.name) 475 - |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_) 476 - |> finish 477 - )) 478 - 479 - (* Decoding preserves the variant: 480 - when = 2024-01-15T10:30:00Z -> `Datetime (ptime, Some 0) 481 - when = 2024-01-15T10:30:00 -> `Datetime_local ptime 482 - when = 2024-01-15 -> `Date (2024, 1, 15) 483 - when = 10:30:00 -> `Time (10, 30, 0, 0) 484 - 485 - Encoding reproduces the original format. *) 486 - v} *) 339 + - {!val:ptime} - Accepts any datetime format, normalizes to [Ptime.t] 340 + - {!val:ptime_opt} - Strict: only accepts offset datetimes with timezone 341 + - {!val:ptime_date} - For date-only fields 342 + - {!val:ptime_span} - For time-only fields (as duration from midnight) 343 + - {!val:ptime_full} - Preserves exact variant for roundtripping *) 487 344 488 345 val ptime : 489 346 ?tz_offset_s:int -> ··· 493 350 unit -> Ptime.t t 494 351 (** Datetime codec that converts any TOML datetime to {!Ptime.t}. 495 352 496 - This is the recommended codec for most datetime use cases. It handles 497 - all TOML datetime variants by filling in sensible defaults: 498 - 499 - - {b Offset datetime} ([2024-01-15T10:30:00Z]): Parsed directly to [Ptime.t] 500 - - {b Local datetime} ([2024-01-15T10:30:00]): Converted using the timezone 501 - - {b Local date} ([2024-01-15]): Assumed to be midnight (00:00:00) in the 502 - given timezone 503 - - {b Local time} ([10:30:00]): Combined with today's date using [now] 504 - 505 - Encoding always produces an RFC 3339 offset datetime string. 506 - 507 - {4 Parameters} 508 - 509 - @param tz_offset_s Explicit timezone offset in seconds, used for: 510 - - Converting local datetimes to [Ptime.t] 511 - - Converting local dates to [Ptime.t] (at midnight) 512 - - Converting local times to [Ptime.t] (on today's date) 513 - - Formatting the timezone when encoding 514 - 515 - Common values: 516 - - [0] = UTC 517 - - [3600] = +01:00 (Central European Time) 518 - - [-18000] = -05:00 (Eastern Standard Time) 519 - - [-28800] = -08:00 (Pacific Standard Time) 520 - 521 - If not provided, [get_tz] is called. If neither is provided, defaults 522 - to UTC (0). 523 - 524 - @param get_tz Function to get the current timezone offset. Called when 525 - [tz_offset_s] is not provided. Pass [Tomlt_unix.current_tz_offset_s] 526 - for OS-specific timezone support: 527 - {[let codec = ptime ~get_tz:Tomlt_unix.current_tz_offset_s ()]} 528 - 529 - @param now Function to get the current time. Used when decoding local 530 - times (e.g., [10:30:00]) to combine with today's date. Pass 531 - [Tomlt_unix.now] for OS-specific time support. If not provided, 532 - defaults to [Ptime.epoch] (1970-01-01). 533 - 534 - @param frac_s Number of fractional second digits to include when encoding. 535 - Range: 0-12. Default: 0 (whole seconds only). For example, [~frac_s:3] 536 - produces [2024-01-15T10:30:00.123Z]. 353 + Handles all TOML datetime variants by filling in sensible defaults. 354 + Encoding produces RFC 3339 offset datetime strings. 537 355 538 - {4 Example} 539 - {[ 540 - type event = { name : string; timestamp : Ptime.t } 356 + See {{!page-cookbook.datetimes}Working with datetimes} for examples. 541 357 542 - let event_codec = Tomlt.(Table.( 543 - obj (fun name timestamp -> { name; timestamp }) 544 - |> mem "name" string ~enc:(fun e -> e.name) 545 - |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp) 546 - |> finish 547 - )) 548 - 549 - (* All of these decode to a Ptime.t: *) 550 - let e1 = decode_string_exn event_codec {|name="a" when=2024-01-15T10:30:00Z|} 551 - let e2 = decode_string_exn event_codec {|name="b" when=2024-01-15T10:30:00|} 552 - let e3 = decode_string_exn event_codec {|name="c" when=2024-01-15|} 553 - let e4 = decode_string_exn event_codec {|name="d" when=10:30:00|} 554 - ]} *) 358 + @param tz_offset_s Timezone offset in seconds for local datetimes. 359 + Common: [0] (UTC), [3600] (+01:00), [-18000] (-05:00). 360 + @param get_tz Function to get timezone offset when [tz_offset_s] 361 + not provided. Use [Tomlt_unix.current_tz_offset_s] for system timezone. 362 + @param now Function for current time, used for time-only values. 363 + Use [Tomlt_unix.now] for system time. 364 + @param frac_s Fractional second digits (0-12) for encoding. *) 555 365 556 366 val ptime_opt : ?tz_offset_s:int -> ?frac_s:int -> unit -> Ptime.t t 557 367 (** Strict datetime codec that only accepts offset datetimes. 558 368 559 - Unlike {!ptime} which accepts any datetime format, this codec requires 560 - an explicit timezone and rejects local datetime variants. Use this when 561 - you need unambiguous timestamps and want to reject values that would 562 - require timezone assumptions. 369 + Requires explicit timezone; rejects local datetimes, dates, and times. 370 + Use when you need unambiguous timestamps. 563 371 564 - {4 Accepted} 565 - - [2024-01-15T10:30:00Z] (UTC) 566 - - [2024-01-15T10:30:00+05:30] (explicit offset) 567 - - [2024-01-15T10:30:00-08:00] (explicit offset) 568 - 569 - {4 Rejected} 570 - 571 - These raise [Value_error]: 572 - 573 - - [2024-01-15T10:30:00] (local datetime - no timezone) 574 - - [2024-01-15] (local date) 575 - - [10:30:00] (local time) 372 + See {{!page-cookbook.datetimes}Working with datetimes} for examples. 576 373 577 374 @param tz_offset_s Timezone offset for encoding. Default: 0 (UTC). 578 - @param frac_s Fractional second digits for encoding. Default: 0. 579 - 580 - {4 Example} 581 - {[ 582 - type audit_log = { action : string; timestamp : Ptime.t } 583 - 584 - let audit_codec = Tomlt.(Table.( 585 - obj (fun action timestamp -> { action; timestamp }) 586 - |> mem "action" string ~enc:(fun a -> a.action) 587 - |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp) 588 - |> finish 589 - )) 590 - 591 - (* Valid: timestamp = 2024-01-15T10:30:00Z *) 592 - (* Error: timestamp = 2024-01-15T10:30:00 (no timezone) *) 593 - ]} *) 375 + @param frac_s Fractional second digits for encoding. Default: 0. *) 594 376 595 377 val ptime_span : Ptime.Span.t t 596 378 (** Codec for TOML local times as [Ptime.Span.t] (duration from midnight). 597 379 598 - Decodes a local time like [07:32:00] or [14:30:45.123] to a [Ptime.Span.t] 599 - representing the time elapsed since midnight (00:00:00). 600 - 601 - When encoding, the span is formatted as a local time string. Values are 602 - clamped to the range [00:00:00] to [23:59:59.999999999]. 603 - 604 - {4 Decoding} 605 - - [07:32:00] -> 27120 seconds (7 hours, 32 minutes) 606 - - [14:30:45.5] -> 52245.5 seconds 607 - - [00:00:00] -> 0 seconds 608 - 609 - {4 Encoding} 610 - - 27120 seconds -> [07:32:00] 611 - - 52245.5 seconds -> [14:30:45.5] 612 - 613 - {4 Example} 614 - {[ 615 - type daily_schedule = { name : string; start_time : Ptime.Span.t } 380 + Decodes [07:32:00] to a span representing time since midnight. 381 + Values are clamped to [00:00:00] to [23:59:59.999999999]. 616 382 617 - let schedule_codec = Tomlt.(Table.( 618 - obj (fun name start_time -> { name; start_time }) 619 - |> mem "name" string ~enc:(fun s -> s.name) 620 - |> mem "start_time" ptime_span ~enc:(fun s -> s.start_time) 621 - |> finish 622 - )) 623 - 624 - (* start_time = 09:00:00 -> 32400 seconds *) 625 - ]} *) 383 + See {{!page-cookbook.datetimes}Working with datetimes} for examples. *) 626 384 627 385 val ptime_date : Ptime.date t 628 - (** Codec for TOML local dates as [Ptime.date] (a [(year, month, day)] tuple). 629 - 630 - Decodes a local date like [1979-05-27] to an [(int * int * int)] tuple. 631 - Only accepts [Date_local] TOML values; rejects datetimes and times. 632 - 633 - {4 Example} 634 - {[ 635 - type person = { name : string; birthday : Ptime.date } 636 - 637 - let person_codec = Tomlt.(Table.( 638 - obj (fun name birthday -> { name; birthday }) 639 - |> mem "name" string ~enc:(fun p -> p.name) 640 - |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday) 641 - |> finish 642 - )) 386 + (** Codec for TOML local dates as [Ptime.date] ([(year, month, day)] tuple). 643 387 644 - (* birthday = 1979-05-27 -> (1979, 5, 27) *) 645 - ]} 388 + Decodes [1979-05-27] to [(1979, 5, 27)]. Only accepts local dates. 389 + To work with dates as [Ptime.t] (at midnight), use {!ptime} instead. 646 390 647 - To work with dates as [Ptime.t] (at midnight), use {!ptime} instead. *) 391 + See {{!page-cookbook.datetimes}Working with datetimes} for examples. *) 648 392 649 393 val ptime_full : 650 394 ?tz_offset_s:int -> ··· 652 396 unit -> Toml.ptime_datetime t 653 397 (** Codec that preserves full datetime variant information. 654 398 655 - Unlike {!ptime} which normalizes all datetime formats to [Ptime.t], 656 - this codec returns a polymorphic variant that indicates exactly what 657 - was present in the TOML source. This is essential for: 658 - 659 - - Distinguishing between datetime formats during decoding 660 - - Roundtripping TOML files while preserving the original format 661 - - Applications that treat different datetime formats differently 662 - 663 - {4 Decoded Variants} 664 - 665 - The [Toml.ptime_datetime] type is: 666 - {[ 667 - type ptime_datetime = [ 668 - | `Datetime of Ptime.t * Ptime.tz_offset_s option 669 - | `Datetime_local of Ptime.t 670 - | `Date of Ptime.date 671 - | `Time of int * int * int * int (* hour, minute, second, nanoseconds *) 672 - ] 673 - ]} 674 - 675 - {4 Mapping from TOML} 676 - 677 - - [2024-01-15T10:30:00Z] -> [`Datetime (ptime, Some 0)] 678 - - [2024-01-15T10:30:00-05:00] -> [`Datetime (ptime, Some (-18000))] 679 - - [2024-01-15T10:30:00] -> [`Datetime_local ptime] 680 - - [2024-01-15] -> [`Date (2024, 1, 15)] 681 - - [10:30:45.123] -> [`Time (10, 30, 45, 123_000_000)] 682 - 683 - {4 Encoding} 399 + Returns a {!Toml.ptime_datetime} variant indicating exactly what was 400 + present in the TOML source. Essential for roundtripping TOML files 401 + while preserving the original format. 684 402 685 - When encoding, the variant determines the output format: 686 - - [`Datetime] -> offset datetime with timezone 687 - - [`Datetime_local] -> local datetime (no timezone) 688 - - [`Date] -> local date 689 - - [`Time] -> local time 403 + See {{!page-cookbook.datetimes}Working with datetimes} and 404 + {{!page-cookbook.roundtripping}Roundtripping TOML} for examples. 690 405 691 - @param tz_offset_s Explicit timezone offset for converting 692 - [`Datetime_local] to [Ptime.t]. 693 - 694 - @param get_tz Function to get the current timezone offset. Called when 695 - [tz_offset_s] is not provided. Pass [Tomlt_unix.current_tz_offset_s] 696 - for OS-specific timezone support. If neither is provided, defaults to 697 - UTC (0). 698 - 699 - {4 Example} 700 - {[ 701 - type schedule_item = { 702 - description : string; 703 - when_ : Toml.ptime_datetime; 704 - } 705 - 706 - let item_codec = Tomlt.(Table.( 707 - obj (fun description when_ -> { description; when_ }) 708 - |> mem "description" string ~enc:(fun i -> i.description) 709 - |> mem "when" (ptime_full ()) ~enc:(fun i -> i.when_) 710 - |> finish 711 - )) 712 - 713 - (* Can distinguish between: 714 - - when = 2024-01-15T10:00:00Z (specific instant) 715 - - when = 2024-01-15T10:00:00 (wall clock time) 716 - - when = 2024-01-15 (all day) 717 - - when = 10:00:00 (daily recurring) 718 - *) 719 - ]} *) 406 + @param tz_offset_s Timezone offset for converting [`Datetime_local]. 407 + @param get_tz Function for timezone when [tz_offset_s] not provided. *) 720 408 721 409 (** {1:combinators Codec Combinators} *) 722 410 ··· 904 592 905 593 (** {1:arrays Array Codecs} 906 594 907 - Build codecs for {{:https://toml.io/en/v1.1.0#array}TOML arrays}. *) 595 + Build codecs for {{:https://toml.io/en/v1.1.0#array}TOML arrays}. 596 + 597 + See {{!page-cookbook.arrays}Working with arrays} for patterns. *) 908 598 909 599 module Array : sig 910 600 type 'a codec = 'a t ··· 947 637 (** {1:tables Table Codecs} 948 638 949 639 Build codecs for {{:https://toml.io/en/v1.1.0#table}TOML tables} 950 - (key-value mappings). The applicative-style builder pattern allows 951 - defining bidirectional codecs declaratively. 952 - 953 - Tables can be defined using standard headers or as 954 - {{:https://toml.io/en/v1.1.0#inline-table}inline tables}. 955 - {{:https://toml.io/en/v1.1.0#keys}Keys} can be bare, quoted, or dotted. 956 - 957 - {2 Basic Usage} 958 - 959 - {v 960 - type person = { name : string; age : int } 640 + using an applicative-style builder pattern. 961 641 962 - let person_codec = Tomlt.Table.( 963 - obj (fun name age -> { name; age }) 964 - |> mem "name" Tomlt.string ~enc:(fun p -> p.name) 965 - |> mem "age" Tomlt.int ~enc:(fun p -> p.age) 966 - |> finish 967 - ) 968 - v} *) 642 + See the {{!page-cookbook.config_files}cookbook} for configuration patterns, 643 + {{!page-cookbook.optional_values}optional values}, and 644 + {{!page-cookbook.unknown_members}unknown member handling}. *) 969 645 970 646 module Table : sig 971 647 type 'a codec = 'a t
+3 -2
lib_bytesrw/tomlt_bytesrw.mli
··· 20 20 |} in 21 21 match config with 22 22 | Ok t -> 23 - let host = Tomlt.Toml.(t.%{["server"; "host"]} |> to_string) in 24 - let port = Tomlt.Toml.(t.%{["server"; "port"]} |> to_int) in 23 + let server = Tomlt.Toml.find "server" t in 24 + let host = Tomlt.Toml.to_string (Tomlt.Toml.find "host" server) in 25 + let port = Tomlt.Toml.to_int (Tomlt.Toml.find "port" server) in 25 26 Printf.printf "Server: %s:%Ld\n" host port 26 27 | Error e -> prerr_endline (Tomlt.Toml.Error.to_string e) 27 28 ]}
+646
test/cookbook.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + [@@@warning "-32"] 7 + 8 + (** Cookbook examples - runnable implementations matching doc/cookbook.mld *) 9 + 10 + (* ============================================ 11 + Configuration Files 12 + ============================================ *) 13 + 14 + module Config_files = struct 15 + (* Basic Configuration *) 16 + type database_config = { 17 + host : string; 18 + port : int; 19 + name : string; 20 + } 21 + 22 + let database_config_codec = 23 + Tomlt.(Table.( 24 + obj (fun host port name -> { host; port; name }) 25 + |> mem "host" string ~enc:(fun c -> c.host) 26 + |> mem "port" int ~enc:(fun c -> c.port) 27 + |> mem "name" string ~enc:(fun c -> c.name) 28 + |> finish 29 + )) 30 + 31 + let example_database_toml = {| 32 + host = "localhost" 33 + port = 5432 34 + name = "myapp" 35 + |} 36 + 37 + (* Nested Configuration *) 38 + type server_config = { 39 + host : string; 40 + port : int; 41 + } 42 + 43 + type app_config = { 44 + name : string; 45 + server : server_config; 46 + debug : bool; 47 + } 48 + 49 + let server_config_codec = 50 + Tomlt.(Table.( 51 + obj (fun host port -> { host; port }) 52 + |> mem "host" string ~enc:(fun s -> s.host) 53 + |> mem "port" int ~enc:(fun s -> s.port) 54 + |> finish 55 + )) 56 + 57 + let app_config_codec = 58 + Tomlt.(Table.( 59 + obj (fun name server debug -> { name; server; debug }) 60 + |> mem "name" string ~enc:(fun c -> c.name) 61 + |> mem "server" server_config_codec ~enc:(fun c -> c.server) 62 + |> mem "debug" bool ~enc:(fun c -> c.debug) 63 + |> finish 64 + )) 65 + 66 + let example_app_toml = {| 67 + name = "My Application" 68 + debug = false 69 + 70 + [server] 71 + host = "0.0.0.0" 72 + port = 8080 73 + |} 74 + 75 + (* Multi-Environment Configuration *) 76 + type env_config = { 77 + database_url : string; 78 + log_level : string; 79 + cache_ttl : int; 80 + } 81 + 82 + type config = { 83 + app_name : string; 84 + development : env_config; 85 + production : env_config; 86 + } 87 + 88 + let env_config_codec = 89 + Tomlt.(Table.( 90 + obj (fun database_url log_level cache_ttl -> 91 + { database_url; log_level; cache_ttl }) 92 + |> mem "database_url" string ~enc:(fun e -> e.database_url) 93 + |> mem "log_level" string ~enc:(fun e -> e.log_level) 94 + |> mem "cache_ttl" int ~enc:(fun e -> e.cache_ttl) 95 + |> finish 96 + )) 97 + 98 + let config_codec = 99 + Tomlt.(Table.( 100 + obj (fun app_name development production -> 101 + { app_name; development; production }) 102 + |> mem "app_name" string ~enc:(fun c -> c.app_name) 103 + |> mem "development" env_config_codec ~enc:(fun c -> c.development) 104 + |> mem "production" env_config_codec ~enc:(fun c -> c.production) 105 + |> finish 106 + )) 107 + 108 + let example_multi_env_toml = {| 109 + app_name = "MyApp" 110 + 111 + [development] 112 + database_url = "postgres://localhost/dev" 113 + log_level = "debug" 114 + cache_ttl = 60 115 + 116 + [production] 117 + database_url = "postgres://prod-db/app" 118 + log_level = "error" 119 + cache_ttl = 3600 120 + |} 121 + end 122 + 123 + (* ============================================ 124 + Optional and Absent Values 125 + ============================================ *) 126 + 127 + module Optional_values = struct 128 + (* Default Values with dec_absent *) 129 + type settings = { 130 + theme : string; 131 + font_size : int; 132 + show_line_numbers : bool; 133 + } 134 + 135 + let settings_codec = 136 + Tomlt.(Table.( 137 + obj (fun theme font_size show_line_numbers -> 138 + { theme; font_size; show_line_numbers }) 139 + |> mem "theme" string ~enc:(fun s -> s.theme) 140 + ~dec_absent:"default" 141 + |> mem "font_size" int ~enc:(fun s -> s.font_size) 142 + ~dec_absent:12 143 + |> mem "show_line_numbers" bool ~enc:(fun s -> s.show_line_numbers) 144 + ~dec_absent:true 145 + |> finish 146 + )) 147 + 148 + let example_settings_toml = {| 149 + theme = "dark" 150 + |} 151 + 152 + (* Option Types with opt_mem *) 153 + type user = { 154 + name : string; 155 + email : string option; 156 + phone : string option; 157 + } 158 + 159 + let user_codec = 160 + Tomlt.(Table.( 161 + obj (fun name email phone -> { name; email; phone }) 162 + |> mem "name" string ~enc:(fun u -> u.name) 163 + |> opt_mem "email" string ~enc:(fun u -> u.email) 164 + |> opt_mem "phone" string ~enc:(fun u -> u.phone) 165 + |> finish 166 + )) 167 + 168 + let example_user_toml = {| 169 + name = "Alice" 170 + email = "alice@example.com" 171 + |} 172 + 173 + (* Conditional Omission with enc_omit *) 174 + type retry_config = { 175 + name : string; 176 + retries : int; 177 + } 178 + 179 + let retry_config_codec = 180 + Tomlt.(Table.( 181 + obj (fun name retries -> { name; retries }) 182 + |> mem "name" string ~enc:(fun c -> c.name) 183 + |> mem "retries" int ~enc:(fun c -> c.retries) 184 + ~dec_absent:0 185 + ~enc_omit:(fun r -> r = 0) 186 + |> finish 187 + )) 188 + end 189 + 190 + (* ============================================ 191 + Datetimes 192 + ============================================ *) 193 + 194 + module Datetimes = struct 195 + (* Basic Datetime Handling *) 196 + type event = { name : string; timestamp : Ptime.t } 197 + 198 + let event_codec = 199 + Tomlt.(Table.( 200 + obj (fun name timestamp -> { name; timestamp }) 201 + |> mem "name" string ~enc:(fun e -> e.name) 202 + |> mem "when" (ptime ()) ~enc:(fun e -> e.timestamp) 203 + |> finish 204 + )) 205 + 206 + let example_event_toml = {| 207 + name = "Meeting" 208 + when = 2024-01-15T10:30:00Z 209 + |} 210 + 211 + (* Strict Timestamp Validation *) 212 + type audit_log = { action : string; timestamp : Ptime.t } 213 + 214 + let audit_codec = 215 + Tomlt.(Table.( 216 + obj (fun action timestamp -> { action; timestamp }) 217 + |> mem "action" string ~enc:(fun a -> a.action) 218 + |> mem "timestamp" (ptime_opt ()) ~enc:(fun a -> a.timestamp) 219 + |> finish 220 + )) 221 + 222 + let example_audit_toml = {| 223 + action = "user_login" 224 + timestamp = 2024-01-15T10:30:00Z 225 + |} 226 + 227 + (* Date-Only Fields *) 228 + type person = { name : string; birthday : Ptime.date } 229 + 230 + let person_codec = 231 + Tomlt.(Table.( 232 + obj (fun name birthday -> { name; birthday }) 233 + |> mem "name" string ~enc:(fun p -> p.name) 234 + |> mem "birthday" ptime_date ~enc:(fun p -> p.birthday) 235 + |> finish 236 + )) 237 + 238 + let example_person_toml = {| 239 + name = "Bob" 240 + birthday = 1985-03-15 241 + |} 242 + 243 + (* Time-Only Fields *) 244 + type alarm = { label : string; time : Ptime.Span.t } 245 + 246 + let alarm_codec = 247 + Tomlt.(Table.( 248 + obj (fun label time -> { label; time }) 249 + |> mem "label" string ~enc:(fun a -> a.label) 250 + |> mem "time" ptime_span ~enc:(fun a -> a.time) 251 + |> finish 252 + )) 253 + 254 + let example_alarm_toml = {| 255 + label = "Wake up" 256 + time = 07:30:00 257 + |} 258 + 259 + (* Preserving Datetime Format *) 260 + type flexible_event = { 261 + name : string; 262 + when_ : Tomlt.Toml.ptime_datetime; 263 + } 264 + 265 + let flexible_codec = 266 + Tomlt.(Table.( 267 + obj (fun name when_ -> { name; when_ }) 268 + |> mem "name" string ~enc:(fun e -> e.name) 269 + |> mem "when" (ptime_full ()) ~enc:(fun e -> e.when_) 270 + |> finish 271 + )) 272 + 273 + let example_flexible_toml = {| 274 + name = "Birthday" 275 + when = 1985-03-15 276 + |} 277 + end 278 + 279 + (* ============================================ 280 + Arrays 281 + ============================================ *) 282 + 283 + module Arrays = struct 284 + (* Basic Arrays *) 285 + type network_config = { 286 + name : string; 287 + ports : int list; 288 + hosts : string list; 289 + } 290 + 291 + let network_config_codec = 292 + Tomlt.(Table.( 293 + obj (fun name ports hosts -> { name; ports; hosts }) 294 + |> mem "name" string ~enc:(fun c -> c.name) 295 + |> mem "ports" (list int) ~enc:(fun c -> c.ports) 296 + |> mem "hosts" (list string) ~enc:(fun c -> c.hosts) 297 + |> finish 298 + )) 299 + 300 + let example_network_toml = {| 301 + name = "load-balancer" 302 + ports = [80, 443, 8080] 303 + hosts = ["web1.example.com", "web2.example.com"] 304 + |} 305 + 306 + (* Arrays of Tables *) 307 + type product = { name : string; price : float } 308 + type catalog = { products : product list } 309 + 310 + let product_codec = 311 + Tomlt.(Table.( 312 + obj (fun name price -> { name; price }) 313 + |> mem "name" string ~enc:(fun p -> p.name) 314 + |> mem "price" float ~enc:(fun p -> p.price) 315 + |> finish 316 + )) 317 + 318 + let catalog_codec = 319 + Tomlt.(Table.( 320 + obj (fun products -> { products }) 321 + |> mem "products" (array_of_tables product_codec) 322 + ~enc:(fun c -> c.products) 323 + |> finish 324 + )) 325 + 326 + let example_catalog_toml = {| 327 + [[products]] 328 + name = "Widget" 329 + price = 9.99 330 + 331 + [[products]] 332 + name = "Gadget" 333 + price = 19.99 334 + |} 335 + 336 + (* Nested Arrays *) 337 + type matrix = { rows : int list list } 338 + 339 + let matrix_codec = 340 + Tomlt.(Table.( 341 + obj (fun rows -> { rows }) 342 + |> mem "rows" (list (list int)) ~enc:(fun m -> m.rows) 343 + |> finish 344 + )) 345 + 346 + let example_matrix_toml = {| 347 + rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 348 + |} 349 + end 350 + 351 + (* ============================================ 352 + Tables 353 + ============================================ *) 354 + 355 + module Tables = struct 356 + (* Inline Tables *) 357 + type point = { x : int; y : int } 358 + 359 + let point_codec = 360 + Tomlt.(Table.( 361 + obj (fun x y -> { x; y }) 362 + |> mem "x" int ~enc:(fun p -> p.x) 363 + |> mem "y" int ~enc:(fun p -> p.y) 364 + |> inline 365 + )) 366 + 367 + (* Deeply Nested Structures *) 368 + type address = { street : string; city : string } 369 + type company = { name : string; address : address } 370 + type employee = { name : string; company : company } 371 + 372 + let address_codec = 373 + Tomlt.(Table.( 374 + obj (fun street city -> { street; city }) 375 + |> mem "street" string ~enc:(fun (a : address) -> a.street) 376 + |> mem "city" string ~enc:(fun a -> a.city) 377 + |> finish 378 + )) 379 + 380 + let company_codec = 381 + Tomlt.(Table.( 382 + obj (fun name address -> { name; address }) 383 + |> mem "name" string ~enc:(fun (c : company) -> c.name) 384 + |> mem "address" address_codec ~enc:(fun c -> c.address) 385 + |> finish 386 + )) 387 + 388 + let employee_codec = 389 + Tomlt.(Table.( 390 + obj (fun name company -> { name; company }) 391 + |> mem "name" string ~enc:(fun (e : employee) -> e.name) 392 + |> mem "company" company_codec ~enc:(fun e -> e.company) 393 + |> finish 394 + )) 395 + 396 + let example_employee_toml = {| 397 + name = "Alice" 398 + 399 + [company] 400 + name = "Acme Corp" 401 + 402 + [company.address] 403 + street = "123 Main St" 404 + city = "Springfield" 405 + |} 406 + end 407 + 408 + (* ============================================ 409 + Unknown Members 410 + ============================================ *) 411 + 412 + module Unknown_members = struct 413 + (* Ignoring Unknown Members (Default) *) 414 + let host_only_codec = 415 + Tomlt.(Table.( 416 + obj (fun host -> host) 417 + |> mem "host" string ~enc:Fun.id 418 + |> skip_unknown 419 + |> finish 420 + )) 421 + 422 + (* Rejecting Unknown Members *) 423 + let strict_config_codec = 424 + Tomlt.(Table.( 425 + obj (fun host port -> (host, port)) 426 + |> mem "host" string ~enc:fst 427 + |> mem "port" int ~enc:snd 428 + |> error_unknown 429 + |> finish 430 + )) 431 + 432 + (* Collecting Unknown Members *) 433 + type extensible_config = { 434 + name : string; 435 + extra : (string * Tomlt.Toml.t) list; 436 + } 437 + 438 + let extensible_config_codec = 439 + Tomlt.(Table.( 440 + obj (fun name extra -> { name; extra }) 441 + |> mem "name" string ~enc:(fun c -> c.name) 442 + |> keep_unknown (Mems.assoc value) ~enc:(fun c -> c.extra) 443 + |> finish 444 + )) 445 + 446 + let example_extensible_toml = {| 447 + name = "app" 448 + foo = 42 449 + bar = "hello" 450 + |} 451 + 452 + (* Typed Unknown Members *) 453 + module StringMap = Map.Make(String) 454 + 455 + type translations = { 456 + default_lang : string; 457 + strings : string StringMap.t; 458 + } 459 + 460 + let translations_codec = 461 + Tomlt.(Table.( 462 + obj (fun default_lang strings -> { default_lang; strings }) 463 + |> mem "default_lang" string ~enc:(fun t -> t.default_lang) 464 + |> keep_unknown (Mems.string_map string) ~enc:(fun t -> t.strings) 465 + |> finish 466 + )) 467 + 468 + let example_translations_toml = {| 469 + default_lang = "en" 470 + hello = "Hello" 471 + goodbye = "Goodbye" 472 + thanks = "Thank you" 473 + |} 474 + end 475 + 476 + (* ============================================ 477 + Validation 478 + ============================================ *) 479 + 480 + module Validation = struct 481 + (* Range Validation with iter *) 482 + let port_codec = 483 + Tomlt.(iter int 484 + ~dec:(fun p -> 485 + if p < 0 || p > 65535 then 486 + failwith "port must be between 0 and 65535")) 487 + 488 + let percentage_codec = 489 + Tomlt.(iter float 490 + ~dec:(fun p -> 491 + if p < 0.0 || p > 100.0 then 492 + failwith "percentage must be between 0 and 100")) 493 + 494 + (* String Enumerations *) 495 + type log_level = Debug | Info | Warning | Error 496 + 497 + let log_level_codec = 498 + Tomlt.enum [ 499 + "debug", Debug; 500 + "info", Info; 501 + "warning", Warning; 502 + "error", Error; 503 + ] 504 + 505 + type log_config = { level : log_level } 506 + 507 + let log_config_codec = 508 + Tomlt.(Table.( 509 + obj (fun level -> { level }) 510 + |> mem "level" log_level_codec ~enc:(fun c -> c.level) 511 + |> finish 512 + )) 513 + 514 + let example_log_toml = {| 515 + level = "info" 516 + |} 517 + end 518 + 519 + (* ============================================ 520 + Recursion 521 + ============================================ *) 522 + 523 + module Recursion = struct 524 + type tree = Node of int * tree list 525 + 526 + let rec tree_codec = lazy Tomlt.( 527 + Table.( 528 + obj (fun value children -> Node (value, children)) 529 + |> mem "value" int ~enc:(function Node (v, _) -> v) 530 + |> mem "children" (list (rec' tree_codec)) 531 + ~enc:(function Node (_, cs) -> cs) 532 + ~dec_absent:[] 533 + |> finish 534 + )) 535 + 536 + let tree_codec = Lazy.force tree_codec 537 + 538 + let example_tree_toml = {| 539 + value = 1 540 + 541 + [[children]] 542 + value = 2 543 + 544 + [[children]] 545 + value = 3 546 + 547 + [[children.children]] 548 + value = 4 549 + |} 550 + end 551 + 552 + (* ============================================ 553 + Main - Run examples 554 + ============================================ *) 555 + 556 + let decode_and_print name codec toml = 557 + Printf.printf "=== %s ===\n" name; 558 + match Tomlt_bytesrw.decode_string codec toml with 559 + | Ok _ -> Printf.printf "OK: Decoded successfully\n\n" 560 + | Error e -> Printf.printf "ERROR: %s\n\n" (Tomlt.Toml.Error.to_string e) 561 + 562 + let () = 563 + Printf.printf "Tomlt Cookbook Examples\n"; 564 + Printf.printf "=======================\n\n"; 565 + 566 + (* Config files *) 567 + decode_and_print "Database config" 568 + Config_files.database_config_codec 569 + Config_files.example_database_toml; 570 + 571 + decode_and_print "App config" 572 + Config_files.app_config_codec 573 + Config_files.example_app_toml; 574 + 575 + decode_and_print "Multi-env config" 576 + Config_files.config_codec 577 + Config_files.example_multi_env_toml; 578 + 579 + (* Optional values *) 580 + decode_and_print "Settings with defaults" 581 + Optional_values.settings_codec 582 + Optional_values.example_settings_toml; 583 + 584 + decode_and_print "User with optional fields" 585 + Optional_values.user_codec 586 + Optional_values.example_user_toml; 587 + 588 + (* Datetimes *) 589 + decode_and_print "Event with datetime" 590 + Datetimes.event_codec 591 + Datetimes.example_event_toml; 592 + 593 + decode_and_print "Audit log (strict)" 594 + Datetimes.audit_codec 595 + Datetimes.example_audit_toml; 596 + 597 + decode_and_print "Person with birthday" 598 + Datetimes.person_codec 599 + Datetimes.example_person_toml; 600 + 601 + decode_and_print "Alarm with time" 602 + Datetimes.alarm_codec 603 + Datetimes.example_alarm_toml; 604 + 605 + decode_and_print "Flexible event" 606 + Datetimes.flexible_codec 607 + Datetimes.example_flexible_toml; 608 + 609 + (* Arrays *) 610 + decode_and_print "Network config" 611 + Arrays.network_config_codec 612 + Arrays.example_network_toml; 613 + 614 + decode_and_print "Product catalog" 615 + Arrays.catalog_codec 616 + Arrays.example_catalog_toml; 617 + 618 + decode_and_print "Matrix" 619 + Arrays.matrix_codec 620 + Arrays.example_matrix_toml; 621 + 622 + (* Tables *) 623 + decode_and_print "Employee (nested)" 624 + Tables.employee_codec 625 + Tables.example_employee_toml; 626 + 627 + (* Unknown members *) 628 + decode_and_print "Extensible config" 629 + Unknown_members.extensible_config_codec 630 + Unknown_members.example_extensible_toml; 631 + 632 + decode_and_print "Translations" 633 + Unknown_members.translations_codec 634 + Unknown_members.example_translations_toml; 635 + 636 + (* Validation *) 637 + decode_and_print "Log config" 638 + Validation.log_config_codec 639 + Validation.example_log_toml; 640 + 641 + (* Recursion *) 642 + decode_and_print "Tree" 643 + Recursion.tree_codec 644 + Recursion.example_tree_toml; 645 + 646 + Printf.printf "All examples completed.\n"
+4
test/dune
··· 9 9 (executable 10 10 (name test_debug) 11 11 (libraries tomlt tomlt.bytesrw)) 12 + 13 + (executable 14 + (name cookbook) 15 + (libraries tomlt tomlt.bytesrw))