OCaml codecs for Python INI file handling compatible with ConfigParser

metadata

+301 -195
+9 -1
.gitignore
··· 1 - _build 1 + _build/ 2 + _opam/ 3 + *.install 4 + *.merlin 5 + .merlin 6 + third_party/ 7 + .DS_Store 8 + *.swp 9 + *~
+1 -1
.ocamlformat
··· 1 - version = 0.27.0 1 + version = 0.28.1 2 2 profile = default
+53
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + 31 + steps: 32 + - name: opam 33 + command: | 34 + opam init --disable-sandboxing -a -y 35 + - name: repo 36 + command: | 37 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 38 + - name: switch 39 + command: | 40 + opam install . --confirm-level=unsafe-yes --deps-only 41 + - name: build 42 + command: | 43 + opam exec -- dune build 44 + - name: switch-test 45 + command: | 46 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 47 + - name: test 48 + command: | 49 + opam exec -- dune runtest --verbose 50 + - name: doc 51 + command: | 52 + opam install -y odoc 53 + opam exec -- dune build @doc
+15
LICENSE
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+88
README.md
··· 1 + # init - Declarative INI Data Manipulation for OCaml 2 + 3 + Init provides bidirectional codecs for INI files following Python's 4 + configparser semantics. Define your configuration types once, and Init 5 + handles both parsing and serialization. 6 + 7 + ## Features 8 + 9 + - **Python-compatible**: Follows configparser semantics for maximum compatibility 10 + - **Bidirectional codecs**: Define once, use for both decoding and encoding 11 + - **Type-safe**: Strongly-typed configuration with compile-time guarantees 12 + - **Multiline values**: Support for continuation lines via indentation 13 + - **Interpolation**: Basic `%(name)s` and extended `${section:name}` variable substitution 14 + - **DEFAULT section**: Automatic inheritance of default values 15 + - **Optional values**: Graceful handling of missing configuration options 16 + 17 + ## Installation 18 + 19 + ```bash 20 + opam install init 21 + ``` 22 + 23 + For parsing/encoding support, also install the bytesrw sub-library: 24 + 25 + ```bash 26 + opam install init bytesrw 27 + ``` 28 + 29 + For Eio file system integration: 30 + 31 + ```bash 32 + opam install init bytesrw eio bytesrw-eio 33 + ``` 34 + 35 + ## Quick Start 36 + 37 + ```ocaml 38 + (* Define your configuration type *) 39 + type server_config = { 40 + host : string; 41 + port : int; 42 + debug : bool; 43 + } 44 + 45 + (* Define the codec *) 46 + let server_codec = Init.Section.( 47 + obj (fun host port debug -> { host; port; debug }) 48 + |> mem "host" Init.string ~enc:(fun c -> c.host) 49 + |> mem "port" Init.int ~enc:(fun c -> c.port) 50 + |> mem "debug" Init.bool ~dec_absent:false ~enc:(fun c -> c.debug) 51 + |> finish 52 + ) 53 + 54 + let config_codec = Init.Document.( 55 + obj (fun server -> server) 56 + |> section "server" server_codec ~enc:Fun.id 57 + |> finish 58 + ) 59 + 60 + (* Parse an INI file *) 61 + let () = 62 + let ini = {| 63 + [server] 64 + host = localhost 65 + port = 8080 66 + debug = yes 67 + |} in 68 + match Init_bytesrw.decode_string config_codec ini with 69 + | Ok config -> 70 + Printf.printf "Server: %s:%d (debug=%b)\n" 71 + config.host config.port config.debug 72 + | Error msg -> 73 + Printf.printf "Error: %s\n" msg 74 + ``` 75 + 76 + ## Sub-libraries 77 + 78 + - **init**: Core codec combinators (no dependencies) 79 + - **init.bytesrw**: Parsing and encoding using bytesrw 80 + - **init.eio**: Eio file system integration 81 + 82 + ## Documentation 83 + 84 + See the [API documentation](https://ocaml.org/p/init/latest/doc/Init/index.html) for complete reference. 85 + 86 + ## License 87 + 88 + ISC License. See [LICENSE](LICENSE) for details.
+3 -3
dune-project
··· 1 1 (lang dune 3.0) 2 2 (name init) 3 - (version 0.1.0) 4 3 5 4 (generate_opam_files true) 6 5 7 6 (license ISC) 8 7 (authors "Anil Madhavapeddy <anil@recoil.org>") 9 8 (maintainers "Anil Madhavapeddy <anil@recoil.org>") 10 - (source (github avsm/ocaml-init)) 11 9 12 10 (package 13 11 (name init) ··· 21 19 sub-library provides parsing/encoding with bytesrw. The optional init.eio 22 20 sub-library provides Eio file system integration.") 23 21 (depends 24 - (ocaml (>= 4.14.0))) 22 + (ocaml (>= 4.14.0)) 23 + (alcotest (and :with-test (>= 1.7.0))) 24 + (str :with-test)) 25 25 (depopts bytesrw eio bytesrw-eio) 26 26 (conflicts 27 27 (bytesrw (< 0.1.0))
+2 -4
init.opam
··· 1 1 # This file is generated by dune, edit dune-project instead 2 2 opam-version: "2.0" 3 - version: "0.1.0" 4 3 synopsis: "Declarative INI data manipulation for OCaml" 5 4 description: """ 6 5 Init provides bidirectional codecs for INI files following Python's ··· 13 12 maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 14 13 authors: ["Anil Madhavapeddy <anil@recoil.org>"] 15 14 license: "ISC" 16 - homepage: "https://github.com/avsm/ocaml-init" 17 - bug-reports: "https://github.com/avsm/ocaml-init/issues" 18 15 depends: [ 19 16 "dune" {>= "3.0"} 20 17 "ocaml" {>= "4.14.0"} 18 + "alcotest" {with-test & >= "1.7.0"} 19 + "str" {with-test} 21 20 "odoc" {with-doc} 22 21 ] 23 22 depopts: ["bytesrw" "eio" "bytesrw-eio"] ··· 39 38 "@doc" {with-doc} 40 39 ] 41 40 ] 42 - dev-repo: "git+https://github.com/avsm/ocaml-init.git"
+66 -93
src/bytesrw/init_bytesrw.ml
··· 14 14 - DEFAULT section inheritance 15 15 - Case-insensitive option lookup *) 16 16 17 + module Result_syntax = struct 18 + let ( let* ) = Result.bind 19 + end 20 + 17 21 (* ---- Configuration ---- *) 18 22 19 23 type interpolation = ··· 524 528 (* ---- Interpolation Pass ---- *) 525 529 526 530 let perform_interpolation state = 531 + let open Result_syntax in 527 532 let interpolate_value ~section iv = 528 - match interpolate state.config ~section ~defaults:state.defaults ~sections:state.sections iv.Init.Repr.raw with 529 - | Ok interpolated -> Ok { iv with Init.Repr.interpolated = interpolated } 530 - | Error e -> Error e 533 + interpolate state.config ~section ~defaults:state.defaults ~sections:state.sections iv.Init.Repr.raw 534 + |> Result.map (fun interpolated -> { iv with Init.Repr.interpolated = interpolated }) 531 535 in 532 536 let interpolate_opts ~section opts = 533 537 let rec loop acc = function 534 538 | [] -> Ok (List.rev acc) 535 539 | ((name, meta), iv) :: rest -> 536 - match interpolate_value ~section iv with 537 - | Ok iv' -> loop (((name, meta), iv') :: acc) rest 538 - | Error e -> Error e 540 + let* iv' = interpolate_value ~section iv in 541 + loop (((name, meta), iv') :: acc) rest 539 542 in 540 543 loop [] opts 541 544 in 542 - (* Interpolate defaults *) 543 - match interpolate_opts ~section:None state.defaults with 544 - | Error e -> Error e 545 - | Ok defaults' -> 546 - state.defaults <- defaults'; 547 - (* Interpolate sections *) 548 - let rec loop_sections acc = function 549 - | [] -> Ok (List.rev acc) 550 - | sec :: rest -> 551 - match interpolate_opts ~section:(Some (fst sec.Init.Repr.name)) sec.options with 552 - | Ok opts' -> loop_sections ({ sec with options = opts' } :: acc) rest 553 - | Error e -> Error e 554 - in 555 - match loop_sections [] state.sections with 556 - | Error e -> Error e 557 - | Ok sections' -> 558 - state.sections <- sections'; 559 - Ok () 545 + let rec loop_sections acc = function 546 + | [] -> Ok (List.rev acc) 547 + | sec :: rest -> 548 + let* opts' = interpolate_opts ~section:(Some (fst sec.Init.Repr.name)) sec.options in 549 + loop_sections ({ sec with options = opts' } :: acc) rest 550 + in 551 + let* defaults' = interpolate_opts ~section:None state.defaults in 552 + state.defaults <- defaults'; 553 + let* sections' = loop_sections [] state.sections in 554 + state.sections <- sections'; 555 + Ok () 560 556 561 557 (* ---- Line splitting ---- *) 562 558 ··· 583 579 (* ---- Main Parse Functions ---- *) 584 580 585 581 let parse_string_internal ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) s = 582 + let open Result_syntax in 586 583 let _ = locs in (* TODO: Use locs to control location tracking *) 587 584 let _ = layout in (* TODO: Use layout to control whitespace preservation *) 588 585 let state = make_state config file in ··· 592 589 finalize_current_option state; 593 590 Ok () 594 591 | line :: rest -> 595 - match process_line state line with 596 - | Ok () -> process rest 597 - | Error e -> Error e 592 + let* () = process_line state line in 593 + process rest 598 594 in 599 - match process lines with 600 - | Error e -> Error e 601 - | Ok () -> 602 - (* Perform interpolation *) 603 - match perform_interpolation state with 604 - | Error e -> Error e 605 - | Ok () -> 606 - let doc = { 607 - Init.Repr.defaults = List.rev state.defaults; 608 - sections = List.rev_map (fun (sec : Init.Repr.ini_section) -> 609 - { sec with options = List.rev sec.options } 610 - ) state.sections; 611 - meta = Init.Meta.none; 612 - } in 613 - Ok doc 595 + let* () = process lines in 596 + let* () = perform_interpolation state in 597 + Ok { 598 + Init.Repr.defaults = List.rev state.defaults; 599 + sections = List.rev_map (fun (sec : Init.Repr.ini_section) -> 600 + { sec with options = List.rev sec.options } 601 + ) state.sections; 602 + meta = Init.Meta.none; 603 + } 614 604 615 605 let parse_reader ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) reader = 616 606 let s = read_all_to_string reader in ··· 621 611 622 612 (* ---- Decoding ---- *) 623 613 624 - let decode' ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) codec reader = 625 - match parse_reader ~config ~locs ~layout ~file reader with 626 - | Error e -> Error e 627 - | Ok doc -> 628 - match Init.document_state codec with 629 - | Some doc_state -> doc_state.decode doc 614 + let decode_doc codec doc = 615 + match Init.document_state codec with 616 + | Some doc_state -> doc_state.decode doc 617 + | None -> 618 + match Init.section_state codec with 619 + | Some sec_state -> 620 + (match doc.Init.Repr.sections with 621 + | [sec] -> sec_state.decode sec 622 + | [] -> Error (Init.Error.make (Init.Error.Codec "no sections in document")) 623 + | _ -> Error (Init.Error.make (Init.Error.Codec "multiple sections; expected single section codec"))) 630 624 | None -> 631 - (* Maybe it's a section codec - try to decode from first/only section *) 632 - match Init.section_state codec with 633 - | Some sec_state -> 634 - (match doc.sections with 635 - | [sec] -> sec_state.decode sec 636 - | [] -> Error (Init.Error.make (Init.Error.Codec "no sections in document")) 637 - | _ -> Error (Init.Error.make (Init.Error.Codec "multiple sections; expected single section codec"))) 638 - | None -> 639 - Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 625 + Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 626 + 627 + let decode' ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) codec reader = 628 + let open Result_syntax in 629 + let* doc = parse_reader ~config ~locs ~layout ~file reader in 630 + decode_doc codec doc 640 631 641 632 let decode ?config ?locs ?layout ?file codec reader = 642 - match decode' ?config ?locs ?layout ?file codec reader with 643 - | Ok v -> Ok v 644 - | Error e -> Error (Init.Error.to_string e) 633 + decode' ?config ?locs ?layout ?file codec reader 634 + |> Result.map_error Init.Error.to_string 645 635 646 636 let decode_string' ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) codec s = 647 - match parse_string ~config ~locs ~layout ~file s with 648 - | Error e -> Error e 649 - | Ok doc -> 650 - match Init.document_state codec with 651 - | Some doc_state -> doc_state.decode doc 652 - | None -> 653 - match Init.section_state codec with 654 - | Some sec_state -> 655 - (match doc.sections with 656 - | [sec] -> sec_state.decode sec 657 - | [] -> Error (Init.Error.make (Init.Error.Codec "no sections in document")) 658 - | _ -> Error (Init.Error.make (Init.Error.Codec "multiple sections; expected single section codec"))) 659 - | None -> 660 - Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 637 + let open Result_syntax in 638 + let* doc = parse_string ~config ~locs ~layout ~file s in 639 + decode_doc codec doc 661 640 662 641 let decode_string ?config ?locs ?layout ?file codec s = 663 - match decode_string' ?config ?locs ?layout ?file codec s with 664 - | Ok v -> Ok v 665 - | Error e -> Error (Init.Error.to_string e) 642 + decode_string' ?config ?locs ?layout ?file codec s 643 + |> Result.map_error Init.Error.to_string 666 644 667 645 (* ---- Encoding ---- *) 668 646 ··· 713 691 Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 714 692 715 693 let encode' ?buf:_ codec value ~eod writer = 694 + let open Result_syntax in 716 695 let buffer = Buffer.create 1024 in 717 - match encode_to_buffer buffer codec value with 718 - | Error e -> Error e 719 - | Ok () -> 720 - let s = Buffer.contents buffer in 721 - Bytes.Writer.write_string writer s; 722 - if eod then Bytes.Writer.write_eod writer; 723 - Ok () 696 + let* () = encode_to_buffer buffer codec value in 697 + Bytes.Writer.write_string writer (Buffer.contents buffer); 698 + if eod then Bytes.Writer.write_eod writer; 699 + Ok () 724 700 725 701 let encode ?buf codec value ~eod writer = 726 - match encode' ?buf codec value ~eod writer with 727 - | Ok () -> Ok () 728 - | Error e -> Error (Init.Error.to_string e) 702 + encode' ?buf codec value ~eod writer 703 + |> Result.map_error Init.Error.to_string 729 704 730 705 let encode_string' ?buf:_ codec value = 731 706 let buffer = Buffer.create 1024 in 732 - match encode_to_buffer buffer codec value with 733 - | Error e -> Error e 734 - | Ok () -> Ok (Buffer.contents buffer) 707 + encode_to_buffer buffer codec value 708 + |> Result.map (fun () -> Buffer.contents buffer) 735 709 736 710 let encode_string ?buf codec value = 737 - match encode_string' ?buf codec value with 738 - | Ok s -> Ok s 739 - | Error e -> Error (Init.Error.to_string e) 711 + encode_string' ?buf codec value 712 + |> Result.map_error Init.Error.to_string
+3 -5
src/eio/init_eio.ml
··· 34 34 let encode_path ?buf codec value path = 35 35 Eio.Path.with_open_out ~create:(`Or_truncate 0o644) path @@ fun flow -> 36 36 let writer = Bytesrw_eio.bytes_writer_of_flow flow in 37 - match Init_bytesrw.encode' ?buf codec value ~eod:true writer with 38 - | Ok () -> Ok () 39 - | Error e -> Error e 37 + Init_bytesrw.encode' ?buf codec value ~eod:true writer 40 38 41 39 let encode_path_exn ?buf codec value path = 42 40 match encode_path ?buf codec value path with ··· 56 54 | Error e -> raise (err e) 57 55 58 56 let encode_flow ?buf codec value ~eod flow = 59 - let writer = Bytesrw_eio.bytes_writer_of_flow flow in 60 - Init_bytesrw.encode' ?buf codec value ~eod writer 57 + Bytesrw_eio.bytes_writer_of_flow flow 58 + |> Init_bytesrw.encode' ?buf codec value ~eod 61 59 62 60 let encode_flow_exn ?buf codec value ~eod flow = 63 61 match encode_flow ?buf codec value ~eod flow with
+61 -88
src/init.ml
··· 508 508 509 509 let default def c = { 510 510 c with 511 - dec = (fun v -> 512 - match c.dec v with 513 - | Ok x -> Ok x 514 - | Error _ -> Ok def); 511 + dec = (fun v -> Ok (Result.value ~default:def (c.dec v))); 515 512 } 516 513 517 514 let list ?(sep = ',') c = { ··· 539 536 document = None; 540 537 } 541 538 539 + (* ---- Result helpers ---- *) 540 + 541 + module Result_syntax = struct 542 + let ( let* ) = Result.bind 543 + end 544 + 542 545 (* ---- Section Codecs ---- *) 543 546 544 547 module Section = struct ··· 570 573 571 574 let mem ?doc:_ ?dec_absent ?enc ?enc_omit name (c : 'a codec) 572 575 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map = 576 + let open Result_syntax in 573 577 let lc_name = String.lowercase_ascii name in 574 578 { 575 579 m with ··· 580 584 let decoded = match opt with 581 585 | Some (_, v) -> c.dec v 582 586 | None -> 583 - match dec_absent with 584 - | Some def -> Ok def 585 - | None -> Error (Error.make (Missing_option { 586 - section = fst sec.name; option = name })) 587 + Option.to_result 588 + ~none:(Error.make (Missing_option { section = fst sec.name; option = name })) 589 + dec_absent 587 590 in 588 - match decoded with 589 - | Ok a -> 590 - (match m.decode sec with 591 - | Ok f -> Ok (f a) 592 - | Error e -> Error e) 593 - | Error e -> Error e); 591 + let* a = decoded in 592 + let* f = m.decode sec in 593 + Ok (f a)); 594 594 encode = (fun o -> 595 595 let sec = m.encode o in 596 596 match enc with 597 597 | None -> sec 598 598 | Some enc_fn -> 599 599 let v = enc_fn o in 600 - let should_omit = match enc_omit with 601 - | Some f -> f v 602 - | None -> false 603 - in 600 + let should_omit = Option.fold ~none:false ~some:(fun f -> f v) enc_omit in 604 601 if should_omit then sec 605 602 else 606 603 let iv = c.enc v Meta.none in ··· 628 625 if List.mem lc_n m.known then None 629 626 else Some (n, v.Repr.interpolated) 630 627 ) sec.Repr.options in 631 - match m.decode sec with 632 - | Ok f -> Ok (f unknown_opts) 633 - | Error e -> Error e); 628 + m.decode sec |> Result.map (fun f -> f unknown_opts)); 634 629 encode = (fun o -> 635 630 let sec = m.encode o in 636 631 match enc with 637 632 | None -> sec 638 633 | Some enc_fn -> 639 - let unknown_opts = enc_fn o in 640 634 let new_opts = List.map (fun (k, v) -> 641 635 ((k, Meta.none), { Repr.raw = v; interpolated = v; meta = Meta.none }) 642 - ) unknown_opts in 636 + ) (enc_fn o) in 643 637 { sec with options = new_opts @ sec.options }); 644 638 } 645 639 ··· 702 696 unknown = `Skip; 703 697 } 704 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 + 705 704 let section ?doc:_ ?enc name (sec_codec : 'a codec) 706 705 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map = 707 - let sec_state = match sec_codec.section with 708 - | Some s -> s 709 - | None -> failwith "section: codec must be a section codec" 710 - in 706 + let open Result_syntax in 707 + let sec_state = get_section_state sec_codec "section" in 711 708 let lc_name = String.lowercase_ascii name in 712 709 { 713 710 m with ··· 715 712 decode = (fun doc -> 716 713 let sec = List.find_opt (fun s -> 717 714 String.lowercase_ascii (fst s.Repr.name) = lc_name) doc.Repr.sections in 718 - match sec with 719 - | None -> Error (Error.make (Missing_section name)) 720 - | Some sec -> 721 - match sec_state.decode sec with 722 - | Ok a -> 723 - (match m.decode doc with 724 - | Ok f -> Ok (f a) 725 - | Error e -> Error e) 726 - | Error e -> Error e); 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)); 727 719 encode = (fun o -> 728 720 let doc = m.encode o in 729 721 match enc with 730 722 | None -> doc 731 723 | Some enc_fn -> 732 - let v = enc_fn o in 733 - let sec = sec_state.encode v in 734 - let sec = { sec with name = (name, Meta.none) } in 735 - { doc with sections = sec :: doc.sections }); 724 + let sec = sec_state.encode (enc_fn o) in 725 + { doc with sections = { sec with name = (name, Meta.none) } :: doc.sections }); 736 726 } 737 727 738 728 let opt_section ?doc:_ ?enc name (sec_codec : 'a codec) 739 729 (m : ('o, 'a option -> 'dec) map) : ('o, 'dec) map = 740 - let sec_state = match sec_codec.section with 741 - | Some s -> s 742 - | None -> failwith "opt_section: codec must be a section codec" 743 - in 730 + let open Result_syntax in 731 + let sec_state = get_section_state sec_codec "opt_section" in 744 732 let lc_name = String.lowercase_ascii name in 745 733 { 746 734 m with ··· 748 736 decode = (fun doc -> 749 737 let sec = List.find_opt (fun s -> 750 738 String.lowercase_ascii (fst s.Repr.name) = lc_name) doc.Repr.sections in 751 - match sec with 752 - | None -> 753 - (match m.decode doc with 754 - | Ok f -> Ok (f None) 755 - | Error e -> Error e) 756 - | Some sec -> 757 - match sec_state.decode sec with 758 - | Ok a -> 759 - (match m.decode doc with 760 - | Ok f -> Ok (f (Some a)) 761 - | Error e -> Error e) 762 - | Error e -> Error e); 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)); 763 747 encode = (fun o -> 764 748 let doc = m.encode o in 765 749 match enc with ··· 769 753 | None -> doc 770 754 | Some v -> 771 755 let sec = sec_state.encode v in 772 - let sec = { sec with name = (name, Meta.none) } in 773 - { doc with sections = sec :: doc.sections }); 756 + { doc with sections = { sec with name = (name, Meta.none) } :: doc.sections }); 774 757 } 775 758 776 759 let defaults ?doc:_ ?enc (sec_codec : 'a codec) 777 760 (m : ('o, 'a -> 'dec) map) : ('o, 'dec) map = 778 - let sec_state = match sec_codec.section with 779 - | Some s -> s 780 - | None -> failwith "defaults: codec must be a section codec" 781 - in 761 + let open Result_syntax in 762 + let sec_state = get_section_state sec_codec "defaults" in 782 763 { 783 764 m with 784 765 known = "default" :: m.known; ··· 788 769 options = doc.defaults; 789 770 meta = Meta.none; 790 771 } in 791 - match sec_state.decode fake_sec with 792 - | Ok a -> 793 - (match m.decode doc with 794 - | Ok f -> Ok (f a) 795 - | Error e -> Error e) 796 - | Error e -> Error e); 772 + let* a = sec_state.decode fake_sec in 773 + let* f = m.decode doc in 774 + Ok (f a)); 797 775 encode = (fun o -> 798 776 let doc = m.encode o in 799 777 match enc with ··· 806 784 807 785 let opt_defaults ?doc:_ ?enc (sec_codec : 'a codec) 808 786 (m : ('o, 'a option -> 'dec) map) : ('o, 'dec) map = 809 - let sec_state = match sec_codec.section with 810 - | Some s -> s 811 - | None -> failwith "opt_defaults: codec must be a section codec" 812 - in 787 + let open Result_syntax in 788 + let sec_state = get_section_state sec_codec "opt_defaults" in 813 789 { 814 790 m with 815 791 known = "default" :: m.known; 816 792 decode = (fun doc -> 817 - if doc.defaults = [] then 818 - (match m.decode doc with 819 - | Ok f -> Ok (f None) 820 - | Error e -> Error e) 821 - else 822 - let fake_sec = { 823 - Repr.name = ("DEFAULT", Meta.none); 824 - options = doc.defaults; 825 - meta = Meta.none; 826 - } in 827 - match sec_state.decode fake_sec with 828 - | Ok a -> 829 - (match m.decode doc with 830 - | Ok f -> Ok (f (Some a)) 831 - | Error e -> Error e) 832 - | Error e -> Error e); 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)); 833 806 encode = (fun o -> 834 807 let doc = m.encode o in 835 808 match enc with