OCaml codecs for Python INI file handling compatible with ConfigParser
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Declarative INI data manipulation for OCaml.
7
8 Init provides bidirectional codecs for INI files following Python's
9 configparser semantics. *)
10
11type 'a fmt = Format.formatter -> 'a -> unit
12
13(* ---- Text Locations ---- *)
14
15module Textloc = struct
16 type fpath = string
17 let file_none = "-"
18
19 type byte_pos = int
20 let byte_pos_none = -1
21
22 type line_num = int
23 let line_num_none = -1
24
25 type line_pos = line_num * byte_pos
26 let line_pos_first = (1, 0)
27 let line_pos_none = (line_num_none, byte_pos_none)
28
29 type t = {
30 file : fpath;
31 first_byte : byte_pos;
32 last_byte : byte_pos;
33 first_line : line_pos;
34 last_line : line_pos;
35 }
36
37 let none = {
38 file = file_none;
39 first_byte = byte_pos_none;
40 last_byte = byte_pos_none;
41 first_line = line_pos_none;
42 last_line = line_pos_none;
43 }
44
45 let make ~file ~first_byte ~last_byte ~first_line ~last_line =
46 { file; first_byte; last_byte; first_line; last_line }
47
48 let file t = t.file
49 let set_file t file = { t with file }
50 let first_byte t = t.first_byte
51 let last_byte t = t.last_byte
52 let first_line t = t.first_line
53 let last_line t = t.last_line
54
55 let is_none t = t.first_byte < 0
56 let is_empty t = t.first_byte > t.last_byte
57
58 let equal t0 t1 =
59 String.equal t0.file t1.file &&
60 t0.first_byte = t1.first_byte &&
61 t0.last_byte = t1.last_byte
62
63 let compare t0 t1 =
64 let c = String.compare t0.file t1.file in
65 if c <> 0 then c else
66 let c = Int.compare t0.first_byte t1.first_byte in
67 if c <> 0 then c else
68 Int.compare t0.last_byte t1.last_byte
69
70 let set_first t ~first_byte ~first_line =
71 { t with first_byte; first_line }
72
73 let set_last t ~last_byte ~last_line =
74 { t with last_byte; last_line }
75
76 let to_first t =
77 { t with last_byte = t.first_byte; last_line = t.first_line }
78
79 let to_last t =
80 { t with first_byte = t.last_byte; first_line = t.last_line }
81
82 let before t =
83 { t with last_byte = t.first_byte - 1; last_line = t.first_line }
84
85 let after t =
86 { t with first_byte = t.last_byte + 1; first_line = t.last_line }
87
88 let span t0 t1 =
89 let first_byte, first_line, last_byte, last_line, file =
90 if t0.first_byte <= t1.first_byte then
91 if t0.last_byte >= t1.last_byte then
92 t0.first_byte, t0.first_line, t0.last_byte, t0.last_line, t0.file
93 else
94 t0.first_byte, t0.first_line, t1.last_byte, t1.last_line, t1.file
95 else
96 if t1.last_byte >= t0.last_byte then
97 t1.first_byte, t1.first_line, t1.last_byte, t1.last_line, t1.file
98 else
99 t1.first_byte, t1.first_line, t0.last_byte, t0.last_line, t0.file
100 in
101 { file; first_byte; last_byte; first_line; last_line }
102
103 let reloc ~first ~last =
104 { file = last.file;
105 first_byte = first.first_byte;
106 first_line = first.first_line;
107 last_byte = last.last_byte;
108 last_line = last.last_line }
109
110 let pp_ocaml ppf t =
111 let l, c = t.first_line in
112 let el, ec = t.last_line in
113 if is_none t then Format.fprintf ppf "%s" t.file
114 else if is_empty t then
115 Format.fprintf ppf "%s:%d:%d" t.file l (t.first_byte - c)
116 else if l = el then
117 Format.fprintf ppf "%s:%d:%d-%d" t.file l (t.first_byte - c) (t.last_byte - ec)
118 else
119 Format.fprintf ppf "%s:%d:%d-%d:%d" t.file l (t.first_byte - c) el (t.last_byte - ec)
120
121 let pp_gnu ppf t =
122 let l, c = t.first_line in
123 if is_none t then Format.fprintf ppf "%s" t.file
124 else Format.fprintf ppf "%s:%d.%d" t.file l (t.first_byte - c + 1)
125
126 let pp = pp_ocaml
127
128 let pp_dump ppf t =
129 Format.fprintf ppf "@[<h>{file=%S;@ first_byte=%d;@ last_byte=%d;@ \
130 first_line=(%d,%d);@ last_line=(%d,%d)}@]"
131 t.file t.first_byte t.last_byte
132 (fst t.first_line) (snd t.first_line)
133 (fst t.last_line) (snd t.last_line)
134end
135
136(* ---- Metadata ---- *)
137
138module Meta = struct
139 type t = {
140 textloc : Textloc.t;
141 ws_before : string;
142 ws_after : string;
143 comment : string option; (* Associated comment *)
144 }
145
146 let none = {
147 textloc = Textloc.none;
148 ws_before = "";
149 ws_after = "";
150 comment = None;
151 }
152
153 let make ?(ws_before = "") ?(ws_after = "") ?comment textloc =
154 { textloc; ws_before; ws_after; comment }
155
156 let is_none t = Textloc.is_none t.textloc
157 let textloc t = t.textloc
158 let ws_before t = t.ws_before
159 let ws_after t = t.ws_after
160 let comment t = t.comment
161
162 let with_textloc t textloc = { t with textloc }
163 let with_ws_before t ws_before = { t with ws_before }
164 let with_ws_after t ws_after = { t with ws_after }
165 let with_comment t comment = { t with comment }
166
167 let clear_ws t = { t with ws_before = ""; ws_after = "" }
168 let clear_textloc t = { t with textloc = Textloc.none }
169
170 let copy_ws src ~dst =
171 { dst with ws_before = src.ws_before; ws_after = src.ws_after }
172end
173
174type 'a node = 'a * Meta.t
175
176(* ---- Paths ---- *)
177
178module Path = struct
179 type index =
180 | Section of string node
181 | Option of string node
182
183 let pp_index ppf = function
184 | Section (s, _) -> Format.fprintf ppf "[%s]" s
185 | Option (s, _) -> Format.fprintf ppf "%s" s
186
187 type t = index list (* Reversed *)
188
189 let root = []
190 let is_root = function [] -> true | _ -> false
191
192 let section ?(meta = Meta.none) name path = Section (name, meta) :: path
193 let option ?(meta = Meta.none) name path = Option (name, meta) :: path
194
195 let rev_indices t = t
196
197 let pp ppf t =
198 let rec loop = function
199 | [] -> ()
200 | [i] -> pp_index ppf i
201 | i :: rest -> loop rest; Format.fprintf ppf "/"; pp_index ppf i
202 in
203 loop (List.rev t)
204end
205
206(* ---- Errors ---- *)
207
208module Error = struct
209 type kind =
210 | Parse of string
211 | Codec of string
212 | Missing_section of string
213 | Missing_option of { section : string; option : string }
214 | Duplicate_section of string
215 | Duplicate_option of { section : string; option : string }
216 | Type_mismatch of { expected : string; got : string }
217 | Interpolation of { option : string; reason : string }
218 | Unknown_option of string
219 | Unknown_section of string
220
221 type t = {
222 kind : kind;
223 meta : Meta.t;
224 path : Path.t;
225 }
226
227 let make ?(meta = Meta.none) ?(path = Path.root) kind =
228 { kind; meta; path }
229
230 let kind e = e.kind
231 let meta e = e.meta
232 let path e = e.path
233
234 exception Error of t
235
236 let raise ?meta ?path kind = raise (Error (make ?meta ?path kind))
237
238 let kind_to_string = function
239 | Parse msg -> Printf.sprintf "parse error: %s" msg
240 | Codec msg -> Printf.sprintf "codec error: %s" msg
241 | Missing_section name -> Printf.sprintf "missing section: [%s]" name
242 | Missing_option { section; option } ->
243 Printf.sprintf "missing option '%s' in section [%s]" option section
244 | Duplicate_section name -> Printf.sprintf "duplicate section: [%s]" name
245 | Duplicate_option { section; option } ->
246 Printf.sprintf "duplicate option '%s' in section [%s]" option section
247 | Type_mismatch { expected; got } ->
248 Printf.sprintf "type mismatch: expected %s, got %s" expected got
249 | Interpolation { option; reason } ->
250 Printf.sprintf "interpolation error in '%s': %s" option reason
251 | Unknown_option name -> Printf.sprintf "unknown option: %s" name
252 | Unknown_section name -> Printf.sprintf "unknown section: [%s]" name
253
254 let to_string e =
255 let loc = if Meta.is_none e.meta then "" else
256 Format.asprintf "%a: " Textloc.pp (Meta.textloc e.meta)
257 in
258 let path = if Path.is_root e.path then "" else
259 Format.asprintf " at %a" Path.pp e.path
260 in
261 Printf.sprintf "%s%s%s" loc (kind_to_string e.kind) path
262
263 let pp ppf e = Format.pp_print_string ppf (to_string e)
264end
265
266(* ---- Codec Types ---- *)
267
268(* Internal representation for codec implementations *)
269module Repr = struct
270 (* A decoded INI value with metadata *)
271 type ini_value = {
272 raw : string; (* Raw string value *)
273 interpolated : string; (* After interpolation *)
274 meta : Meta.t;
275 }
276
277 (* A section's options *)
278 type ini_section = {
279 name : string node;
280 options : (string node * ini_value) list;
281 meta : Meta.t; (* Section header metadata *)
282 }
283
284 (* A full INI document *)
285 type ini_doc = {
286 defaults : (string node * ini_value) list;
287 sections : ini_section list;
288 meta : Meta.t; (* Document metadata *)
289 }
290
291 (* Codec error during decode/encode *)
292 type 'a codec_result = ('a, Error.t) result
293
294 (* Section decoder state *)
295 type 'dec section_state = {
296 decode : ini_section -> 'dec codec_result;
297 encode : 'dec -> ini_section;
298 known_options : string list;
299 unknown_handler : [ `Skip | `Error | `Keep ];
300 }
301
302 (* Document decoder state *)
303 type 'dec document_state = {
304 decode : ini_doc -> 'dec codec_result;
305 encode : 'dec -> ini_doc;
306 known_sections : string list;
307 unknown_handler : [ `Skip | `Error ];
308 }
309end
310
311(* The abstract codec type *)
312type 'a t = {
313 kind : string;
314 doc : string;
315 (* Value-level decode/encode (for individual option values) *)
316 dec : Repr.ini_value -> ('a, Error.t) result;
317 enc : 'a -> Meta.t -> Repr.ini_value;
318 (* Section-level decode/encode (for Section.finish) *)
319 section : 'a Repr.section_state option;
320 (* Document-level decode/encode (for Document.finish) *)
321 document : 'a Repr.document_state option;
322}
323
324let kind c = c.kind
325let doc c = c.doc
326
327let with_doc ?kind:k ?doc:d c =
328 { c with
329 kind = Option.value ~default:c.kind k;
330 doc = Option.value ~default:c.doc d }
331
332let section_state c = c.section
333let document_state c = c.document
334
335(* ---- Base Codecs ---- *)
336
337let make_value_codec ~kind ~doc ~dec ~enc = {
338 kind; doc; dec; enc;
339 section = None;
340 document = None;
341}
342
343let string = make_value_codec
344 ~kind:"string"
345 ~doc:""
346 ~dec:(fun v -> Ok v.Repr.interpolated)
347 ~enc:(fun s meta -> { Repr.raw = s; interpolated = s; meta })
348
349let int = make_value_codec
350 ~kind:"integer"
351 ~doc:""
352 ~dec:(fun v ->
353 match int_of_string_opt v.Repr.interpolated with
354 | Some i -> Ok i
355 | None -> Error (Error.make (Type_mismatch {
356 expected = "integer"; got = v.interpolated })))
357 ~enc:(fun i meta ->
358 let s = Int.to_string i in
359 { Repr.raw = s; interpolated = s; meta })
360
361let int32 = make_value_codec
362 ~kind:"int32"
363 ~doc:""
364 ~dec:(fun v ->
365 match Int32.of_string_opt v.Repr.interpolated with
366 | Some i -> Ok i
367 | None -> Error (Error.make (Type_mismatch {
368 expected = "int32"; got = v.interpolated })))
369 ~enc:(fun i meta ->
370 let s = Int32.to_string i in
371 { Repr.raw = s; interpolated = s; meta })
372
373let int64 = make_value_codec
374 ~kind:"int64"
375 ~doc:""
376 ~dec:(fun v ->
377 match Int64.of_string_opt v.Repr.interpolated with
378 | Some i -> Ok i
379 | None -> Error (Error.make (Type_mismatch {
380 expected = "int64"; got = v.interpolated })))
381 ~enc:(fun i meta ->
382 let s = Int64.to_string i in
383 { Repr.raw = s; interpolated = s; meta })
384
385let float = make_value_codec
386 ~kind:"float"
387 ~doc:""
388 ~dec:(fun v ->
389 match float_of_string_opt v.Repr.interpolated with
390 | Some f -> Ok f
391 | None -> Error (Error.make (Type_mismatch {
392 expected = "float"; got = v.interpolated })))
393 ~enc:(fun f meta ->
394 let s = Float.to_string f in
395 { Repr.raw = s; interpolated = s; meta })
396
397(* Python configparser-compatible boolean parsing *)
398let parse_bool s =
399 match String.lowercase_ascii s with
400 | "1" | "yes" | "true" | "on" -> Some true
401 | "0" | "no" | "false" | "off" -> Some false
402 | _ -> None
403
404let bool = make_value_codec
405 ~kind:"boolean"
406 ~doc:"Accepts: 1/yes/true/on (true), 0/no/false/off (false)"
407 ~dec:(fun v ->
408 match parse_bool v.Repr.interpolated with
409 | Some b -> Ok b
410 | None -> Error (Error.make (Type_mismatch {
411 expected = "boolean (yes/no/true/false/on/off/1/0)";
412 got = v.interpolated })))
413 ~enc:(fun b meta ->
414 let s = if b then "true" else "false" in
415 { Repr.raw = s; interpolated = s; meta })
416
417let bool_01 = make_value_codec
418 ~kind:"boolean (0/1)"
419 ~doc:""
420 ~dec:(fun v ->
421 match v.Repr.interpolated with
422 | "1" -> Ok true
423 | "0" -> Ok false
424 | s -> Error (Error.make (Type_mismatch { expected = "0 or 1"; got = s })))
425 ~enc:(fun b meta ->
426 let s = if b then "1" else "0" in
427 { Repr.raw = s; interpolated = s; meta })
428
429let bool_yesno = make_value_codec
430 ~kind:"boolean (yes/no)"
431 ~doc:""
432 ~dec:(fun v ->
433 match String.lowercase_ascii v.Repr.interpolated with
434 | "yes" -> Ok true
435 | "no" -> Ok false
436 | s -> Error (Error.make (Type_mismatch { expected = "yes or no"; got = s })))
437 ~enc:(fun b meta ->
438 let s = if b then "yes" else "no" in
439 { Repr.raw = s; interpolated = s; meta })
440
441let bool_truefalse = make_value_codec
442 ~kind:"boolean (true/false)"
443 ~doc:""
444 ~dec:(fun v ->
445 match String.lowercase_ascii v.Repr.interpolated with
446 | "true" -> Ok true
447 | "false" -> Ok false
448 | s -> Error (Error.make (Type_mismatch {
449 expected = "true or false"; got = s })))
450 ~enc:(fun b meta ->
451 let s = if b then "true" else "false" in
452 { Repr.raw = s; interpolated = s; meta })
453
454let bool_onoff = make_value_codec
455 ~kind:"boolean (on/off)"
456 ~doc:""
457 ~dec:(fun v ->
458 match String.lowercase_ascii v.Repr.interpolated with
459 | "on" -> Ok true
460 | "off" -> Ok false
461 | s -> Error (Error.make (Type_mismatch { expected = "on or off"; got = s })))
462 ~enc:(fun b meta ->
463 let s = if b then "on" else "off" in
464 { Repr.raw = s; interpolated = s; meta })
465
466(* ---- Combinators ---- *)
467
468let map ?kind:k ?doc:d ~dec ~enc c =
469 let kind = Option.value ~default:c.kind k in
470 let doc = Option.value ~default:c.doc d in
471 { kind; doc;
472 dec = (fun v -> Result.map dec (c.dec v));
473 enc = (fun x meta -> c.enc (enc x) meta);
474 section = None;
475 document = None;
476 }
477
478let enum ?cmp ?kind ?doc assoc =
479 let cmp = Option.value ~default:Stdlib.compare cmp in
480 let kind = Option.value ~default:"enum" kind in
481 let doc = Option.value ~default:"" doc in
482 let lc_assoc = List.map (fun (k, v) -> (String.lowercase_ascii k, v)) assoc in
483 let rev_assoc = List.map (fun (s, v) -> (v, s)) assoc in
484 make_value_codec ~kind ~doc
485 ~dec:(fun v ->
486 match List.assoc_opt (String.lowercase_ascii v.Repr.interpolated) lc_assoc with
487 | Some x -> Ok x
488 | None -> Error (Error.make (Type_mismatch {
489 expected = kind; got = v.interpolated })))
490 ~enc:(fun x meta ->
491 match List.find_opt (fun (v', _) -> cmp x v' = 0) rev_assoc with
492 | Some (_, s) -> { Repr.raw = s; interpolated = s; meta }
493 | None -> failwith "enum value not in association list")
494
495let option ?kind ?doc c =
496 let kind = Option.value ~default:("optional " ^ c.kind) kind in
497 let doc = Option.value ~default:c.doc doc in
498 { kind; doc;
499 dec = (fun v ->
500 if v.Repr.interpolated = "" then Ok None
501 else Result.map Option.some (c.dec v));
502 enc = (function
503 | Some x -> c.enc x
504 | None -> fun meta -> { Repr.raw = ""; interpolated = ""; meta });
505 section = None;
506 document = None;
507 }
508
509let default def c = {
510 c with
511 dec = (fun v -> Ok (Result.value ~default:def (c.dec v)));
512}
513
514let list ?(sep = ',') c = {
515 kind = "list of " ^ c.kind;
516 doc = "";
517 dec = (fun v ->
518 if v.Repr.interpolated = "" then Ok []
519 else
520 let parts = String.split_on_char sep v.Repr.interpolated in
521 let parts = List.map String.trim parts in
522 let rec decode_all acc = function
523 | [] -> Ok (List.rev acc)
524 | part :: rest ->
525 let pv = { v with Repr.raw = part; interpolated = part } in
526 match c.dec pv with
527 | Ok x -> decode_all (x :: acc) rest
528 | Error e -> Error e
529 in
530 decode_all [] parts);
531 enc = (fun xs meta ->
532 let parts = List.map (fun x -> (c.enc x meta).Repr.interpolated) xs in
533 let s = String.concat (String.make 1 sep ^ " ") parts in
534 { Repr.raw = s; interpolated = s; meta });
535 section = None;
536 document = None;
537}
538
539(* ---- Result helpers ---- *)
540
541module Result_syntax = struct
542 let ( let* ) = Result.bind
543end
544
545(* ---- Section Codecs ---- *)
546
547module Section = struct
548 type 'a codec = 'a t
549
550 type ('o, 'dec) map = {
551 kind : string;
552 doc : string;
553 decode : Repr.ini_section -> 'dec Repr.codec_result;
554 encode : 'o -> Repr.ini_section;
555 known : string list;
556 unknown : [ `Skip | `Error | `Keep ];
557 }
558
559 let obj ?kind ?doc (f : 'dec) : ('o, 'dec) map =
560 let kind = Option.value ~default:"section" kind in
561 let doc = Option.value ~default:"" doc in
562 {
563 kind; doc;
564 decode = (fun _ -> Ok f);
565 encode = (fun _ -> {
566 Repr.name = ("", Meta.none);
567 options = [];
568 meta = Meta.none;
569 });
570 known = [];
571 unknown = `Skip;
572 }
573
574 let mem ?doc:_ ?dec_absent ?enc ?enc_omit name (c : 'a codec)
575 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map =
576 let open Result_syntax in
577 let lc_name = String.lowercase_ascii name in
578 {
579 m with
580 known = lc_name :: m.known;
581 decode = (fun sec ->
582 let opt = List.find_opt (fun ((n, _), _) ->
583 String.lowercase_ascii n = lc_name) sec.Repr.options in
584 let decoded = match opt with
585 | Some (_, v) -> c.dec v
586 | None ->
587 Option.to_result
588 ~none:(Error.make (Missing_option { section = fst sec.name; option = name }))
589 dec_absent
590 in
591 let* a = decoded in
592 let* f = m.decode sec in
593 Ok (f a));
594 encode = (fun o ->
595 let sec = m.encode o in
596 match enc with
597 | None -> sec
598 | Some enc_fn ->
599 let v = enc_fn o in
600 let should_omit = Option.fold ~none:false ~some:(fun f -> f v) enc_omit in
601 if should_omit then sec
602 else
603 let iv = c.enc v Meta.none in
604 { sec with options = ((name, Meta.none), iv) :: sec.options });
605 }
606
607 let opt_mem ?doc ?enc name c m =
608 let opt_c = option c in
609 let enc' = Option.map (fun f o -> f o) enc in
610 mem ?doc ~dec_absent:None ?enc:enc' ~enc_omit:Option.is_none name opt_c m
611
612 let skip_unknown m = { m with unknown = `Skip }
613 let error_unknown m = { m with unknown = `Error }
614
615 let keep_unknown ?enc (m : ('o, (string * string) list -> 'dec) map)
616 : ('o, 'dec) map =
617 {
618 kind = m.kind;
619 doc = m.doc;
620 known = m.known;
621 unknown = `Keep;
622 decode = (fun sec ->
623 let unknown_opts = List.filter_map (fun ((n, _), v) ->
624 let lc_n = String.lowercase_ascii n in
625 if List.mem lc_n m.known then None
626 else Some (n, v.Repr.interpolated)
627 ) sec.Repr.options in
628 m.decode sec |> Result.map (fun f -> f unknown_opts));
629 encode = (fun o ->
630 let sec = m.encode o in
631 match enc with
632 | None -> sec
633 | Some enc_fn ->
634 let new_opts = List.map (fun (k, v) ->
635 ((k, Meta.none), { Repr.raw = v; interpolated = v; meta = Meta.none })
636 ) (enc_fn o) in
637 { sec with options = new_opts @ sec.options });
638 }
639
640 let finish (m : ('o, 'o) map) : 'o codec =
641 let section_state : 'o Repr.section_state = {
642 decode = (fun sec ->
643 (* Check for unknown options *)
644 (match m.unknown with
645 | `Skip -> ()
646 | `Keep -> ()
647 | `Error ->
648 List.iter (fun ((n, _), _) ->
649 let lc_n = String.lowercase_ascii n in
650 if not (List.mem lc_n m.known) then
651 Error.raise (Unknown_option n)
652 ) sec.Repr.options);
653 m.decode sec);
654 encode = (fun o ->
655 let sec = m.encode o in
656 { sec with options = List.rev sec.options });
657 known_options = m.known;
658 unknown_handler = m.unknown;
659 } in
660 {
661 kind = m.kind;
662 doc = m.doc;
663 dec = (fun _ -> Error (Error.make (Codec "section codec requires section-level decode")));
664 enc = (fun _ _ -> { Repr.raw = ""; interpolated = ""; meta = Meta.none });
665 section = Some section_state;
666 document = None;
667 }
668end
669
670(* ---- Document Codecs ---- *)
671
672module Document = struct
673 type 'a codec = 'a t
674
675 type ('o, 'dec) map = {
676 kind : string;
677 doc : string;
678 decode : Repr.ini_doc -> 'dec Repr.codec_result;
679 encode : 'o -> Repr.ini_doc;
680 known : string list;
681 unknown : [ `Skip | `Error ];
682 }
683
684 let obj ?kind ?doc (f : 'dec) : ('o, 'dec) map =
685 let kind = Option.value ~default:"document" kind in
686 let doc = Option.value ~default:"" doc in
687 {
688 kind; doc;
689 decode = (fun _ -> Ok f);
690 encode = (fun _ -> {
691 Repr.defaults = [];
692 sections = [];
693 meta = Meta.none;
694 });
695 known = [];
696 unknown = `Skip;
697 }
698
699 let get_section_state sec_codec fn_name =
700 match sec_codec.section with
701 | Some s -> s
702 | None -> failwith (fn_name ^ ": codec must be a section codec")
703
704 let section ?doc:_ ?enc name (sec_codec : 'a codec)
705 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map =
706 let open Result_syntax in
707 let sec_state = get_section_state sec_codec "section" in
708 let lc_name = String.lowercase_ascii name in
709 {
710 m with
711 known = lc_name :: m.known;
712 decode = (fun doc ->
713 let sec = List.find_opt (fun s ->
714 String.lowercase_ascii (fst s.Repr.name) = lc_name) doc.Repr.sections in
715 let* sec = Option.to_result ~none:(Error.make (Missing_section name)) sec in
716 let* a = sec_state.decode sec in
717 let* f = m.decode doc in
718 Ok (f a));
719 encode = (fun o ->
720 let doc = m.encode o in
721 match enc with
722 | None -> doc
723 | Some enc_fn ->
724 let sec = sec_state.encode (enc_fn o) in
725 { doc with sections = { sec with name = (name, Meta.none) } :: doc.sections });
726 }
727
728 let opt_section ?doc:_ ?enc name (sec_codec : 'a codec)
729 (m : ('o, 'a option -> 'dec) map) : ('o, 'dec) map =
730 let open Result_syntax in
731 let sec_state = get_section_state sec_codec "opt_section" in
732 let lc_name = String.lowercase_ascii name in
733 {
734 m with
735 known = lc_name :: m.known;
736 decode = (fun doc ->
737 let sec = List.find_opt (fun s ->
738 String.lowercase_ascii (fst s.Repr.name) = lc_name) doc.Repr.sections in
739 let* value = match sec with
740 | None -> Ok None
741 | Some sec ->
742 let* a = sec_state.decode sec in
743 Ok (Some a)
744 in
745 let* f = m.decode doc in
746 Ok (f value));
747 encode = (fun o ->
748 let doc = m.encode o in
749 match enc with
750 | None -> doc
751 | Some enc_fn ->
752 match enc_fn o with
753 | None -> doc
754 | Some v ->
755 let sec = sec_state.encode v in
756 { doc with sections = { sec with name = (name, Meta.none) } :: doc.sections });
757 }
758
759 let defaults ?doc:_ ?enc (sec_codec : 'a codec)
760 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map =
761 let open Result_syntax in
762 let sec_state = get_section_state sec_codec "defaults" in
763 {
764 m with
765 known = "default" :: m.known;
766 decode = (fun doc ->
767 let fake_sec = {
768 Repr.name = ("DEFAULT", Meta.none);
769 options = doc.defaults;
770 meta = Meta.none;
771 } in
772 let* a = sec_state.decode fake_sec in
773 let* f = m.decode doc in
774 Ok (f a));
775 encode = (fun o ->
776 let doc = m.encode o in
777 match enc with
778 | None -> doc
779 | Some enc_fn ->
780 let v = enc_fn o in
781 let sec = sec_state.encode v in
782 { doc with defaults = sec.options });
783 }
784
785 let opt_defaults ?doc:_ ?enc (sec_codec : 'a codec)
786 (m : ('o, 'a option -> 'dec) map) : ('o, 'dec) map =
787 let open Result_syntax in
788 let sec_state = get_section_state sec_codec "opt_defaults" in
789 {
790 m with
791 known = "default" :: m.known;
792 decode = (fun doc ->
793 let* value =
794 if doc.Repr.defaults = [] then Ok None
795 else
796 let fake_sec = {
797 Repr.name = ("DEFAULT", Meta.none);
798 options = doc.defaults;
799 meta = Meta.none;
800 } in
801 let* a = sec_state.decode fake_sec in
802 Ok (Some a)
803 in
804 let* f = m.decode doc in
805 Ok (f value));
806 encode = (fun o ->
807 let doc = m.encode o in
808 match enc with
809 | None -> doc
810 | Some enc_fn ->
811 match enc_fn o with
812 | None -> doc
813 | Some v ->
814 let sec = sec_state.encode v in
815 { doc with defaults = sec.options });
816 }
817
818 let skip_unknown m = { m with unknown = `Skip }
819 let error_unknown m = { m with unknown = `Error }
820
821 let finish (m : ('o, 'o) map) : 'o codec =
822 let document_state : 'o Repr.document_state = {
823 decode = (fun doc ->
824 (* Check for unknown sections *)
825 (match m.unknown with
826 | `Skip -> ()
827 | `Error ->
828 List.iter (fun sec ->
829 let lc_n = String.lowercase_ascii (fst sec.Repr.name) in
830 if not (List.mem lc_n m.known) then
831 Error.raise (Unknown_section (fst sec.name))
832 ) doc.Repr.sections);
833 m.decode doc);
834 encode = (fun o ->
835 let doc = m.encode o in
836 { doc with sections = List.rev doc.sections });
837 known_sections = m.known;
838 unknown_handler = m.unknown;
839 } in
840 {
841 kind = m.kind;
842 doc = m.doc;
843 dec = (fun _ -> Error (Error.make (Codec "document codec requires document-level decode")));
844 enc = (fun _ _ -> { Repr.raw = ""; interpolated = ""; meta = Meta.none });
845 section = None;
846 document = Some document_state;
847 }
848end