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