(*--------------------------------------------------------------------------- Copyright (c) 2025 Anil Madhavapeddy . All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) (** Init Cookbook This file contains complete, runnable examples demonstrating common patterns for using the Init library. Each example is self-contained and can be adapted to your use case. Run with: [dune exec ./test/cookbook.exe] *) (** {1 Basic Configuration} The simplest use case: parse a configuration file with a single section into an OCaml record. *) module Basic = struct type server_config = { host : string; port : int; debug : bool; } let server_codec = Init.Section.( obj (fun host port debug -> { host; port; debug }) |> mem "host" Init.string ~enc:(fun c -> c.host) |> mem "port" Init.int ~enc:(fun c -> c.port) |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug) |> finish ) let config_codec = Init.Document.( obj (fun server -> server) |> section "server" server_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [server] host = localhost port = 8080 debug = yes |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "Server: %s:%d (debug=%b)\n" config.host config.port config.debug | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Optional Values and Defaults} Handle missing options gracefully with defaults or optional fields. *) module Optional_values = struct type database_config = { host : string; port : int; (* Uses default if missing *) username : string; password : string option; (* Optional field *) ssl : bool; } let database_codec = Init.Section.( obj (fun host port username password ssl -> { host; port; username; password; ssl }) |> mem "host" Init.string ~enc:(fun c -> c.host) (* dec_absent provides a default value when the option is missing *) |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port) |> mem "username" Init.string ~enc:(fun c -> c.username) (* opt_mem decodes to None when the option is missing *) |> opt_mem "password" Init.string ~enc:(fun c -> c.password) |> mem "ssl" Init.bool ~dec_absent:true ~enc:(fun c -> c.ssl) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "database" database_codec ~enc:Fun.id |> finish ) let example () = (* Minimal config - uses defaults *) let ini = {| [database] host = db.example.com username = admin |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "DB: %s@%s:%d (ssl=%b, password=%s)\n" config.username config.host config.port config.ssl (match config.password with Some _ -> "***" | None -> "none") | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Multiple Sections} Parse a configuration with multiple sections, some required and some optional. *) module Multiple_sections = struct type server = { host : string; port : int } type database = { connection : string; pool_size : int } type cache = { enabled : bool; ttl : int } type config = { server : server; database : database; cache : cache option; (* Optional section *) } let server_codec = Init.Section.( obj (fun host port -> { host; port }) |> mem "host" Init.string ~enc:(fun s -> s.host) |> mem "port" Init.int ~enc:(fun s -> s.port) |> finish ) let database_codec = Init.Section.( obj (fun connection pool_size -> { connection; pool_size }) |> mem "connection" Init.string ~enc:(fun d -> d.connection) |> mem "pool_size" Init.int ~dec_absent:10 ~enc:(fun d -> d.pool_size) |> finish ) let cache_codec = Init.Section.( obj (fun enabled ttl -> { enabled; ttl }) |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled) |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl) |> finish ) let config_codec = Init.Document.( obj (fun server database cache -> { server; database; cache }) |> section "server" server_codec ~enc:(fun c -> c.server) |> section "database" database_codec ~enc:(fun c -> c.database) (* opt_section allows the section to be absent *) |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache) |> finish ) let example () = let ini = {| [server] host = api.example.com port = 443 [database] connection = postgres://localhost/mydb [cache] enabled = yes ttl = 7200 |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "Server: %s:%d\n" config.server.host config.server.port; Printf.printf "Database: %s (pool=%d)\n" config.database.connection config.database.pool_size; (match config.cache with | Some c -> Printf.printf "Cache: enabled=%b ttl=%d\n" c.enabled c.ttl | None -> Printf.printf "Cache: disabled\n") | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Lists and Comma-Separated Values} Parse comma-separated lists of values. *) module Lists = struct type config = { hosts : string list; ports : int list; tags : string list; } let section_codec = Init.Section.( obj (fun hosts ports tags -> { hosts; ports; tags }) |> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts) |> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports) |> mem "tags" (Init.list Init.string) ~dec_absent:[] ~enc:(fun c -> c.tags) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "cluster" section_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [cluster] hosts = node1.example.com, node2.example.com, node3.example.com ports = 8080, 8081, 8082 |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "Hosts: %s\n" (String.concat ", " config.hosts); Printf.printf "Ports: %s\n" (String.concat ", " (List.map string_of_int config.ports)); Printf.printf "Tags: %s\n" (if config.tags = [] then "(none)" else String.concat ", " config.tags) | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Enums and Custom Types} Parse enumerated values and custom types. *) module Enums = struct type log_level = Debug | Info | Warn | Error type environment = Development | Staging | Production type config = { log_level : log_level; environment : environment; max_connections : int; } let log_level_codec = Init.enum [ "debug", Debug; "info", Info; "warn", Warn; "error", Error; ] let environment_codec = Init.enum [ "development", Development; "dev", Development; (* Alias *) "staging", Staging; "production", Production; "prod", Production; (* Alias *) ] let section_codec = Init.Section.( obj (fun log_level environment max_connections -> { log_level; environment; max_connections }) |> mem "log_level" log_level_codec ~dec_absent:Info ~enc:(fun c -> c.log_level) |> mem "environment" environment_codec ~enc:(fun c -> c.environment) |> mem "max_connections" Init.int ~dec_absent:100 ~enc:(fun c -> c.max_connections) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "app" section_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [app] log_level = debug environment = prod |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> let env_str = match config.environment with | Development -> "development" | Staging -> "staging" | Production -> "production" in Printf.printf "Env: %s, Log: %s, MaxConn: %d\n" env_str (match config.log_level with | Debug -> "debug" | Info -> "info" | Warn -> "warn" | Error -> "error") config.max_connections | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Handling Unknown Options} Three strategies for dealing with options you didn't expect. *) module Unknown_options = struct (* Strategy 1: Skip unknown (default) - silently ignore extra options *) module Skip = struct type config = { known_key : string } let section_codec = Init.Section.( obj (fun known_key -> { known_key }) |> mem "known_key" Init.string ~enc:(fun c -> c.known_key) |> skip_unknown (* This is the default *) |> finish ) let _config_codec = Init.Document.( obj Fun.id |> section "test" section_codec ~enc:Fun.id |> finish ) end (* Strategy 2: Error on unknown - strict validation *) module Strict = struct type config = { known_key : string } let section_codec = Init.Section.( obj (fun known_key -> { known_key }) |> mem "known_key" Init.string ~enc:(fun c -> c.known_key) |> error_unknown (* Reject unknown options *) |> finish ) let _config_codec = Init.Document.( obj Fun.id |> section "test" section_codec ~enc:Fun.id |> error_unknown (* Also reject unknown sections *) |> finish ) end (* Strategy 3: Keep unknown - capture for pass-through *) module Passthrough = struct type config = { known_key : string; extra : (string * string) list; } let section_codec = Init.Section.( obj (fun known_key extra -> { known_key; extra }) |> mem "known_key" Init.string ~enc:(fun c -> c.known_key) |> keep_unknown ~enc:(fun c -> c.extra) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "test" section_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [test] known_key = hello extra1 = world extra2 = foo |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "Known: %s\n" config.known_key; List.iter (fun (k, v) -> Printf.printf "Extra: %s = %s\n" k v ) config.extra | Error msg -> Printf.printf "Error: %s\n" msg end end (** {1 Interpolation} Variable substitution in values. *) module Interpolation = struct (* Basic interpolation: %(name)s *) module Basic = struct type paths = { base : string; data : string; logs : string; config : string; } let paths_codec = Init.Section.( obj (fun base data logs config -> { base; data; logs; config }) |> mem "base" Init.string ~enc:(fun p -> p.base) |> mem "data" Init.string ~enc:(fun p -> p.data) |> mem "logs" Init.string ~enc:(fun p -> p.logs) |> mem "config" Init.string ~enc:(fun p -> p.config) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "paths" paths_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [paths] base = /opt/myapp data = %(base)s/data logs = %(base)s/logs config = %(base)s/etc |} in match Init_bytesrw.decode_string config_codec ini with | Ok paths -> Printf.printf "Base: %s\n" paths.base; Printf.printf "Data: %s\n" paths.data; Printf.printf "Logs: %s\n" paths.logs; Printf.printf "Config: %s\n" paths.config | Error msg -> Printf.printf "Error: %s\n" msg end (* Extended interpolation: ${section:name} *) module Extended = struct type common = { base : string } type server = { data_dir : string; log_dir : string } type config = { common : common; server : server } let common_codec = Init.Section.( obj (fun base -> { base }) |> mem "base" Init.string ~enc:(fun c -> c.base) |> finish ) let server_codec = Init.Section.( obj (fun data_dir log_dir -> { data_dir; log_dir }) |> mem "data_dir" Init.string ~enc:(fun s -> s.data_dir) |> mem "log_dir" Init.string ~enc:(fun s -> s.log_dir) |> finish ) let config_codec = Init.Document.( obj (fun common server -> { common; server }) |> section "common" common_codec ~enc:(fun c -> c.common) |> section "server" server_codec ~enc:(fun c -> c.server) |> finish ) let example () = let config = { Init_bytesrw.default_config with interpolation = `Extended_interpolation } in let ini = {| [common] base = /opt/myapp [server] data_dir = ${common:base}/data log_dir = ${common:base}/logs |} in match Init_bytesrw.decode_string ~config config_codec ini with | Ok cfg -> Printf.printf "Base: %s\n" cfg.common.base; Printf.printf "Data: %s\n" cfg.server.data_dir; Printf.printf "Log: %s\n" cfg.server.log_dir | Error msg -> Printf.printf "Error: %s\n" msg end end (** {1 The DEFAULT Section} The DEFAULT section provides fallback values for all other sections. *) module Defaults = struct type section = { host : string; port : int; timeout : int; (* Falls back to DEFAULT *) } type config = { production : section; staging : section; } let section_codec = Init.Section.( obj (fun host port timeout -> { host; port; timeout }) |> mem "host" Init.string ~enc:(fun s -> s.host) |> mem "port" Init.int ~enc:(fun s -> s.port) |> mem "timeout" Init.int ~enc:(fun s -> s.timeout) |> finish ) let config_codec = Init.Document.( obj (fun production staging -> { production; staging }) |> section "production" section_codec ~enc:(fun c -> c.production) |> section "staging" section_codec ~enc:(fun c -> c.staging) |> skip_unknown (* Ignore DEFAULT section in output *) |> finish ) let example () = let ini = {| [DEFAULT] timeout = 30 [production] host = api.example.com port = 443 [staging] host = staging.example.com port = 8443 timeout = 60 |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "Production: %s:%d (timeout=%d)\n" config.production.host config.production.port config.production.timeout; Printf.printf "Staging: %s:%d (timeout=%d)\n" config.staging.host config.staging.port config.staging.timeout | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Round-Trip Encoding} Decode, modify, and re-encode a configuration. *) module Roundtrip = struct type config = { host : string; port : int; } let section_codec = Init.Section.( obj (fun host port -> { host; port }) |> mem "host" Init.string ~enc:(fun c -> c.host) |> mem "port" Init.int ~enc:(fun c -> c.port) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "server" section_codec ~enc:Fun.id |> finish ) let example () = (* Decode *) let ini = {| [server] host = localhost port = 8080 |} in match Init_bytesrw.decode_string config_codec ini with | Error msg -> Printf.printf "Decode error: %s\n" msg | Ok config -> Printf.printf "Decoded: %s:%d\n" config.host config.port; (* Modify *) let modified = { config with port = 9000 } in (* Encode *) (match Init_bytesrw.encode_string config_codec modified with | Ok output -> Printf.printf "Encoded:\n%s" output | Error msg -> Printf.printf "Encode error: %s\n" msg) end (** {1 Custom Boolean Formats} Different applications use different boolean representations. *) module Custom_booleans = struct type config = { python_style : bool; (* 1/yes/true/on or 0/no/false/off *) strict_01 : bool; (* Only 0 or 1 *) yes_no : bool; (* Only yes or no *) on_off : bool; (* Only on or off *) } let section_codec = Init.Section.( obj (fun python_style strict_01 yes_no on_off -> { python_style; strict_01; yes_no; on_off }) |> mem "python_style" Init.bool ~enc:(fun c -> c.python_style) |> mem "strict_01" Init.bool_01 ~enc:(fun c -> c.strict_01) |> mem "yes_no" Init.bool_yesno ~enc:(fun c -> c.yes_no) |> mem "on_off" Init.bool_onoff ~enc:(fun c -> c.on_off) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "flags" section_codec ~enc:Fun.id |> finish ) let example () = let ini = {| [flags] python_style = YES strict_01 = 1 yes_no = no on_off = on |} in match Init_bytesrw.decode_string config_codec ini with | Ok config -> Printf.printf "python_style=%b strict_01=%b yes_no=%b on_off=%b\n" config.python_style config.strict_01 config.yes_no config.on_off | Error msg -> Printf.printf "Error: %s\n" msg end (** {1 Error Handling} Demonstrate different error scenarios and how to handle them. *) module Error_handling = struct type config = { port : int } let section_codec = Init.Section.( obj (fun port -> { port }) |> mem "port" Init.int ~enc:(fun c -> c.port) |> finish ) let config_codec = Init.Document.( obj Fun.id |> section "server" section_codec ~enc:Fun.id |> finish ) let example () = (* Missing section *) let ini1 = {| [wrong_name] port = 8080 |} in (match Init_bytesrw.decode_string config_codec ini1 with | Ok _ -> Printf.printf "Unexpected success\n" | Error msg -> Printf.printf "Missing section: %s\n\n" msg); (* Missing option *) let ini2 = {| [server] host = localhost |} in (match Init_bytesrw.decode_string config_codec ini2 with | Ok _ -> Printf.printf "Unexpected success\n" | Error msg -> Printf.printf "Missing option: %s\n\n" msg); (* Type mismatch *) let ini3 = {| [server] port = not_a_number |} in (match Init_bytesrw.decode_string config_codec ini3 with | Ok _ -> Printf.printf "Unexpected success\n" | Error msg -> Printf.printf "Type mismatch: %s\n\n" msg); (* Using structured errors *) let ini4 = {| [server] port = abc |} in match Init_bytesrw.decode_string' config_codec ini4 with | Ok _ -> Printf.printf "Unexpected success\n" | Error e -> Printf.printf "Structured error:\n"; Printf.printf " Kind: %s\n" (Init.Error.kind_to_string (Init.Error.kind e)); Format.printf " Path: %a\n" Init.Path.pp (Init.Error.path e) end (** {1 Running Examples} *) let () = Printf.printf "=== Basic Configuration ===\n"; Basic.example (); Printf.printf "\n"; Printf.printf "=== Optional Values ===\n"; Optional_values.example (); Printf.printf "\n"; Printf.printf "=== Multiple Sections ===\n"; Multiple_sections.example (); Printf.printf "\n"; Printf.printf "=== Lists ===\n"; Lists.example (); Printf.printf "\n"; Printf.printf "=== Enums ===\n"; Enums.example (); Printf.printf "\n"; Printf.printf "=== Unknown Options (Passthrough) ===\n"; Unknown_options.Passthrough.example (); Printf.printf "\n"; Printf.printf "=== Basic Interpolation ===\n"; Interpolation.Basic.example (); Printf.printf "\n"; Printf.printf "=== Extended Interpolation ===\n"; Interpolation.Extended.example (); Printf.printf "\n"; Printf.printf "=== DEFAULT Section ===\n"; Defaults.example (); Printf.printf "\n"; Printf.printf "=== Round-Trip Encoding ===\n"; Roundtrip.example (); Printf.printf "\n"; Printf.printf "=== Custom Booleans ===\n"; Custom_booleans.example (); Printf.printf "\n"; Printf.printf "=== Error Handling ===\n"; Error_handling.example ()