···5566(** INI parser and encoder using bytesrw.
7788- Implements Python configparser semantics including:
99- - Multiline values via indentation
1010- - Basic interpolation: [%(name)s]
1111- - Extended interpolation: [$\{section:name\}]
1212- - DEFAULT section inheritance
1313- - Case-insensitive option lookup
88+ This module provides functions to parse and encode INI files using the
99+ {{:https://erratique.ch/software/bytesrw}bytesrw} streaming I/O library.
1010+ It implements {{:https://docs.python.org/3/library/configparser.html}Python's
1111+ configparser} semantics for maximum compatibility.
1212+1313+ {1:basic_usage Basic Usage}
14141515- See notes about {{!layout}layout preservation}. *)
1515+ {@ocaml[
1616+ (* Define your configuration type and codec *)
1717+ let config_codec = Init.Document.(
1818+ obj (fun server -> server)
1919+ |> section "server" server_codec ~enc:Fun.id
2020+ |> finish
2121+ )
2222+2323+ (* Decode from a string *)
2424+ match Init_bytesrw.decode_string config_codec ini_text with
2525+ | Ok config -> (* use config *)
2626+ | Error msg -> (* handle error *)
2727+2828+ (* Encode back to a string *)
2929+ match Init_bytesrw.encode_string config_codec config with
3030+ | Ok text -> (* write text *)
3131+ | Error msg -> (* handle error *)
3232+ ]}
3333+3434+ {1:python_compat Python Compatibility}
3535+3636+ This parser implements the same semantics as Python's [configparser] module.
3737+ Configuration files that work with Python will work here, and vice versa.
3838+3939+ {2:syntax Supported Syntax}
4040+4141+ {@ini[
4242+ # Comments start with # or ;
4343+ ; This is also a comment
4444+4545+ [section]
4646+ key = value
4747+ key2 : value2 ; Both = and : are delimiters
4848+ key3=no spaces needed
4949+5050+ [multiline]
5151+ long_value = This is a long value
5252+ that continues on indented lines
5353+ for as long as needed
5454+5555+ [types]
5656+ integer = 42
5757+ float = 3.14
5858+ boolean = yes ; Also: true, on, 1, no, false, off, 0
5959+ list = a, b, c, d
6060+ ]}
6161+6262+ {2:edge_cases Edge Cases and Gotchas}
6363+6464+ {ul
6565+ {- {b Section names are case-sensitive}: [[Server]] and [[server]] are
6666+ different.}
6767+ {- {b Option names are case-insensitive}: [Port] and [port] are the same.}
6868+ {- {b Whitespace is trimmed} from keys and values automatically.}
6969+ {- {b Empty values are allowed}: [key =] gives an empty string.}
7070+ {- {b Comments are NOT preserved} during round-trips (matching Python).}
7171+ {- {b Inline comments are disabled by default}: [key = value ; comment]
7272+ gives the value ["value ; comment"] unless you configure
7373+ {!field-inline_comment_prefixes}.}} *)
16741775open Bytesrw
18761919-(** {1:config Configuration} *)
7777+(** {1:config Parser Configuration}
7878+7979+ Configure the parser to match different INI dialects. The default
8080+ configuration matches Python's [ConfigParser]. *)
20812182type interpolation =
2222- | No_interpolation (** RawConfigParser behavior. *)
2323- | Basic_interpolation (** [%(name)s] syntax. *)
2424- | Extended_interpolation (** [$\{section:name\}] syntax. *)
2525-(** The type for interpolation modes. *)
8383+ [ `No_interpolation
8484+ (** No variable substitution. Values like ["%(foo)s"] are returned
8585+ literally. Equivalent to Python's [RawConfigParser].
8686+8787+ Use this for configuration files that contain literal [%] or [$]
8888+ characters that shouldn't be interpreted. *)
8989+ | `Basic_interpolation
9090+ (** Basic variable substitution using [%(name)s] syntax (default).
9191+ Equivalent to Python's [ConfigParser] default.
9292+9393+ Variables reference options in the current section or the DEFAULT
9494+ section:
9595+ {@ini[
9696+ [paths]
9797+ base = /opt/app
9898+ data = %(base)s/data ; Becomes "/opt/app/data"
9999+ ]}
100100+101101+ {b Escaping:} Use [%%] to get a literal [%]. *)
102102+ | `Extended_interpolation
103103+ (** Extended substitution using [$\{section:name\}] syntax.
104104+ Equivalent to Python's [ExtendedInterpolation].
105105+106106+ Variables can reference options in any section:
107107+ {@ini[
108108+ [common]
109109+ base = /opt/app
110110+111111+ [server]
112112+ data = ${common:base}/data ; Cross-section reference
113113+ logs = ${base}/logs ; Same section or DEFAULT
114114+ ]}
115115+116116+ {b Escaping:} Use [$$] to get a literal [$]. *)
117117+ ]
118118+(** The type for interpolation modes. Controls how variable references
119119+ in values are expanded.
120120+121121+ {b Recursion limit:} Interpolation follows references up to 10 levels
122122+ deep to prevent infinite loops. Deeper nesting raises an error.
123123+124124+ {b Missing references:} If a referenced option doesn't exist, decoding
125125+ fails with {!Init.Error.Interpolation}. *)
2612627127type config = {
28128 delimiters : string list;
2929- (** Key-value delimiters. Default: [["="; ":"]]. *)
129129+ (** Characters that separate option names from values.
130130+ Default: [["="; ":"]].
131131+132132+ The {e first} delimiter on a line is used, so values can contain
133133+ delimiter characters:
134134+ {@ini[
135135+ url = https://example.com:8080 ; Colon in value is fine
136136+ ]} *)
3013731138 comment_prefixes : string list;
3232- (** Full-line comment prefixes. Default: [["#"; ";"]]. *)
139139+ (** Prefixes that start full-line comments. Default: [["#"; ";"]].
140140+141141+ A line starting with any of these (after optional whitespace) is
142142+ treated as a comment and ignored. *)
3314334144 inline_comment_prefixes : string list;
3535- (** Inline comment prefixes (require preceding whitespace).
3636- Default: [[]] (disabled). *)
145145+ (** Prefixes that start inline comments. Default: [[]] (disabled).
146146+147147+ {b Warning:} Enabling inline comments (e.g., [[";"]]) prevents using
148148+ those characters in values. For example:
149149+ {@ini[
150150+ url = https://example.com;port=8080 ; Would be truncated!
151151+ ]}
152152+153153+ A space must precede inline comments: [value;comment] keeps the
154154+ semicolon, but [value ; comment] removes it. *)
3715538156 default_section : string;
3939- (** Name of the default section. Default: ["DEFAULT"]. *)
157157+ (** Name of the default section. Default: ["DEFAULT"].
158158+159159+ Options in this section are inherited by all other sections and
160160+ available for interpolation. You can customize this, e.g., to
161161+ ["general"] or ["common"]. *)
4016241163 interpolation : interpolation;
4242- (** Interpolation mode. Default: {!Basic_interpolation}. *)
164164+ (** How to handle variable references. Default: [`Basic_interpolation].
165165+166166+ See {!type-interpolation} for details on each mode. *)
4316744168 allow_no_value : bool;
4545- (** Allow options without values. Default: [false]. *)
169169+ (** Allow options without values. Default: [false].
170170+171171+ When [true], options can appear without a delimiter:
172172+ {@ini[
173173+ [mysqld]
174174+ skip-innodb ; No = sign, value is None
175175+ port = 3306
176176+ ]}
177177+178178+ Such options decode as [None] when using {!Init.option}. *)
4617947180 strict : bool;
4848- (** Error on duplicate sections/options. Default: [true]. *)
181181+ (** Reject duplicate sections and options. Default: [true].
182182+183183+ When [true], if the same section or option appears twice, decoding
184184+ fails with {!Init.Error.Duplicate_section} or
185185+ {!Init.Error.Duplicate_option}.
186186+187187+ When [false], later values silently override earlier ones. *)
4918850189 empty_lines_in_values : bool;
5151- (** Allow empty lines in multiline values. Default: [true]. *)
190190+ (** Allow empty lines in multiline values. Default: [true].
191191+192192+ When [true], empty lines can be part of multiline values:
193193+ {@ini[
194194+ [section]
195195+ key = line 1
196196+197197+ line 3 ; Empty line 2 is preserved
198198+ ]}
199199+200200+ When [false], empty lines terminate the multiline value. *)
52201}
5353-(** The type for parser configuration. *)
202202+(** Parser configuration. Adjust these settings to parse different INI
203203+ dialects or to match specific Python configparser settings. *)
5420455205val default_config : config
5656-(** [default_config] is the default configuration matching Python's
5757- [configparser.ConfigParser]. *)
206206+(** Default configuration matching Python's [configparser.ConfigParser]:
207207+208208+ {ul
209209+ {- [delimiters = ["="; ":"]]}
210210+ {- [comment_prefixes = ["#"; ";"]]}
211211+ {- [inline_comment_prefixes = []] (disabled)}
212212+ {- [default_section = "DEFAULT"]}
213213+ {- [interpolation = `Basic_interpolation]}
214214+ {- [allow_no_value = false]}
215215+ {- [strict = true]}
216216+ {- [empty_lines_in_values = true]}} *)
5821759218val raw_config : config
6060-(** [raw_config] is configuration with no interpolation, matching
6161- Python's [configparser.RawConfigParser]. *)
219219+(** Configuration matching Python's [configparser.RawConfigParser]:
220220+ same as {!default_config} but with [interpolation = `No_interpolation].
221221+222222+ Use this when your values contain literal [%] or [$] characters. *)
223223+224224+225225+(** {1:decode Decoding}
622266363-(** {1:decode Decode} *)
227227+ Parse INI data into OCaml values. All decode functions return
228228+ [Result.t] - they never raise exceptions for parse errors. *)
6422965230val decode :
66231 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
67232 'a Init.t -> Bytes.Reader.t -> ('a, string) result
6868-(** [decode codec r] decodes a value from [r] according to [codec].
233233+(** [decode codec r] decodes INI data from reader [r] using [codec].
234234+69235 {ul
7070- {- [config] is the parser configuration. Defaults to {!default_config}.}
7171- {- If [locs] is [true] locations are preserved in metadata.
7272- Defaults to [false].}
7373- {- If [layout] is [true] whitespace is preserved in metadata.
7474- Defaults to [false].}
7575- {- [file] is the file path for error messages.
7676- Defaults to {!Init.Textloc.file_none}.}} *)
236236+ {- [config] configures the parser. Default: {!default_config}.}
237237+ {- [locs] if [true], preserves source locations in metadata.
238238+ Default: [false].}
239239+ {- [layout] if [true], preserves whitespace in metadata for
240240+ layout-preserving round-trips. Default: [false].}
241241+ {- [file] is the file path for error messages. Default: ["-"].}}
242242+243243+ Returns [Ok value] on success or [Error message] on failure, where
244244+ [message] includes location information when available. *)
7724578246val decode' :
79247 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
80248 'a Init.t -> Bytes.Reader.t -> ('a, Init.Error.t) result
8181-(** [decode'] is like {!val-decode} but preserves the error structure. *)
249249+(** [decode'] is like {!val-decode} but returns a structured error
250250+ with separate {!Init.Error.type-kind}, location, and path information.
251251+252252+ Use this when you need to programmatically handle different error
253253+ types or extract location information. *)
8225483255val decode_string :
84256 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
85257 'a Init.t -> string -> ('a, string) result
8686-(** [decode_string] is like {!val-decode} but decodes from a string. *)
258258+(** [decode_string codec s] decodes INI data from string [s].
259259+260260+ This is the most common entry point for parsing:
261261+ {@ocaml[
262262+ let ini_text = {|
263263+ [server]
264264+ host = localhost
265265+ port = 8080
266266+ |} in
267267+ Init_bytesrw.decode_string config_codec ini_text
268268+ ]} *)
8726988270val decode_string' :
89271 ?config:config -> ?locs:bool -> ?layout:bool -> ?file:Init.Textloc.fpath ->
90272 'a Init.t -> string -> ('a, Init.Error.t) result
9191-(** [decode_string'] is like {!val-decode'} but decodes from a string. *)
273273+(** [decode_string'] is like {!val-decode_string} with structured errors. *)
274274+275275+276276+(** {1:encode Encoding}
922779393-(** {1:encode Encode} *)
278278+ Serialize OCaml values to INI format. *)
9427995280val encode :
96281 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
97282 (unit, string) result
9898-(** [encode codec v w] encodes [v] according to [codec] on [w].
283283+(** [encode codec v ~eod w] encodes [v] to writer [w] using [codec].
284284+99285 {ul
100100- {- [buf] is an optional buffer for writing.}
101101- {- [eod] indicates whether to write end-of-data.}} *)
286286+ {- [buf] is an optional scratch buffer for writing.}
287287+ {- [eod] if [true], signals end-of-data after writing.}}
288288+289289+ The output format follows standard INI conventions:
290290+ - Sections are written as [[section_name]]
291291+ - Options are written as [key = value]
292292+ - Multiline values are continued with indentation *)
102293103294val encode' :
104295 ?buf:Bytes.t -> 'a Init.t -> 'a -> eod:bool -> Bytes.Writer.t ->
105296 (unit, Init.Error.t) result
106106-(** [encode'] is like {!val-encode} but preserves the error structure. *)
297297+(** [encode'] is like {!val-encode} with structured errors. *)
107298108299val encode_string :
109300 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, string) result
110110-(** [encode_string] is like {!val-encode} but writes to a string. *)
301301+(** [encode_string codec v] encodes [v] to a string.
302302+303303+ {@ocaml[
304304+ let config = { server = { host = "localhost"; port = 8080 } } in
305305+ match Init_bytesrw.encode_string config_codec config with
306306+ | Ok text -> print_endline text
307307+ | Error msg -> failwith msg
308308+ ]}
309309+310310+ Produces:
311311+ {@ini[
312312+ [server]
313313+ host = localhost
314314+ port = 8080
315315+ ]} *)
111316112317val encode_string' :
113318 ?buf:Bytes.t -> 'a Init.t -> 'a -> (string, Init.Error.t) result
114114-(** [encode_string'] is like {!val-encode'} but writes to a string. *)
319319+(** [encode_string'] is like {!val-encode_string} with structured errors. *)
320320+321321+322322+(** {1:layout Layout Preservation}
115323116116-(** {1:layout Layout preservation}
324324+ When decoding with [~layout:true], whitespace and comment positions
325325+ are preserved in the {!Init.Meta.t} values attached to each element.
326326+ When re-encoding, this information is used to reproduce the original
327327+ formatting as closely as possible.
117328118118- When [layout:true] is passed to decode functions, whitespace and
119119- comments are preserved in {!Init.Meta.t} values. This enables
120120- layout-preserving round-trips where the original formatting is
121121- maintained as much as possible. *)
329329+ {b Limitations:}
330330+ {ul
331331+ {- Comments are NOT preserved (matching Python's behavior).}
332332+ {- Whitespace within values may be normalized.}
333333+ {- The output may differ slightly from the input in edge cases.}}
334334+335335+ {b Performance tip:} For maximum performance when you don't need
336336+ layout preservation, use [~layout:false ~locs:false] (the default).
337337+ Enabling [~locs:true] improves error messages at a small cost. *)
338338+339339+340340+(** {1:examples Examples}
341341+342342+ {2:simple Simple Configuration}
343343+344344+ {@ocaml[
345345+ type config = { debug : bool; port : int }
346346+347347+ let codec = Init.Document.(
348348+ let section = Init.Section.(
349349+ obj (fun debug port -> { debug; port })
350350+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
351351+ |> mem "port" Init.int ~dec_absent:8080 ~enc:(fun c -> c.port)
352352+ |> finish
353353+ ) in
354354+ obj Fun.id
355355+ |> section "server" section ~enc:Fun.id
356356+ |> finish
357357+ )
358358+359359+ let config = Init_bytesrw.decode_string codec "[server]\nport = 9000"
360360+ (* Ok { debug = false; port = 9000 } *)
361361+ ]}
362362+363363+ {2:multi_section Multiple Sections}
364364+365365+ {@ocaml[
366366+ type db = { host : string; port : int }
367367+ type cache = { enabled : bool; ttl : int }
368368+ type config = { db : db; cache : cache option }
369369+370370+ let db_codec = Init.Section.(
371371+ obj (fun host port -> { host; port })
372372+ |> mem "host" Init.string ~enc:(fun d -> d.host)
373373+ |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun d -> d.port)
374374+ |> finish
375375+ )
376376+377377+ let cache_codec = Init.Section.(
378378+ obj (fun enabled ttl -> { enabled; ttl })
379379+ |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
380380+ |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
381381+ |> finish
382382+ )
383383+384384+ let config_codec = Init.Document.(
385385+ obj (fun db cache -> { db; cache })
386386+ |> section "database" db_codec ~enc:(fun c -> c.db)
387387+ |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
388388+ |> finish
389389+ )
390390+ ]}
391391+392392+ {2:interpolation_example Interpolation}
393393+394394+ {@ocaml[
395395+ let paths_codec = Init.Section.(
396396+ obj (fun base data logs -> (base, data, logs))
397397+ |> mem "base" Init.string ~enc:(fun (b,_,_) -> b)
398398+ |> mem "data" Init.string ~enc:(fun (_,d,_) -> d)
399399+ |> mem "logs" Init.string ~enc:(fun (_,_,l) -> l)
400400+ |> finish
401401+ )
402402+403403+ let doc_codec = Init.Document.(
404404+ obj Fun.id
405405+ |> section "paths" paths_codec ~enc:Fun.id
406406+ |> finish
407407+ )
408408+409409+ (* Basic interpolation expands %(base)s *)
410410+ let ini = {|
411411+ [paths]
412412+ base = /opt/app
413413+ data = %(base)s/data
414414+ logs = %(base)s/logs
415415+ |}
416416+417417+ match Init_bytesrw.decode_string doc_codec ini with
418418+ | Ok (_, data, logs) ->
419419+ assert (data = "/opt/app/data");
420420+ assert (logs = "/opt/app/logs")
421421+ | Error _ -> assert false
422422+ ]}
423423+424424+ {2:raw_parser Disabling Interpolation}
425425+426426+ {@ocaml[
427427+ (* Use raw_config for files with literal % characters *)
428428+ let config = Init_bytesrw.raw_config
429429+430430+ let result = Init_bytesrw.decode_string ~config codec {|
431431+ [display]
432432+ format = 100%% complete ; Would fail with basic interpolation
433433+ |}
434434+ ]} *)
+610-144
src/init.mli
···5566(** Declarative INI data manipulation for OCaml.
7788- Init provides bidirectional codecs for INI files following Python's
99- configparser semantics. The core module has no dependencies.
88+ Init provides bidirectional codecs for INI configuration files following
99+ {{:https://docs.python.org/3/library/configparser.html}Python's
1010+ configparser} semantics. This ensures configuration files are compatible
1111+ with Python tools while providing a type-safe OCaml interface.
1212+1313+ {1:quick_start Quick Start}
1414+1515+ INI files consist of sections (in square brackets) containing key-value
1616+ pairs. Here's a simple configuration file:
1717+1818+ {@ini[
1919+ [server]
2020+ host = localhost
2121+ port = 8080
2222+ debug = false
2323+2424+ [database]
2525+ connection_string = postgres://localhost/mydb
2626+ pool_size = 10
2727+ ]}
2828+2929+ To decode this, define an OCaml type and create a codec:
3030+3131+ {@ocaml[
3232+ (* Define the OCaml types *)
3333+ type server = { host : string; port : int; debug : bool }
3434+ type database = { connection : string; pool_size : int }
3535+ type config = { server : server; database : database }
3636+3737+ (* Create section codecs *)
3838+ let server_codec = Init.Section.(
3939+ obj (fun host port debug -> { host; port; debug })
4040+ |> mem "host" Init.string ~enc:(fun s -> s.host)
4141+ |> mem "port" Init.int ~enc:(fun s -> s.port)
4242+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun s -> s.debug)
4343+ |> finish
4444+ )
4545+4646+ let database_codec = Init.Section.(
4747+ obj (fun connection pool_size -> { connection; pool_size })
4848+ |> mem "connection_string" Init.string ~enc:(fun d -> d.connection)
4949+ |> mem "pool_size" Init.int ~dec_absent:5 ~enc:(fun d -> d.pool_size)
5050+ |> finish
5151+ )
5252+5353+ (* Create the document codec *)
5454+ let config_codec = Init.Document.(
5555+ obj (fun server database -> { server; database })
5656+ |> section "server" server_codec ~enc:(fun c -> c.server)
5757+ |> section "database" database_codec ~enc:(fun c -> c.database)
5858+ |> finish
5959+ )
6060+6161+ (* Use Init_bytesrw to decode *)
6262+ let config = Init_bytesrw.decode_string config_codec ini_string
6363+ ]}
6464+6565+ Read the {{!page-cookbook}cookbook} for more examples.
6666+6767+ {1:concepts Key Concepts}
6868+6969+ {2:ini_format The INI Format}
7070+7171+ An INI file is a simple text format for configuration data:
7272+7373+ {ul
7474+ {- {b Sections} are named groups enclosed in square brackets: [[section]].
7575+ Section names are case-sensitive and can contain spaces.}
7676+ {- {b Options} (also called keys) are name-value pairs separated by
7777+ [=] or [:]. Option names are {e case-insensitive} by default.}
7878+ {- {b Values} are strings. All data is stored as strings and converted
7979+ to other types (int, bool, etc.) by codecs during decoding.}
8080+ {- {b Comments} start with [#] or [;] at the beginning of a line.}
8181+ {- {b Multiline values} are continued on indented lines.}}
8282+8383+ {2:defaults_section The DEFAULT Section}
8484+8585+ The special [[DEFAULT]] section provides fallback values for all other
8686+ sections. When an option is not found in a section, the parser looks
8787+ in DEFAULT:
8888+8989+ {@ini[
9090+ [DEFAULT]
9191+ base_dir = /opt/app
9292+ log_level = info
9393+9494+ [production]
9595+ base_dir = /var/app
9696+9797+ [development]
9898+ # Inherits base_dir from DEFAULT
9999+ log_level = debug
100100+ ]}
101101+102102+ {2:interpolation Value Interpolation}
103103+104104+ Values can reference other values using interpolation syntax:
105105+106106+ {b Basic interpolation} (Python's ConfigParser default):
107107+108108+ {@ini[
109109+ [paths]
110110+ base = /opt/app
111111+ data = %(base)s/data
112112+ logs = %(base)s/logs
113113+ ]}
114114+115115+ {b Extended interpolation} (cross-section references):
116116+117117+ {@ini[
118118+ [common]
119119+ base = /opt/app
120120+121121+ [server]
122122+ data_dir = ${common:base}/data
123123+ ]}
124124+125125+ See {!Init_bytesrw.type-interpolation} for configuration options.
126126+127127+ {2:booleans Boolean Values}
128128+129129+ Following Python's configparser, these values are recognized as booleans
130130+ (case-insensitive):
101311111- {b Features:}
1212- - Multiline values via indentation
1313- - Basic interpolation: [%(name)s]
1414- - Extended interpolation: [$\{section:name\}]
1515- - DEFAULT section inheritance
1616- - Case-insensitive option lookup
1717- - Layout preservation (whitespace and comments)
132132+ {ul
133133+ {- {b True}: [1], [yes], [true], [on]}
134134+ {- {b False}: [0], [no], [false], [off]}}
135135+136136+ Use {!bool} for Python-compatible parsing, or the stricter variants
137137+ {!bool_01}, {!bool_yesno}, {!bool_truefalse}, {!bool_onoff}.
181381919- {b Sub-libraries:}
2020- - {!Init_bytesrw} for parsing/encoding with bytesrw
2121- - {!Init_eio} for Eio file system integration *)
139139+ {2:case_sensitivity Case Sensitivity}
140140+141141+ By default (matching Python's behavior):
142142+ {ul
143143+ {- Section names are {b case-sensitive}: [[Server]] and [[server]] are
144144+ different sections.}
145145+ {- Option names are {b case-insensitive}: [Port] and [port] refer to the
146146+ same option. Options are normalized to lowercase internally.}}
147147+148148+ {1:architecture Library Architecture}
149149+150150+ The library is split into multiple packages:
151151+152152+ {ul
153153+ {- {b init} (this module): Core types and codec combinators. No I/O
154154+ dependencies.}
155155+ {- {b init.bytesrw} ({!Init_bytesrw}): Parsing and encoding using
156156+ {{:https://erratique.ch/software/bytesrw}bytesrw}.}
157157+ {- {b init.eio}: File system integration with
158158+ {{:https://github.com/ocaml-multicore/eio}Eio}.}}
159159+160160+ {1:error_handling Error Handling}
161161+162162+ All decode functions return [('a, string) result] or [('a, Error.t) result].
163163+ Common error cases:
164164+165165+ {ul
166166+ {- {!Error.Missing_section}: Required section not found in file.}
167167+ {- {!Error.Missing_option}: Required option not found in section.}
168168+ {- {!Error.Type_mismatch}: Value could not be converted (e.g., ["abc"]
169169+ as an integer).}
170170+ {- {!Error.Interpolation}: Variable reference could not be resolved.}
171171+ {- {!Error.Duplicate_section}, {!Error.Duplicate_option}: In strict mode,
172172+ duplicates are errors.}}
173173+174174+ {1:cookbook Cookbook Patterns}
175175+176176+ See the {{!page-cookbook}cookbook} for detailed examples:
177177+178178+ {ul
179179+ {- {{!page-cookbook.optional_values}Optional values and defaults}}
180180+ {- {{!page-cookbook.lists}Lists and comma-separated values}}
181181+ {- {{!page-cookbook.unknown_options}Handling unknown options}}
182182+ {- {{!page-cookbook.interpolation}Interpolation (basic and extended)}}
183183+ {- {{!page-cookbook.roundtrip}Layout-preserving round-trips}}} *)
2218423185type 'a fmt = Format.formatter -> 'a -> unit
2424-(** The type for formatters. *)
186186+(** The type for formatters of values of type ['a]. *)
187187+188188+189189+(** {1:textlocs Text Locations}
251902626-(** {1:textlocs Text Locations} *)
191191+ Text locations track where elements appear in source files. This enables
192192+ good error messages and layout-preserving round-trips. *)
2719328194(** Text locations.
2919530196 A text location identifies a text span in a given file by an inclusive
3131- byte position range and the start position on lines. *)
197197+ byte position range and the start position on lines. Use these to
198198+ provide precise error messages pointing to the source. *)
32199module Textloc : sig
3320034201 (** {1:fpath File paths} *)
···37204 (** The type for file paths. *)
3820539206 val file_none : fpath
4040- (** [file_none] is ["-"]. A file path for when there is none. *)
207207+ (** [file_none] is ["-"]. A placeholder file path when there is no file
208208+ (e.g., when parsing from a string). *)
412094242- (** {1:pos Positions} *)
210210+ (** {1:pos Positions}
211211+212212+ Positions identify locations within text. Byte positions are zero-based
213213+ absolute offsets. Line positions combine a one-based line number with
214214+ the byte position of the line's start. *)
4321544216 type byte_pos = int
4545- (** The type for zero-based byte positions in text. *)
217217+ (** The type for zero-based, absolute byte positions in text. If the text
218218+ has [n] bytes, [0] is the first byte and [n-1] is the last. *)
4621947220 val byte_pos_none : byte_pos
4848- (** [byte_pos_none] is [-1]. A position to use when there is none. *)
221221+ (** [byte_pos_none] is [-1]. A sentinel value indicating no position. *)
4922250223 type line_num = int
5151- (** The type for one-based line numbers. *)
224224+ (** The type for one-based line numbers. The first line is line [1]. *)
5222553226 val line_num_none : line_num
5454- (** [line_num_none] is [-1]. A line number to use when there is none. *)
227227+ (** [line_num_none] is [-1]. A sentinel value indicating no line number. *)
5522856229 type line_pos = line_num * byte_pos
5757- (** The type for line positions. A one-based line number and the
5858- byte position of the first byte of the line. *)
230230+ (** The type for line positions. A pair of:
231231+ {ul
232232+ {- A one-based line number.}
233233+ {- The absolute byte position of the first character of that line.}} *)
5923460235 val line_pos_first : line_pos
6161- (** [line_pos_first] is [(1, 0)]. *)
236236+ (** [line_pos_first] is [(1, 0)]. The position of the first line. *)
6223763238 val line_pos_none : line_pos
6464- (** [line_pos_none] is [(line_num_none, byte_pos_none)]. *)
239239+ (** [line_pos_none] is [(line_num_none, byte_pos_none)]. Indicates no
240240+ line position. *)
652416666- (** {1:tlocs Text locations} *)
242242+ (** {1:tlocs Text locations}
243243+244244+ A text location spans from a first position to a last position
245245+ (inclusive), tracking both byte offsets and line numbers. *)
6724668247 type t
6969- (** The type for text locations. A text location identifies a text span
7070- in a file by an inclusive byte position range and its line positions. *)
248248+ (** The type for text locations. A text location identifies a span of
249249+ text in a file by its start and end positions. *)
7125072251 val none : t
7373- (** [none] is a text location with no information. *)
252252+ (** [none] is a location with no information. Use {!is_none} to test. *)
7425375254 val make :
76255 file:fpath ->
77256 first_byte:byte_pos -> last_byte:byte_pos ->
78257 first_line:line_pos -> last_line:line_pos -> t
7979- (** [make ~file ~first_byte ~last_byte ~first_line ~last_line] is a text
8080- location with the given data. *)
258258+ (** [make ~file ~first_byte ~last_byte ~first_line ~last_line] creates
259259+ a text location spanning from [first_byte] to [last_byte] in [file].
260260+ The line positions provide human-readable context.
261261+262262+ Use {!file_none} if there is no file (e.g., parsing from a string). *)
8126382264 val file : t -> fpath
8383- (** [file l] is the file of [l]. *)
265265+ (** [file l] is the file path of [l]. *)
8426685267 val set_file : t -> fpath -> t
8686- (** [set_file l f] is [l] with [file] set to [f]. *)
268268+ (** [set_file l f] is [l] with the file set to [f]. *)
8726988270 val first_byte : t -> byte_pos
8989- (** [first_byte l] is the first byte position of [l]. *)
271271+ (** [first_byte l] is the byte position where [l] starts (inclusive). *)
9027291273 val last_byte : t -> byte_pos
9292- (** [last_byte l] is the last byte position of [l]. *)
274274+ (** [last_byte l] is the byte position where [l] ends (inclusive). *)
9327594276 val first_line : t -> line_pos
9595- (** [first_line l] is the first line position of [l]. *)
277277+ (** [first_line l] is the line position where [l] starts. *)
9627897279 val last_line : t -> line_pos
9898- (** [last_line l] is the last line position of [l]. *)
280280+ (** [last_line l] is the line position where [l] ends. *)
281281+282282+ (** {2:preds Predicates and comparisons} *)
99283100284 val is_none : t -> bool
101285 (** [is_none l] is [true] iff [first_byte l < 0]. *)
102286103287 val is_empty : t -> bool
104104- (** [is_empty l] is [true] iff [first_byte l > last_byte l]. *)
288288+ (** [is_empty l] is [true] iff [first_byte l > last_byte l]. An empty
289289+ location represents a position between characters. *)
105290106291 val equal : t -> t -> bool
107107- (** [equal l0 l1] tests [l0] and [l1] for equality. *)
292292+ (** [equal l0 l1] is [true] iff [l0] and [l1] are equal. *)
108293109294 val compare : t -> t -> int
110110- (** [compare l0 l1] is a total order on locations. *)
295295+ (** [compare l0 l1] is a total order on locations, comparing by file,
296296+ then start position, then end position. *)
297297+298298+ (** {2:transforms Transformations} *)
111299112300 val set_first : t -> first_byte:byte_pos -> first_line:line_pos -> t
113113- (** [set_first l ~first_byte ~first_line] updates the first position of [l]. *)
301301+ (** [set_first l ~first_byte ~first_line] updates the start position. *)
114302115303 val set_last : t -> last_byte:byte_pos -> last_line:line_pos -> t
116116- (** [set_last l ~last_byte ~last_line] updates the last position of [l]. *)
304304+ (** [set_last l ~last_byte ~last_line] updates the end position. *)
117305118306 val to_first : t -> t
119119- (** [to_first l] has the start of [l] as its start and end. *)
307307+ (** [to_first l] is a zero-width location at the start of [l]. *)
120308121309 val to_last : t -> t
122122- (** [to_last l] has the end of [l] as its start and end. *)
310310+ (** [to_last l] is a zero-width location at the end of [l]. *)
123311124312 val before : t -> t
125125- (** [before l] is the empty location just before [l]. *)
313313+ (** [before l] is an empty location immediately before [l]. *)
126314127315 val after : t -> t
128128- (** [after l] is the empty location just after [l]. *)
316316+ (** [after l] is an empty location immediately after [l]. *)
129317130318 val span : t -> t -> t
131131- (** [span l0 l1] is the span from the smallest position of [l0] and [l1]
132132- to the largest position of [l0] and [l1]. *)
319319+ (** [span l0 l1] is the smallest location containing both [l0] and [l1]. *)
133320134321 val reloc : first:t -> last:t -> t
135135- (** [reloc ~first ~last] is a location that spans from [first] to [last]. *)
322322+ (** [reloc ~first ~last] creates a location from the start of [first]
323323+ to the end of [last]. *)
136324137137- (** {1:fmt Formatting} *)
325325+ (** {2:fmt Formatting} *)
138326139327 val pp_ocaml : t fmt
140140- (** [pp_ocaml] formats location using OCaml syntax. *)
328328+ (** [pp_ocaml] formats locations in OCaml-style:
329329+ [File "path", line N, characters M-P]. *)
141330142331 val pp_gnu : t fmt
143143- (** [pp_gnu] formats location using GNU syntax. *)
332332+ (** [pp_gnu] formats locations in GNU-style: [path:line:column]. *)
144333145334 val pp : t fmt
146335 (** [pp] is {!pp_ocaml}. *)
147336148337 val pp_dump : t fmt
149149- (** [pp_dump] formats the location for debugging. *)
338338+ (** [pp_dump] formats all location fields for debugging. *)
150339end
151340152152-(** {1:meta Metadata} *)
341341+342342+(** {1:meta Metadata}
343343+344344+ Metadata tracks source location and layout (whitespace/comments) for
345345+ INI elements. This enables layout-preserving round-trips where your
346346+ output matches your input formatting. *)
153347154348(** INI element metadata.
155349156350 Metadata holds text location and layout information (whitespace and
157157- comments) for INI elements. This enables layout-preserving round-trips. *)
351351+ comments) for INI elements. When decoding with [~layout:true], this
352352+ information is captured and can be used to preserve formatting when
353353+ re-encoding.
354354+355355+ {b Example:} A value decoded from:
356356+ {[ port = 8080 # server port]}
357357+ would have [ws_before = " "], [ws_after = " "], and
358358+ [comment = Some "# server port"]. *)
158359module Meta : sig
159360160361 type t
161362 (** The type for element metadata. *)
162363163364 val none : t
164164- (** [none] is metadata with no information. *)
365365+ (** [none] is metadata with no information (no location, no whitespace). *)
165366166367 val make : ?ws_before:string -> ?ws_after:string -> ?comment:string ->
167368 Textloc.t -> t
168168- (** [make ?ws_before ?ws_after ?comment textloc] creates metadata. *)
369369+ (** [make ?ws_before ?ws_after ?comment textloc] creates metadata.
370370+ {ul
371371+ {- [ws_before] is whitespace preceding the element.}
372372+ {- [ws_after] is whitespace following the element.}
373373+ {- [comment] is an associated comment (including the [#] or [;]).}
374374+ {- [textloc] is the source location.}} *)
169375170376 val is_none : t -> bool
171377 (** [is_none m] is [true] iff [m] has no text location. *)
···174380 (** [textloc m] is the text location of [m]. *)
175381176382 val ws_before : t -> string
177177- (** [ws_before m] is whitespace before the element. *)
383383+ (** [ws_before m] is whitespace that appeared before the element. *)
178384179385 val ws_after : t -> string
180180- (** [ws_after m] is whitespace after the element. *)
386386+ (** [ws_after m] is whitespace that appeared after the element. *)
181387182388 val comment : t -> string option
183183- (** [comment m] is the associated comment, if any. *)
389389+ (** [comment m] is the comment associated with the element, if any.
390390+ Includes the comment prefix ([#] or [;]). *)
391391+392392+ (** {2:with_accessors Functional updates} *)
184393185394 val with_textloc : t -> Textloc.t -> t
186395 (** [with_textloc m loc] is [m] with text location [loc]. *)
···195404 (** [with_comment m c] is [m] with [comment] set to [c]. *)
196405197406 val clear_ws : t -> t
198198- (** [clear_ws m] clears whitespace from [m]. *)
407407+ (** [clear_ws m] is [m] with whitespace cleared. *)
199408200409 val clear_textloc : t -> t
201201- (** [clear_textloc m] sets textloc to {!Textloc.none}. *)
410410+ (** [clear_textloc m] is [m] with textloc set to {!Textloc.none}. *)
202411203412 val copy_ws : t -> dst:t -> t
204413 (** [copy_ws src ~dst] copies whitespace from [src] to [dst]. *)
205414end
206415207416type 'a node = 'a * Meta.t
208208-(** The type for values with metadata. *)
417417+(** The type for values paired with metadata. Used internally to track
418418+ source information for section and option names. *)
209419210210-(** {1:paths Paths} *)
420420+421421+(** {1:paths Paths}
422422+423423+ Paths identify locations within an INI document, like
424424+ [[server]/port]. They are used in error messages to show where
425425+ problems occurred. *)
211426212427(** INI paths.
213428214214- Paths identify locations within an INI document, such as
215215- [\[section\]/option]. *)
429429+ Paths provide a way to address locations within an INI document.
430430+ A path is a sequence of section and option indices.
431431+432432+ {b Example paths:}
433433+ {ul
434434+ {- [[server]] - the server section}
435435+ {- [[server]/host] - the host option in the server section}
436436+ {- [[database]/pool_size] - the pool_size option in database}} *)
216437module Path : sig
217438218439 (** {1:indices Path indices} *)
219440220441 type index =
221221- | Section of string node (** A section name. *)
222222- | Option of string node (** An option name. *)
223223- (** The type for path indices. *)
442442+ | Section of string node (** A section name with metadata. *)
443443+ | Option of string node (** An option name with metadata. *)
444444+ (** The type for path indices. An index is either a section or option name. *)
224445225446 val pp_index : index fmt
226226- (** [pp_index] formats an index. *)
447447+ (** [pp_index] formats an index as [[section]] or [/option]. *)
227448228449 (** {1:paths Paths} *)
229450230451 type t
231231- (** The type for paths. *)
452452+ (** The type for paths. A sequence of indices from root to leaf. *)
232453233454 val root : t
234234- (** [root] is the empty path. *)
455455+ (** [root] is the empty path (the document root). *)
235456236457 val is_root : t -> bool
237458 (** [is_root p] is [true] iff [p] is {!root}. *)
···243464 (** [option ?meta name p] appends an option index to [p]. *)
244465245466 val rev_indices : t -> index list
246246- (** [rev_indices p] is the list of indices in reverse order. *)
467467+ (** [rev_indices p] is the indices of [p] in reverse order
468468+ (from leaf to root). *)
247469248470 val pp : t fmt
249249- (** [pp] formats a path. *)
471471+ (** [pp] formats a path as [[section]/option]. *)
250472end
251473252252-(** {1:errors Errors} *)
474474+475475+(** {1:error_module Errors}
476476+477477+ Error handling for INI parsing and codec operations. Errors include
478478+ source location information when available. *)
253479254254-(** Error handling. *)
480480+(** Error handling.
481481+482482+ The {!Error} module defines the types of errors that can occur during
483483+ parsing and codec operations. All errors carry optional location
484484+ information for good error messages.
485485+486486+ {b Error categories:}
487487+ {ul
488488+ {- {b Parse errors}: Malformed INI syntax.}
489489+ {- {b Structure errors}: Missing or duplicate sections/options.}
490490+ {- {b Type errors}: Values that cannot be converted to the expected type.}
491491+ {- {b Interpolation errors}: Unresolved variable references.}} *)
255492module Error : sig
256493257257- (** {1:kinds Error kinds} *)
494494+ (** {1:kinds Error kinds}
495495+496496+ Each error kind corresponds to a specific failure mode. This mirrors
497497+ Python's configparser exception hierarchy. *)
258498259499 type kind =
260500 | Parse of string
501501+ (** Malformed INI syntax. The string describes the issue. *)
261502 | Codec of string
503503+ (** Generic codec error. *)
262504 | Missing_section of string
505505+ (** A required section was not found. Analogous to Python's
506506+ [NoSectionError]. *)
263507 | Missing_option of { section : string; option : string }
508508+ (** A required option was not found in the section. Analogous to
509509+ Python's [NoOptionError]. *)
264510 | Duplicate_section of string
511511+ (** In strict mode, a section appeared more than once. Analogous to
512512+ Python's [DuplicateSectionError]. *)
265513 | Duplicate_option of { section : string; option : string }
514514+ (** In strict mode, an option appeared more than once. Analogous to
515515+ Python's [DuplicateOptionError]. *)
266516 | Type_mismatch of { expected : string; got : string }
517517+ (** The value could not be converted. For example, ["abc"] when
518518+ expecting an integer. *)
267519 | Interpolation of { option : string; reason : string }
520520+ (** Variable interpolation failed. Analogous to Python's
521521+ [InterpolationError] subclasses. *)
268522 | Unknown_option of string
523523+ (** An option was present but not expected (when using
524524+ {!Section.error_unknown}). *)
269525 | Unknown_section of string
526526+ (** A section was present but not expected (when using
527527+ {!Document.error_unknown}). *)
270528 (** The type for error kinds. *)
271529272530 (** {1:errors Errors} *)
273531274532 type t
275275- (** The type for errors. *)
533533+ (** The type for errors. An error combines a {!type-kind} with optional
534534+ location ({!Meta.t}) and path ({!Path.t}) information. *)
276535277536 val make : ?meta:Meta.t -> ?path:Path.t -> kind -> t
278537 (** [make ?meta ?path kind] creates an error. *)
···281540 (** [kind e] is the error kind. *)
282541283542 val meta : t -> Meta.t
284284- (** [meta e] is the error metadata. *)
543543+ (** [meta e] is the error metadata (contains source location). *)
285544286545 val path : t -> Path.t
287287- (** [path e] is the error path. *)
546546+ (** [path e] is the path where the error occurred. *)
288547289548 exception Error of t
290290- (** Exception for errors. *)
549549+ (** Exception for errors. Raised by {!raise}. *)
291550292551 val raise : ?meta:Meta.t -> ?path:Path.t -> kind -> 'a
293293- (** [raise ?meta ?path kind] raises {!Error}. *)
552552+ (** [raise ?meta ?path kind] raises [Error (make ?meta ?path kind)]. *)
553553+554554+ (** {2:fmt Formatting} *)
294555295556 val kind_to_string : kind -> string
296296- (** [kind_to_string k] is a string representation of [k]. *)
557557+ (** [kind_to_string k] is a human-readable description of [k]. *)
297558298559 val to_string : t -> string
299299- (** [to_string e] formats the error as a string. *)
560560+ (** [to_string e] formats [e] as a string with location information. *)
300561301562 val pp : t fmt
302302- (** [pp] formats an error. *)
563563+ (** [pp] formats an error for display. *)
303564end
304565566566+305567(** {1:repr Internal Representations}
306568307307- These types are exposed for use by {!Init_bytesrw}. *)
569569+ These types expose the internal INI representation for use by
570570+ {!Init_bytesrw} and other codec backends. Most users should use
571571+ the high-level {!Section} and {!Document} modules instead. *)
308572module Repr : sig
309573310574 (** {1:values INI Values} *)
311575312576 type ini_value = {
313577 raw : string;
578578+ (** The raw value before interpolation (e.g., ["%(base)s/data"]). *)
314579 interpolated : string;
580580+ (** The value after interpolation (e.g., ["/opt/app/data"]). *)
315581 meta : Meta.t;
582582+ (** Source location and layout. *)
316583 }
317317- (** The type for decoded INI values. [raw] is the value before
318318- interpolation, [interpolated] after. *)
584584+ (** The type for decoded INI values. The [raw] field preserves the original
585585+ text for round-tripping, while [interpolated] has variables expanded. *)
319586320587 (** {1:sections INI Sections} *)
321588322589 type ini_section = {
323590 name : string node;
591591+ (** Section name with metadata (e.g., ["server"]). *)
324592 options : (string node * ini_value) list;
593593+ (** List of (option_name, value) pairs in file order. *)
325594 meta : Meta.t;
595595+ (** Metadata for the section header line. *)
326596 }
327597 (** The type for decoded INI sections. *)
328598···330600331601 type ini_doc = {
332602 defaults : (string node * ini_value) list;
603603+ (** Options from the DEFAULT section (available to all sections). *)
333604 sections : ini_section list;
605605+ (** All non-DEFAULT sections in file order. *)
334606 meta : Meta.t;
607607+ (** Document-level metadata. *)
335608 }
336609 (** The type for decoded INI documents. *)
337610338338- (** {1:codec_state Codec State} *)
611611+ (** {1:codec_state Codec State}
612612+613613+ Internal state used by section and document codecs. *)
339614340615 type 'a codec_result = ('a, Error.t) result
341616 (** The type for codec results. *)
···346621 known_options : string list;
347622 unknown_handler : [ `Skip | `Error | `Keep ];
348623 }
349349- (** Section codec state. *)
624624+ (** Section codec state. The [known_options] list is used to validate
625625+ that required options are present and to detect unknown options. *)
350626351627 type 'a document_state = {
352628 decode : ini_doc -> 'a codec_result;
···357633 (** Document codec state. *)
358634end
359635360360-(** {1:codecs Codecs} *)
636636+637637+(** {1:codecs Codecs}
638638+639639+ Codecs describe bidirectional mappings between INI text and OCaml values.
640640+ A codec of type ['a t] can:
641641+ {ul
642642+ {- {b Decode}: Parse INI text into an OCaml value of type ['a].}
643643+ {- {b Encode}: Serialize an OCaml value of type ['a] to INI text.}}
644644+645645+ Base codecs handle primitive types ({!string}, {!int}, {!bool}, etc.).
646646+ Compose them using {!Section} and {!Document} modules to build codecs
647647+ for complex configuration types. *)
361648362649type 'a t
363650(** The type for INI codecs. A value of type ['a t] describes how to
364651 decode INI data to type ['a] and encode ['a] to INI data. *)
365652366653val kind : 'a t -> string
367367-(** [kind c] is a description of the kind of values [c] represents. *)
654654+(** [kind c] is a short description of what [c] represents (e.g.,
655655+ ["integer"], ["server configuration"]). Used in error messages. *)
368656369657val doc : 'a t -> string
370370-(** [doc c] is the documentation for [c]. *)
658658+(** [doc c] is the documentation string for [c]. *)
371659372660val with_doc : ?kind:string -> ?doc:string -> 'a t -> 'a t
373373-(** [with_doc ?kind ?doc c] is [c] with updated kind and doc. *)
661661+(** [with_doc ?kind ?doc c] is [c] with updated kind and documentation. *)
374662375663val section_state : 'a t -> 'a Repr.section_state option
376376-(** [section_state c] returns the section decode/encode state, if [c]
377377- was created with {!Section.finish}. *)
664664+(** [section_state c] returns the section codec state if [c] was created
665665+ with {!Section.finish}. Returns [None] for non-section codecs. *)
378666379667val document_state : 'a t -> 'a Repr.document_state option
380380-(** [document_state c] returns the document decode/encode state, if [c]
381381- was created with {!Document.finish}. *)
668668+(** [document_state c] returns the document codec state if [c] was created
669669+ with {!Document.finish}. Returns [None] for non-document codecs. *)
382670383383-(** {2:base_codecs Base Codecs} *)
671671+672672+(** {2:base_codecs Base Codecs}
673673+674674+ Codecs for primitive INI value types. All INI values are strings
675675+ internally; these codecs handle the conversion to/from OCaml types. *)
384676385677val string : string t
386386-(** [string] is a codec for string values. *)
678678+(** [string] is the identity codec. Values are returned as-is.
679679+680680+ {b Example:}
681681+ - ["hello world"] decodes to ["hello world"]. *)
387682388683val int : int t
389389-(** [int] is a codec for integer values. *)
684684+(** [int] decodes decimal integers.
685685+686686+ {b Example:}
687687+ - ["42"] decodes to [42]
688688+ - ["-100"] decodes to [-100]
689689+ - ["0xFF"] fails (hex not supported)
690690+691691+ {b Warning.} Behavior depends on [Sys.int_size] for very large values. *)
390692391693val int32 : int32 t
392392-(** [int32] is a codec for 32-bit integer values. *)
694694+(** [int32] decodes 32-bit integers. *)
393695394696val int64 : int64 t
395395-(** [int64] is a codec for 64-bit integer values. *)
697697+(** [int64] decodes 64-bit integers. *)
396698397699val float : float t
398398-(** [float] is a codec for floating-point values. *)
700700+(** [float] decodes floating-point numbers.
701701+702702+ {b Example:}
703703+ - ["3.14"] decodes to [3.14]
704704+ - ["1e-10"] decodes to [1e-10] *)
399705400706val bool : bool t
401401-(** [bool] is a codec for Python-compatible booleans.
402402- Accepts (case-insensitive): [1/yes/true/on] for true,
403403- [0/no/false/off] for false. *)
707707+(** [bool] decodes Python-compatible boolean values. Matching is
708708+ case-insensitive.
709709+710710+ {b True values:} [1], [yes], [true], [on]
711711+712712+ {b False values:} [0], [no], [false], [off]
713713+714714+ This matches Python's [configparser.getboolean()] behavior exactly.
715715+716716+ {b Example:}
717717+ - ["yes"], ["YES"], ["Yes"] all decode to [true]
718718+ - ["0"], ["no"], ["OFF"] all decode to [false]
719719+ - ["maybe"] fails with a type error *)
404720405721val bool_01 : bool t
406406-(** [bool_01] is a strict codec for ["0"]/["1"] booleans. *)
722722+(** [bool_01] decodes only ["0"] and ["1"].
723723+724724+ Use this for stricter parsing when you want exactly these values. *)
407725408726val bool_yesno : bool t
409409-(** [bool_yesno] is a codec for ["yes"]/["no"] booleans. *)
727727+(** [bool_yesno] decodes ["yes"] and ["no"] (case-insensitive). *)
410728411729val bool_truefalse : bool t
412412-(** [bool_truefalse] is a codec for ["true"]/["false"] booleans. *)
730730+(** [bool_truefalse] decodes ["true"] and ["false"] (case-insensitive). *)
413731414732val bool_onoff : bool t
415415-(** [bool_onoff] is a codec for ["on"]/["off"] booleans. *)
733733+(** [bool_onoff] decodes ["on"] and ["off"] (case-insensitive). *)
734734+735735+736736+(** {2:combinators Combinators}
416737417417-(** {2:combinators Combinators} *)
738738+ Build complex codecs from simpler ones. *)
418739419740val map : ?kind:string -> ?doc:string ->
420741 dec:('a -> 'b) -> enc:('b -> 'a) -> 'a t -> 'b t
421421-(** [map ~dec ~enc c] transforms [c] using [dec] for decoding
422422- and [enc] for encoding. *)
742742+(** [map ~dec ~enc c] transforms codec [c] using [dec] for decoding
743743+ and [enc] for encoding.
744744+745745+ {b Example:} A codec for URIs:
746746+ {@ocaml[
747747+ let uri = Init.map
748748+ ~dec:Uri.of_string
749749+ ~enc:Uri.to_string
750750+ Init.string
751751+ ]} *)
423752424753val enum : ?cmp:('a -> 'a -> int) -> ?kind:string -> ?doc:string ->
425754 (string * 'a) list -> 'a t
426426-(** [enum assoc] is a codec for enumerated values. String matching
427427- is case-insensitive. *)
755755+(** [enum assoc] creates a codec for enumerated values. String matching
756756+ is case-insensitive.
757757+758758+ {b Example:}
759759+ {@ocaml[
760760+ type log_level = Debug | Info | Warn | Error
761761+ let log_level = Init.enum [
762762+ "debug", Debug;
763763+ "info", Info;
764764+ "warn", Warn;
765765+ "error", Error;
766766+ ]
767767+ ]}
768768+769769+ @param cmp Comparison function for encoding (default: polymorphic compare). *)
428770429771val option : ?kind:string -> ?doc:string -> 'a t -> 'a option t
430430-(** [option c] is a codec for optional values. Empty strings decode
431431- to [None]. *)
772772+(** [option c] wraps codec [c] to handle optional values. Empty strings
773773+ decode to [None]; [None] encodes to empty string.
774774+775775+ {b Note:} For optional INI options (that may be absent), use
776776+ {!Section.opt_mem} instead. This codec is for values that are
777777+ present but may be empty. *)
432778433779val default : 'a -> 'a t -> 'a t
434434-(** [default v c] uses [v] when decoding fails. *)
780780+(** [default v c] uses [v] when decoding with [c] fails.
781781+782782+ {b Example:}
783783+ {@ocaml[
784784+ let port = Init.default 8080 Init.int
785785+ (* "abc" decodes to 8080 instead of failing *)
786786+ ]} *)
435787436788val list : ?sep:char -> 'a t -> 'a list t
437437-(** [list ?sep c] is a codec for lists of values separated by [sep]
438438- (default: [',']). *)
789789+(** [list ?sep c] decodes comma-separated (or [sep]-separated) values.
790790+791791+ {b Example:}
792792+ - ["a,b,c"] with [list string] decodes to [["a"; "b"; "c"]]
793793+ - ["1, 2, 3"] with [list int] decodes to [[1; 2; 3]]
794794+795795+ Whitespace around elements is trimmed.
796796+797797+ @param sep Separator character (default: [',']). *)
798798+439799440800(** {1:sections Section Codecs}
441801442442- Build codecs for INI sections using an applicative style. *)
802802+ Build codecs for INI sections using an applicative style. A section
803803+ codec maps the options in a [[section]] to fields of an OCaml record.
804804+805805+ {2:section_pattern The Pattern}
806806+807807+ {@ocaml[
808808+ type server = { host : string; port : int; debug : bool }
809809+810810+ let server_codec = Init.Section.(
811811+ obj (fun host port debug -> { host; port; debug })
812812+ |> mem "host" Init.string ~enc:(fun s -> s.host)
813813+ |> mem "port" Init.int ~enc:(fun s -> s.port)
814814+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun s -> s.debug)
815815+ |> finish
816816+ )
817817+ ]}
818818+819819+ The [obj] function takes a constructor. Each [mem] call adds an option
820820+ and supplies one argument to the constructor. The [~enc] parameter
821821+ extracts the value for encoding.
822822+823823+ {2:section_optional Optional and Absent Values}
824824+825825+ {ul
826826+ {- [mem] with [~dec_absent:v] uses [v] if the option is missing.}
827827+ {- [opt_mem] decodes to [None] if the option is missing.}
828828+ {- Both still require the option name for encoding.}} *)
443829module Section : sig
444830445831 type 'a codec = 'a t
446446- (** Alias for codec type. *)
832832+ (** Alias for the codec type. *)
447833448834 type ('o, 'dec) map
449449- (** The type for section maps. ['o] is the OCaml type being built,
450450- ['dec] is the remaining constructor arguments. *)
835835+ (** The type for section maps under construction. ['o] is the final
836836+ OCaml type being built. ['dec] is the remaining constructor
837837+ arguments needed. *)
451838452839 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
453453- (** [obj f] starts building a section codec with constructor [f]. *)
840840+ (** [obj f] starts building a section codec with constructor [f].
841841+842842+ The constructor [f] should have type
843843+ [arg1 -> arg2 -> ... -> argN -> 'o] where each argument corresponds
844844+ to a {!mem} or {!opt_mem} call. *)
454845455846 val mem : ?doc:string -> ?dec_absent:'a -> ?enc:('o -> 'a) ->
456847 ?enc_omit:('a -> bool) ->
457848 string -> 'a codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
458458- (** [mem name c m] adds an option [name] decoded by [c] to map [m].
459459- @param dec_absent Default value if option is absent.
460460- @param enc Encoder function to extract value from ['o].
461461- @param enc_omit Predicate; if true, omit option during encoding. *)
849849+ (** [mem name c m] adds option [name] to the section map.
850850+851851+ {ul
852852+ {- [name] is the option name (case-insensitive for lookup).}
853853+ {- [c] is the codec for the option's value.}
854854+ {- [~dec_absent] provides a default if the option is missing.
855855+ Without this, a missing option is an error.}
856856+ {- [~enc] extracts the value from the record for encoding.
857857+ Required for encoding.}
858858+ {- [~enc_omit] if [true] for a value, omits the option during
859859+ encoding.}} *)
462860463861 val opt_mem : ?doc:string -> ?enc:('o -> 'a option) ->
464862 string -> 'a codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
465465- (** [opt_mem name c m] adds an optional option (decodes to [None] if absent). *)
863863+ (** [opt_mem name c m] adds an optional option. Decodes to [None] if
864864+ the option is absent, [Some v] otherwise.
865865+866866+ {b Example:}
867867+ {@ocaml[
868868+ |> opt_mem "timeout" Init.int ~enc:(fun s -> s.timeout)
869869+ (* Absent -> None, "30" -> Some 30 *)
870870+ ]} *)
871871+872872+ (** {2:unknown Handling Unknown Options}
873873+874874+ By default, unknown options in a section are ignored ({!skip_unknown}).
875875+ You can change this behavior. *)
466876467877 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
468468- (** [skip_unknown m] ignores unknown options (default). *)
878878+ (** [skip_unknown m] ignores options not declared with {!mem}.
879879+ This is the default behavior, matching Python's configparser. *)
469880470881 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
471471- (** [error_unknown m] raises an error on unknown options. *)
882882+ (** [error_unknown m] raises an error if the section contains options
883883+ not declared with {!mem}. Use this for strict validation. *)
472884473885 val keep_unknown : ?enc:('o -> (string * string) list) ->
474886 ('o, (string * string) list -> 'dec) map -> ('o, 'dec) map
475475- (** [keep_unknown m] captures unknown options as a list of (name, value) pairs. *)
887887+ (** [keep_unknown m] captures unknown options as a list of
888888+ [(name, value)] pairs. Useful for pass-through or dynamic configs.
889889+890890+ {b Example:}
891891+ {@ocaml[
892892+ type section = {
893893+ known : string;
894894+ extra : (string * string) list;
895895+ }
896896+897897+ let codec = Init.Section.(
898898+ obj (fun known extra -> { known; extra })
899899+ |> mem "known" Init.string ~enc:(fun s -> s.known)
900900+ |> keep_unknown ~enc:(fun s -> s.extra)
901901+ |> finish
902902+ )
903903+ ]} *)
476904477905 val finish : ('o, 'o) map -> 'o codec
478478- (** [finish m] completes the section codec. *)
906906+ (** [finish m] completes the section codec. The map's constructor must
907907+ be fully saturated (all arguments provided via {!mem} calls). *)
479908end
480909910910+481911(** {1:documents Document Codecs}
482912483483- Build codecs for complete INI documents. *)
913913+ Build codecs for complete INI documents. A document codec maps
914914+ sections to fields of an OCaml record.
915915+916916+ {2:document_pattern The Pattern}
917917+918918+ {@ocaml[
919919+ type config = {
920920+ server : server;
921921+ database : database option;
922922+ }
923923+924924+ let config_codec = Init.Document.(
925925+ obj (fun server database -> { server; database })
926926+ |> section "server" server_codec ~enc:(fun c -> c.server)
927927+ |> opt_section "database" database_codec ~enc:(fun c -> c.database)
928928+ |> finish
929929+ )
930930+ ]} *)
484931module Document : sig
485932486933 type 'a codec = 'a t
487487- (** Alias for codec type. *)
934934+ (** Alias for the codec type. *)
488935489936 type ('o, 'dec) map
490490- (** The type for document maps. *)
937937+ (** The type for document maps under construction. *)
491938492939 val obj : ?kind:string -> ?doc:string -> 'dec -> ('o, 'dec) map
493940 (** [obj f] starts building a document codec with constructor [f]. *)
494941495942 val section : ?doc:string -> ?enc:('o -> 'a) ->
496943 string -> 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
497497- (** [section name c m] adds a required section [name] to map [m]. *)
944944+ (** [section name c m] adds a required section.
945945+946946+ The section must be present in the INI file. If it's missing, decoding
947947+ fails with {!Error.Missing_section}.
948948+949949+ {b Note:} Section names are case-sensitive. [[Server]] and [[server]]
950950+ are different sections. *)
498951499952 val opt_section : ?doc:string -> ?enc:('o -> 'a option) ->
500953 string -> 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
501501- (** [opt_section name c m] adds an optional section [name] to map [m]. *)
954954+ (** [opt_section name c m] adds an optional section.
955955+956956+ Decodes to [None] if the section is absent, [Some v] otherwise. *)
502957503958 val defaults : ?doc:string -> ?enc:('o -> 'a) ->
504959 'a Section.codec -> ('o, 'a -> 'dec) map -> ('o, 'dec) map
505505- (** [defaults c m] decodes the DEFAULT section using [c]. *)
960960+ (** [defaults c m] decodes the [[DEFAULT]] section.
961961+962962+ The DEFAULT section's values are available for interpolation in all
963963+ other sections. Use this when you need to access DEFAULT values
964964+ directly in your OCaml type.
965965+966966+ {b Note:} Even without this, DEFAULT values are used for interpolation
967967+ and as fallbacks during option lookup. *)
506968507969 val opt_defaults : ?doc:string -> ?enc:('o -> 'a option) ->
508970 'a Section.codec -> ('o, 'a option -> 'dec) map -> ('o, 'dec) map
509509- (** [opt_defaults c m] optionally decodes the DEFAULT section. *)
971971+ (** [opt_defaults c m] optionally decodes the [[DEFAULT]] section. *)
972972+973973+ (** {2:unknown_sections Handling Unknown Sections} *)
510974511975 val skip_unknown : ('o, 'dec) map -> ('o, 'dec) map
512512- (** [skip_unknown m] ignores unknown sections (default). *)
976976+ (** [skip_unknown m] ignores sections not declared with {!section}.
977977+ This is the default behavior. *)
513978514979 val error_unknown : ('o, 'dec) map -> ('o, 'dec) map
515515- (** [error_unknown m] raises an error on unknown sections. *)
980980+ (** [error_unknown m] raises an error if the document contains sections
981981+ not declared with {!section}. Use for strict validation. *)
516982517983 val finish : ('o, 'o) map -> 'o codec
518984 (** [finish m] completes the document codec. *)
+713
test/cookbook.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Init Cookbook
77+88+ This file contains complete, runnable examples demonstrating common
99+ patterns for using the Init library. Each example is self-contained
1010+ and can be adapted to your use case.
1111+1212+ Run with: [dune exec ./test/cookbook.exe] *)
1313+1414+(** {1 Basic Configuration}
1515+1616+ The simplest use case: parse a configuration file with a single section
1717+ into an OCaml record. *)
1818+1919+module Basic = struct
2020+ type server_config = {
2121+ host : string;
2222+ port : int;
2323+ debug : bool;
2424+ }
2525+2626+ let server_codec = Init.Section.(
2727+ obj (fun host port debug -> { host; port; debug })
2828+ |> mem "host" Init.string ~enc:(fun c -> c.host)
2929+ |> mem "port" Init.int ~enc:(fun c -> c.port)
3030+ |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug)
3131+ |> finish
3232+ )
3333+3434+ let config_codec = Init.Document.(
3535+ obj (fun server -> server)
3636+ |> section "server" server_codec ~enc:Fun.id
3737+ |> finish
3838+ )
3939+4040+ let example () =
4141+ let ini = {|
4242+[server]
4343+host = localhost
4444+port = 8080
4545+debug = yes
4646+|} in
4747+ match Init_bytesrw.decode_string config_codec ini with
4848+ | Ok config ->
4949+ Printf.printf "Server: %s:%d (debug=%b)\n"
5050+ config.host config.port config.debug
5151+ | Error msg ->
5252+ Printf.printf "Error: %s\n" msg
5353+end
5454+5555+(** {1 Optional Values and Defaults}
5656+5757+ Handle missing options gracefully with defaults or optional fields. *)
5858+5959+module Optional_values = struct
6060+ type database_config = {
6161+ host : string;
6262+ port : int; (* Uses default if missing *)
6363+ username : string;
6464+ password : string option; (* Optional field *)
6565+ ssl : bool;
6666+ }
6767+6868+ let database_codec = Init.Section.(
6969+ obj (fun host port username password ssl ->
7070+ { host; port; username; password; ssl })
7171+ |> mem "host" Init.string ~enc:(fun c -> c.host)
7272+ (* dec_absent provides a default value when the option is missing *)
7373+ |> mem "port" Init.int ~dec_absent:5432 ~enc:(fun c -> c.port)
7474+ |> mem "username" Init.string ~enc:(fun c -> c.username)
7575+ (* opt_mem decodes to None when the option is missing *)
7676+ |> opt_mem "password" Init.string ~enc:(fun c -> c.password)
7777+ |> mem "ssl" Init.bool ~dec_absent:true ~enc:(fun c -> c.ssl)
7878+ |> finish
7979+ )
8080+8181+ let config_codec = Init.Document.(
8282+ obj Fun.id
8383+ |> section "database" database_codec ~enc:Fun.id
8484+ |> finish
8585+ )
8686+8787+ let example () =
8888+ (* Minimal config - uses defaults *)
8989+ let ini = {|
9090+[database]
9191+host = db.example.com
9292+username = admin
9393+|} in
9494+ match Init_bytesrw.decode_string config_codec ini with
9595+ | Ok config ->
9696+ Printf.printf "DB: %s@%s:%d (ssl=%b, password=%s)\n"
9797+ config.username config.host config.port config.ssl
9898+ (match config.password with Some _ -> "***" | None -> "none")
9999+ | Error msg ->
100100+ Printf.printf "Error: %s\n" msg
101101+end
102102+103103+(** {1 Multiple Sections}
104104+105105+ Parse a configuration with multiple sections, some required and some
106106+ optional. *)
107107+108108+module Multiple_sections = struct
109109+ type server = { host : string; port : int }
110110+ type database = { connection : string; pool_size : int }
111111+ type cache = { enabled : bool; ttl : int }
112112+ type config = {
113113+ server : server;
114114+ database : database;
115115+ cache : cache option; (* Optional section *)
116116+ }
117117+118118+ let server_codec = Init.Section.(
119119+ obj (fun host port -> { host; port })
120120+ |> mem "host" Init.string ~enc:(fun s -> s.host)
121121+ |> mem "port" Init.int ~enc:(fun s -> s.port)
122122+ |> finish
123123+ )
124124+125125+ let database_codec = Init.Section.(
126126+ obj (fun connection pool_size -> { connection; pool_size })
127127+ |> mem "connection" Init.string ~enc:(fun d -> d.connection)
128128+ |> mem "pool_size" Init.int ~dec_absent:10 ~enc:(fun d -> d.pool_size)
129129+ |> finish
130130+ )
131131+132132+ let cache_codec = Init.Section.(
133133+ obj (fun enabled ttl -> { enabled; ttl })
134134+ |> mem "enabled" Init.bool ~enc:(fun c -> c.enabled)
135135+ |> mem "ttl" Init.int ~dec_absent:3600 ~enc:(fun c -> c.ttl)
136136+ |> finish
137137+ )
138138+139139+ let config_codec = Init.Document.(
140140+ obj (fun server database cache -> { server; database; cache })
141141+ |> section "server" server_codec ~enc:(fun c -> c.server)
142142+ |> section "database" database_codec ~enc:(fun c -> c.database)
143143+ (* opt_section allows the section to be absent *)
144144+ |> opt_section "cache" cache_codec ~enc:(fun c -> c.cache)
145145+ |> finish
146146+ )
147147+148148+ let example () =
149149+ let ini = {|
150150+[server]
151151+host = api.example.com
152152+port = 443
153153+154154+[database]
155155+connection = postgres://localhost/mydb
156156+157157+[cache]
158158+enabled = yes
159159+ttl = 7200
160160+|} in
161161+ match Init_bytesrw.decode_string config_codec ini with
162162+ | Ok config ->
163163+ Printf.printf "Server: %s:%d\n" config.server.host config.server.port;
164164+ Printf.printf "Database: %s (pool=%d)\n"
165165+ config.database.connection config.database.pool_size;
166166+ (match config.cache with
167167+ | Some c -> Printf.printf "Cache: enabled=%b ttl=%d\n" c.enabled c.ttl
168168+ | None -> Printf.printf "Cache: disabled\n")
169169+ | Error msg ->
170170+ Printf.printf "Error: %s\n" msg
171171+end
172172+173173+(** {1 Lists and Comma-Separated Values}
174174+175175+ Parse comma-separated lists of values. *)
176176+177177+module Lists = struct
178178+ type config = {
179179+ hosts : string list;
180180+ ports : int list;
181181+ tags : string list;
182182+ }
183183+184184+ let section_codec = Init.Section.(
185185+ obj (fun hosts ports tags -> { hosts; ports; tags })
186186+ |> mem "hosts" (Init.list Init.string) ~enc:(fun c -> c.hosts)
187187+ |> mem "ports" (Init.list Init.int) ~enc:(fun c -> c.ports)
188188+ |> mem "tags" (Init.list Init.string) ~dec_absent:[] ~enc:(fun c -> c.tags)
189189+ |> finish
190190+ )
191191+192192+ let config_codec = Init.Document.(
193193+ obj Fun.id
194194+ |> section "cluster" section_codec ~enc:Fun.id
195195+ |> finish
196196+ )
197197+198198+ let example () =
199199+ let ini = {|
200200+[cluster]
201201+hosts = node1.example.com, node2.example.com, node3.example.com
202202+ports = 8080, 8081, 8082
203203+|} in
204204+ match Init_bytesrw.decode_string config_codec ini with
205205+ | Ok config ->
206206+ Printf.printf "Hosts: %s\n" (String.concat ", " config.hosts);
207207+ Printf.printf "Ports: %s\n"
208208+ (String.concat ", " (List.map string_of_int config.ports));
209209+ Printf.printf "Tags: %s\n"
210210+ (if config.tags = [] then "(none)" else String.concat ", " config.tags)
211211+ | Error msg ->
212212+ Printf.printf "Error: %s\n" msg
213213+end
214214+215215+(** {1 Enums and Custom Types}
216216+217217+ Parse enumerated values and custom types. *)
218218+219219+module Enums = struct
220220+ type log_level = Debug | Info | Warn | Error
221221+ type environment = Development | Staging | Production
222222+223223+ type config = {
224224+ log_level : log_level;
225225+ environment : environment;
226226+ max_connections : int;
227227+ }
228228+229229+ let log_level_codec = Init.enum [
230230+ "debug", Debug;
231231+ "info", Info;
232232+ "warn", Warn;
233233+ "error", Error;
234234+ ]
235235+236236+ let environment_codec = Init.enum [
237237+ "development", Development;
238238+ "dev", Development; (* Alias *)
239239+ "staging", Staging;
240240+ "production", Production;
241241+ "prod", Production; (* Alias *)
242242+ ]
243243+244244+ let section_codec = Init.Section.(
245245+ obj (fun log_level environment max_connections ->
246246+ { log_level; environment; max_connections })
247247+ |> mem "log_level" log_level_codec ~dec_absent:Info
248248+ ~enc:(fun c -> c.log_level)
249249+ |> mem "environment" environment_codec ~enc:(fun c -> c.environment)
250250+ |> mem "max_connections" Init.int ~dec_absent:100
251251+ ~enc:(fun c -> c.max_connections)
252252+ |> finish
253253+ )
254254+255255+ let config_codec = Init.Document.(
256256+ obj Fun.id
257257+ |> section "app" section_codec ~enc:Fun.id
258258+ |> finish
259259+ )
260260+261261+ let example () =
262262+ let ini = {|
263263+[app]
264264+log_level = debug
265265+environment = prod
266266+|} in
267267+ match Init_bytesrw.decode_string config_codec ini with
268268+ | Ok config ->
269269+ let env_str = match config.environment with
270270+ | Development -> "development"
271271+ | Staging -> "staging"
272272+ | Production -> "production"
273273+ in
274274+ Printf.printf "Env: %s, Log: %s, MaxConn: %d\n"
275275+ env_str
276276+ (match config.log_level with
277277+ | Debug -> "debug" | Info -> "info"
278278+ | Warn -> "warn" | Error -> "error")
279279+ config.max_connections
280280+ | Error msg ->
281281+ Printf.printf "Error: %s\n" msg
282282+end
283283+284284+(** {1 Handling Unknown Options}
285285+286286+ Three strategies for dealing with options you didn't expect. *)
287287+288288+module Unknown_options = struct
289289+ (* Strategy 1: Skip unknown (default) - silently ignore extra options *)
290290+ module Skip = struct
291291+ type config = { known_key : string }
292292+293293+ let section_codec = Init.Section.(
294294+ obj (fun known_key -> { known_key })
295295+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
296296+ |> skip_unknown (* This is the default *)
297297+ |> finish
298298+ )
299299+300300+ let _config_codec = Init.Document.(
301301+ obj Fun.id
302302+ |> section "test" section_codec ~enc:Fun.id
303303+ |> finish
304304+ )
305305+ end
306306+307307+ (* Strategy 2: Error on unknown - strict validation *)
308308+ module Strict = struct
309309+ type config = { known_key : string }
310310+311311+ let section_codec = Init.Section.(
312312+ obj (fun known_key -> { known_key })
313313+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
314314+ |> error_unknown (* Reject unknown options *)
315315+ |> finish
316316+ )
317317+318318+ let _config_codec = Init.Document.(
319319+ obj Fun.id
320320+ |> section "test" section_codec ~enc:Fun.id
321321+ |> error_unknown (* Also reject unknown sections *)
322322+ |> finish
323323+ )
324324+ end
325325+326326+ (* Strategy 3: Keep unknown - capture for pass-through *)
327327+ module Passthrough = struct
328328+ type config = {
329329+ known_key : string;
330330+ extra : (string * string) list;
331331+ }
332332+333333+ let section_codec = Init.Section.(
334334+ obj (fun known_key extra -> { known_key; extra })
335335+ |> mem "known_key" Init.string ~enc:(fun c -> c.known_key)
336336+ |> keep_unknown ~enc:(fun c -> c.extra)
337337+ |> finish
338338+ )
339339+340340+ let config_codec = Init.Document.(
341341+ obj Fun.id
342342+ |> section "test" section_codec ~enc:Fun.id
343343+ |> finish
344344+ )
345345+346346+ let example () =
347347+ let ini = {|
348348+[test]
349349+known_key = hello
350350+extra1 = world
351351+extra2 = foo
352352+|} in
353353+ match Init_bytesrw.decode_string config_codec ini with
354354+ | Ok config ->
355355+ Printf.printf "Known: %s\n" config.known_key;
356356+ List.iter (fun (k, v) ->
357357+ Printf.printf "Extra: %s = %s\n" k v
358358+ ) config.extra
359359+ | Error msg ->
360360+ Printf.printf "Error: %s\n" msg
361361+ end
362362+end
363363+364364+(** {1 Interpolation}
365365+366366+ Variable substitution in values. *)
367367+368368+module Interpolation = struct
369369+ (* Basic interpolation: %(name)s *)
370370+ module Basic = struct
371371+ type paths = {
372372+ base : string;
373373+ data : string;
374374+ logs : string;
375375+ config : string;
376376+ }
377377+378378+ let paths_codec = Init.Section.(
379379+ obj (fun base data logs config -> { base; data; logs; config })
380380+ |> mem "base" Init.string ~enc:(fun p -> p.base)
381381+ |> mem "data" Init.string ~enc:(fun p -> p.data)
382382+ |> mem "logs" Init.string ~enc:(fun p -> p.logs)
383383+ |> mem "config" Init.string ~enc:(fun p -> p.config)
384384+ |> finish
385385+ )
386386+387387+ let config_codec = Init.Document.(
388388+ obj Fun.id
389389+ |> section "paths" paths_codec ~enc:Fun.id
390390+ |> finish
391391+ )
392392+393393+ let example () =
394394+ let ini = {|
395395+[paths]
396396+base = /opt/myapp
397397+data = %(base)s/data
398398+logs = %(base)s/logs
399399+config = %(base)s/etc
400400+|} in
401401+ match Init_bytesrw.decode_string config_codec ini with
402402+ | Ok paths ->
403403+ Printf.printf "Base: %s\n" paths.base;
404404+ Printf.printf "Data: %s\n" paths.data;
405405+ Printf.printf "Logs: %s\n" paths.logs;
406406+ Printf.printf "Config: %s\n" paths.config
407407+ | Error msg ->
408408+ Printf.printf "Error: %s\n" msg
409409+ end
410410+411411+ (* Extended interpolation: ${section:name} *)
412412+ module Extended = struct
413413+ type common = { base : string }
414414+ type server = { data_dir : string; log_dir : string }
415415+ type config = { common : common; server : server }
416416+417417+ let common_codec = Init.Section.(
418418+ obj (fun base -> { base })
419419+ |> mem "base" Init.string ~enc:(fun c -> c.base)
420420+ |> finish
421421+ )
422422+423423+ let server_codec = Init.Section.(
424424+ obj (fun data_dir log_dir -> { data_dir; log_dir })
425425+ |> mem "data_dir" Init.string ~enc:(fun s -> s.data_dir)
426426+ |> mem "log_dir" Init.string ~enc:(fun s -> s.log_dir)
427427+ |> finish
428428+ )
429429+430430+ let config_codec = Init.Document.(
431431+ obj (fun common server -> { common; server })
432432+ |> section "common" common_codec ~enc:(fun c -> c.common)
433433+ |> section "server" server_codec ~enc:(fun c -> c.server)
434434+ |> finish
435435+ )
436436+437437+ let example () =
438438+ let config = { Init_bytesrw.default_config with
439439+ interpolation = `Extended_interpolation } in
440440+ let ini = {|
441441+[common]
442442+base = /opt/myapp
443443+444444+[server]
445445+data_dir = ${common:base}/data
446446+log_dir = ${common:base}/logs
447447+|} in
448448+ match Init_bytesrw.decode_string ~config config_codec ini with
449449+ | Ok cfg ->
450450+ Printf.printf "Base: %s\n" cfg.common.base;
451451+ Printf.printf "Data: %s\n" cfg.server.data_dir;
452452+ Printf.printf "Log: %s\n" cfg.server.log_dir
453453+ | Error msg ->
454454+ Printf.printf "Error: %s\n" msg
455455+ end
456456+end
457457+458458+(** {1 The DEFAULT Section}
459459+460460+ The DEFAULT section provides fallback values for all other sections. *)
461461+462462+module Defaults = struct
463463+ type section = {
464464+ host : string;
465465+ port : int;
466466+ timeout : int; (* Falls back to DEFAULT *)
467467+ }
468468+469469+ type config = {
470470+ production : section;
471471+ staging : section;
472472+ }
473473+474474+ let section_codec = Init.Section.(
475475+ obj (fun host port timeout -> { host; port; timeout })
476476+ |> mem "host" Init.string ~enc:(fun s -> s.host)
477477+ |> mem "port" Init.int ~enc:(fun s -> s.port)
478478+ |> mem "timeout" Init.int ~enc:(fun s -> s.timeout)
479479+ |> finish
480480+ )
481481+482482+ let config_codec = Init.Document.(
483483+ obj (fun production staging -> { production; staging })
484484+ |> section "production" section_codec ~enc:(fun c -> c.production)
485485+ |> section "staging" section_codec ~enc:(fun c -> c.staging)
486486+ |> skip_unknown (* Ignore DEFAULT section in output *)
487487+ |> finish
488488+ )
489489+490490+ let example () =
491491+ let ini = {|
492492+[DEFAULT]
493493+timeout = 30
494494+495495+[production]
496496+host = api.example.com
497497+port = 443
498498+499499+[staging]
500500+host = staging.example.com
501501+port = 8443
502502+timeout = 60
503503+|} in
504504+ match Init_bytesrw.decode_string config_codec ini with
505505+ | Ok config ->
506506+ Printf.printf "Production: %s:%d (timeout=%d)\n"
507507+ config.production.host config.production.port config.production.timeout;
508508+ Printf.printf "Staging: %s:%d (timeout=%d)\n"
509509+ config.staging.host config.staging.port config.staging.timeout
510510+ | Error msg ->
511511+ Printf.printf "Error: %s\n" msg
512512+end
513513+514514+(** {1 Round-Trip Encoding}
515515+516516+ Decode, modify, and re-encode a configuration. *)
517517+518518+module Roundtrip = struct
519519+ type config = {
520520+ host : string;
521521+ port : int;
522522+ }
523523+524524+ let section_codec = Init.Section.(
525525+ obj (fun host port -> { host; port })
526526+ |> mem "host" Init.string ~enc:(fun c -> c.host)
527527+ |> mem "port" Init.int ~enc:(fun c -> c.port)
528528+ |> finish
529529+ )
530530+531531+ let config_codec = Init.Document.(
532532+ obj Fun.id
533533+ |> section "server" section_codec ~enc:Fun.id
534534+ |> finish
535535+ )
536536+537537+ let example () =
538538+ (* Decode *)
539539+ let ini = {|
540540+[server]
541541+host = localhost
542542+port = 8080
543543+|} in
544544+ match Init_bytesrw.decode_string config_codec ini with
545545+ | Error msg ->
546546+ Printf.printf "Decode error: %s\n" msg
547547+ | Ok config ->
548548+ Printf.printf "Decoded: %s:%d\n" config.host config.port;
549549+550550+ (* Modify *)
551551+ let modified = { config with port = 9000 } in
552552+553553+ (* Encode *)
554554+ (match Init_bytesrw.encode_string config_codec modified with
555555+ | Ok output ->
556556+ Printf.printf "Encoded:\n%s" output
557557+ | Error msg ->
558558+ Printf.printf "Encode error: %s\n" msg)
559559+end
560560+561561+(** {1 Custom Boolean Formats}
562562+563563+ Different applications use different boolean representations. *)
564564+565565+module Custom_booleans = struct
566566+ type config = {
567567+ python_style : bool; (* 1/yes/true/on or 0/no/false/off *)
568568+ strict_01 : bool; (* Only 0 or 1 *)
569569+ yes_no : bool; (* Only yes or no *)
570570+ on_off : bool; (* Only on or off *)
571571+ }
572572+573573+ let section_codec = Init.Section.(
574574+ obj (fun python_style strict_01 yes_no on_off ->
575575+ { python_style; strict_01; yes_no; on_off })
576576+ |> mem "python_style" Init.bool ~enc:(fun c -> c.python_style)
577577+ |> mem "strict_01" Init.bool_01 ~enc:(fun c -> c.strict_01)
578578+ |> mem "yes_no" Init.bool_yesno ~enc:(fun c -> c.yes_no)
579579+ |> mem "on_off" Init.bool_onoff ~enc:(fun c -> c.on_off)
580580+ |> finish
581581+ )
582582+583583+ let config_codec = Init.Document.(
584584+ obj Fun.id
585585+ |> section "flags" section_codec ~enc:Fun.id
586586+ |> finish
587587+ )
588588+589589+ let example () =
590590+ let ini = {|
591591+[flags]
592592+python_style = YES
593593+strict_01 = 1
594594+yes_no = no
595595+on_off = on
596596+|} in
597597+ match Init_bytesrw.decode_string config_codec ini with
598598+ | Ok config ->
599599+ Printf.printf "python_style=%b strict_01=%b yes_no=%b on_off=%b\n"
600600+ config.python_style config.strict_01 config.yes_no config.on_off
601601+ | Error msg ->
602602+ Printf.printf "Error: %s\n" msg
603603+end
604604+605605+(** {1 Error Handling}
606606+607607+ Demonstrate different error scenarios and how to handle them. *)
608608+609609+module Error_handling = struct
610610+ type config = { port : int }
611611+612612+ let section_codec = Init.Section.(
613613+ obj (fun port -> { port })
614614+ |> mem "port" Init.int ~enc:(fun c -> c.port)
615615+ |> finish
616616+ )
617617+618618+ let config_codec = Init.Document.(
619619+ obj Fun.id
620620+ |> section "server" section_codec ~enc:Fun.id
621621+ |> finish
622622+ )
623623+624624+ let example () =
625625+ (* Missing section *)
626626+ let ini1 = {|
627627+[wrong_name]
628628+port = 8080
629629+|} in
630630+ (match Init_bytesrw.decode_string config_codec ini1 with
631631+ | Ok _ -> Printf.printf "Unexpected success\n"
632632+ | Error msg -> Printf.printf "Missing section: %s\n\n" msg);
633633+634634+ (* Missing option *)
635635+ let ini2 = {|
636636+[server]
637637+host = localhost
638638+|} in
639639+ (match Init_bytesrw.decode_string config_codec ini2 with
640640+ | Ok _ -> Printf.printf "Unexpected success\n"
641641+ | Error msg -> Printf.printf "Missing option: %s\n\n" msg);
642642+643643+ (* Type mismatch *)
644644+ let ini3 = {|
645645+[server]
646646+port = not_a_number
647647+|} in
648648+ (match Init_bytesrw.decode_string config_codec ini3 with
649649+ | Ok _ -> Printf.printf "Unexpected success\n"
650650+ | Error msg -> Printf.printf "Type mismatch: %s\n\n" msg);
651651+652652+ (* Using structured errors *)
653653+ let ini4 = {|
654654+[server]
655655+port = abc
656656+|} in
657657+ match Init_bytesrw.decode_string' config_codec ini4 with
658658+ | Ok _ -> Printf.printf "Unexpected success\n"
659659+ | Error e ->
660660+ Printf.printf "Structured error:\n";
661661+ Printf.printf " Kind: %s\n" (Init.Error.kind_to_string (Init.Error.kind e));
662662+ Format.printf " Path: %a\n" Init.Path.pp (Init.Error.path e)
663663+end
664664+665665+(** {1 Running Examples} *)
666666+667667+let () =
668668+ Printf.printf "=== Basic Configuration ===\n";
669669+ Basic.example ();
670670+ Printf.printf "\n";
671671+672672+ Printf.printf "=== Optional Values ===\n";
673673+ Optional_values.example ();
674674+ Printf.printf "\n";
675675+676676+ Printf.printf "=== Multiple Sections ===\n";
677677+ Multiple_sections.example ();
678678+ Printf.printf "\n";
679679+680680+ Printf.printf "=== Lists ===\n";
681681+ Lists.example ();
682682+ Printf.printf "\n";
683683+684684+ Printf.printf "=== Enums ===\n";
685685+ Enums.example ();
686686+ Printf.printf "\n";
687687+688688+ Printf.printf "=== Unknown Options (Passthrough) ===\n";
689689+ Unknown_options.Passthrough.example ();
690690+ Printf.printf "\n";
691691+692692+ Printf.printf "=== Basic Interpolation ===\n";
693693+ Interpolation.Basic.example ();
694694+ Printf.printf "\n";
695695+696696+ Printf.printf "=== Extended Interpolation ===\n";
697697+ Interpolation.Extended.example ();
698698+ Printf.printf "\n";
699699+700700+ Printf.printf "=== DEFAULT Section ===\n";
701701+ Defaults.example ();
702702+ Printf.printf "\n";
703703+704704+ Printf.printf "=== Round-Trip Encoding ===\n";
705705+ Roundtrip.example ();
706706+ Printf.printf "\n";
707707+708708+ Printf.printf "=== Custom Booleans ===\n";
709709+ Custom_booleans.example ();
710710+ Printf.printf "\n";
711711+712712+ Printf.printf "=== Error Handling ===\n";
713713+ Error_handling.example ()
+40
test/data/basic.ini
···11+# Basic INI test file - matches Python configparser basic test
22+33+[Foo Bar]
44+foo=bar1
55+66+[Spacey Bar]
77+foo = bar2
88+99+[Spacey Bar From The Beginning]
1010+ foo = bar3
1111+ baz = qwe
1212+1313+[Commented Bar]
1414+foo: bar4 ; comment
1515+baz=qwe #another one
1616+1717+[Long Line]
1818+foo: this line is much, much longer than my editor
1919+ likes it.
2020+2121+[Section\with$weird%characters[ ]
2222+2323+[Internationalized Stuff]
2424+foo[bg]: Bulgarian
2525+foo=Default
2626+foo[en]=English
2727+foo[de]=Deutsch
2828+2929+[Spaces]
3030+key with spaces : value
3131+another with spaces = splat!
3232+3333+[Types]
3434+int : 42
3535+float = 0.44
3636+boolean = NO
3737+123 : strange but acceptable
3838+3939+[This One Has A ] In It]
4040+ forks = spoons
+17
test/data/booleans.ini
···11+# Boolean test cases - all valid Python configparser boolean values
22+[BOOLTEST]
33+T1=1
44+T2=TRUE
55+T3=True
66+T4=oN
77+T5=yes
88+F1=0
99+F2=FALSE
1010+F3=False
1111+F4=oFF
1212+F5=nO
1313+E1=2
1414+E2=foo
1515+E3=-1
1616+E4=0.1
1717+E5=FALSE AND MORE
+12
test/data/case_sensitivity.ini
···11+# Case sensitivity tests
22+# Section names are case-sensitive
33+# Option names are case-insensitive
44+55+[Section]
66+OPTION = value1
77+88+[SECTION]
99+option = value2
1010+1111+[section]
1212+Option = value3
+20
test/data/comments.ini
···11+# This is a comment
22+; This is also a comment
33+44+[section]
55+# Comment before option
66+key1 = value1
77+key2 = value2 ; inline comment
88+key3 = value3 # another inline comment
99+1010+; Comment between options
1111+1212+key4 = value4
1313+1414+[Commented Bar]
1515+baz=qwe ; a comment
1616+foo: bar # not a comment!
1717+# but this is a comment
1818+; another comment
1919+quirk: this;is not a comment
2020+; a space must precede an inline comment
···11+# Basic interpolation test cases
22+[Foo]
33+bar=something %(with1)s interpolation (1 step)
44+bar9=something %(with9)s lots of interpolation (9 steps)
55+bar10=something %(with10)s lots of interpolation (10 steps)
66+bar11=something %(with11)s lots of interpolation (11 steps)
77+with11=%(with10)s
88+with10=%(with9)s
99+with9=%(with8)s
1010+with8=%(With7)s
1111+with7=%(WITH6)s
1212+with6=%(with5)s
1313+With5=%(with4)s
1414+WITH4=%(with3)s
1515+with3=%(with2)s
1616+with2=%(with1)s
1717+with1=with
1818+1919+[Mutual Recursion]
2020+foo=%(bar)s
2121+bar=%(foo)s
2222+2323+[Interpolation Error]
2424+# no definition for 'reference'
2525+name=%(reference)s
+22
test/data/interpolation_extended.ini
···11+# Extended interpolation test cases
22+[common]
33+favourite Beatle = Paul
44+favourite color = green
55+66+[tom]
77+favourite band = ${favourite color} day
88+favourite pope = John ${favourite Beatle} II
99+sequel = ${favourite pope}I
1010+1111+[ambv]
1212+favourite Beatle = George
1313+son of Edward VII = ${favourite Beatle} V
1414+son of George V = ${son of Edward VII}I
1515+1616+[stanley]
1717+favourite Beatle = ${ambv:favourite Beatle}
1818+favourite pope = ${tom:favourite pope}
1919+favourite color = black
2020+favourite state of mind = paranoid
2121+favourite movie = soylent ${common:favourite color}
2222+favourite song = ${favourite color} sabbath - ${favourite state of mind}
+24
test/data/multiline.ini
···11+# Multiline value test cases
22+[Long Line]
33+foo: this line is much, much longer than my editor
44+ likes it.
55+66+[DEFAULT]
77+foo: another very
88+ long line
99+1010+[Long Line - With Comments!]
1111+test : we ; can
1212+ also ; place
1313+ comments ; in
1414+ multiline ; values
1515+1616+[MultiValue]
1717+value1 = first line
1818+ second line
1919+ third line
2020+2121+value2 = line 1
2222+ line 2
2323+ line 3
2424+ line 4