OCaml codecs for Python INI file handling compatible with ConfigParser
at main 848 lines 26 kB view raw
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