OCaml codecs for Python INI file handling compatible with ConfigParser
at main 712 lines 25 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6open Bytesrw 7 8(** INI parser and encoder using bytesrw. 9 10 Implements Python configparser semantics including: 11 - Multiline values via indentation 12 - Basic interpolation: %(name)s 13 - Extended interpolation: ${section:name} 14 - DEFAULT section inheritance 15 - Case-insensitive option lookup *) 16 17module Result_syntax = struct 18 let ( let* ) = Result.bind 19end 20 21(* ---- Configuration ---- *) 22 23type interpolation = 24 [ `No_interpolation 25 | `Basic_interpolation 26 | `Extended_interpolation ] 27 28type config = { 29 delimiters : string list; 30 comment_prefixes : string list; 31 inline_comment_prefixes : string list; 32 default_section : string; 33 interpolation : interpolation; 34 allow_no_value : bool; 35 strict : bool; 36 empty_lines_in_values : bool; 37} 38 39let default_config = { 40 delimiters = ["="; ":"]; 41 comment_prefixes = ["#"; ";"]; 42 inline_comment_prefixes = []; 43 default_section = "DEFAULT"; 44 interpolation = `Basic_interpolation; 45 allow_no_value = false; 46 strict = true; 47 empty_lines_in_values = true; 48} 49 50let raw_config = { default_config with interpolation = `No_interpolation } 51 52(* ---- Reading from bytesrw ---- *) 53 54let read_all_to_string reader = 55 let buf = Buffer.create 4096 in 56 let rec loop () = 57 let slice = Bytes.Reader.read reader in 58 if Bytes.Slice.length slice = 0 then 59 Buffer.contents buf 60 else begin 61 Buffer.add_subbytes buf 62 (Bytes.Slice.bytes slice) 63 (Bytes.Slice.first slice) 64 (Bytes.Slice.length slice); 65 loop () 66 end 67 in 68 loop () 69 70(* ---- Parsing State ---- *) 71 72type parse_state = { 73 mutable file : string; 74 mutable line_num : int; 75 mutable byte_pos : int; 76 mutable line_start_byte : int; 77 config : config; 78 (* Accumulated data *) 79 mutable defaults : (string Init.node * Init.Repr.ini_value) list; 80 mutable sections : Init.Repr.ini_section list; 81 (* Current parse state *) 82 mutable cur_section : string option; 83 mutable cur_option : (string * Init.Meta.t) option; 84 mutable cur_value : string list; 85 mutable cur_indent : int; 86 mutable cur_value_meta : Init.Meta.t; 87 mutable pending_ws : string; 88} 89 90let make_state config file = { 91 file; 92 line_num = 1; 93 byte_pos = 0; 94 line_start_byte = 0; 95 config; 96 defaults = []; 97 sections = []; 98 cur_section = None; 99 cur_option = None; 100 cur_value = []; 101 cur_indent = 0; 102 cur_value_meta = Init.Meta.none; 103 pending_ws = ""; 104} 105 106let current_textloc state first_byte last_byte first_line = 107 Init.Textloc.make 108 ~file:state.file 109 ~first_byte ~last_byte 110 ~first_line 111 ~last_line:(state.line_num, state.line_start_byte) 112 113let current_meta state first_byte first_line = 114 let textloc = current_textloc state first_byte state.byte_pos first_line in 115 Init.Meta.make textloc 116 117(* ---- String Utilities ---- *) 118 119let string_starts_with ~prefix s = 120 let plen = String.length prefix in 121 let slen = String.length s in 122 slen >= plen && String.sub s 0 plen = prefix 123 124let lstrip s = 125 let len = String.length s in 126 let rec find_start i = 127 if i >= len then len 128 else match s.[i] with 129 | ' ' | '\t' -> find_start (i + 1) 130 | _ -> i 131 in 132 let start = find_start 0 in 133 if start = 0 then s 134 else String.sub s start (len - start) 135 136let rstrip s = 137 let rec find_end i = 138 if i < 0 then -1 139 else match s.[i] with 140 | ' ' | '\t' | '\r' | '\n' -> find_end (i - 1) 141 | _ -> i 142 in 143 let end_pos = find_end (String.length s - 1) in 144 if end_pos = String.length s - 1 then s 145 else String.sub s 0 (end_pos + 1) 146 147let strip s = lstrip (rstrip s) 148 149let count_indent s = 150 let len = String.length s in 151 let rec count i = 152 if i >= len then i 153 else match s.[i] with 154 | ' ' | '\t' -> count (i + 1) 155 | _ -> i 156 in 157 count 0 158 159(* ---- Comment and Delimiter Handling ---- *) 160 161let is_comment_line config line = 162 let trimmed = lstrip line in 163 List.exists (fun prefix -> string_starts_with ~prefix trimmed) config.comment_prefixes 164 165let is_empty_line line = 166 String.length (strip line) = 0 167 168let strip_inline_comment config value = 169 if config.inline_comment_prefixes = [] then value 170 else 171 (* Find inline comment with preceding whitespace *) 172 let len = String.length value in 173 let rec find_comment i = 174 if i >= len then value 175 else if value.[i] = ' ' || value.[i] = '\t' then begin 176 let rest = String.sub value i (len - i) in 177 let trimmed = lstrip rest in 178 if List.exists (fun p -> string_starts_with ~prefix:p trimmed) config.inline_comment_prefixes then 179 rstrip (String.sub value 0 i) 180 else 181 find_comment (i + 1) 182 end 183 else find_comment (i + 1) 184 in 185 find_comment 0 186 187let find_delimiter config line = 188 let trimmed = lstrip line in 189 let len = String.length trimmed in 190 let rec try_delimiters delims = 191 match delims with 192 | [] -> None 193 | delim :: rest -> 194 let dlen = String.length delim in 195 let rec find_at i = 196 if i + dlen > len then try_delimiters rest 197 else if String.sub trimmed i dlen = delim then Some (delim, i) 198 else find_at (i + 1) 199 in 200 find_at 0 201 in 202 try_delimiters config.delimiters 203 204(* ---- Section Header Parsing ---- *) 205 206let parse_section_header line = 207 let trimmed = strip line in 208 let len = String.length trimmed in 209 if len >= 2 && trimmed.[0] = '[' && trimmed.[len - 1] = ']' then 210 Some (String.sub trimmed 1 (len - 2)) 211 else 212 None 213 214(* ---- Interpolation ---- *) 215 216let rec basic_interpolate ~section ~defaults ~sections value max_depth = 217 if max_depth <= 0 then 218 Error (Init.Error.make (Init.Error.Interpolation { 219 option = ""; reason = "recursion depth exceeded" })) 220 else 221 let buf = Buffer.create (String.length value) in 222 let len = String.length value in 223 let rec scan i = 224 if i >= len then Ok (Buffer.contents buf) 225 else if i + 1 < len && value.[i] = '%' && value.[i+1] = '%' then begin 226 Buffer.add_char buf '%'; 227 scan (i + 2) 228 end 229 else if value.[i] = '%' && i + 1 < len && value.[i+1] = '(' then begin 230 (* Find closing )s *) 231 let rec find_close j = 232 if j + 1 >= len then None 233 else if value.[j] = ')' && value.[j+1] = 's' then Some j 234 else find_close (j + 1) 235 in 236 match find_close (i + 2) with 237 | None -> 238 Buffer.add_char buf value.[i]; 239 scan (i + 1) 240 | Some close_pos -> 241 let name = String.lowercase_ascii (String.sub value (i + 2) (close_pos - i - 2)) in 242 (* Look up value: current section first, then defaults *) 243 let lookup_result = 244 let find_in_opts opts = 245 List.find_opt (fun ((n, _), _) -> 246 String.lowercase_ascii n = name) opts 247 in 248 match section with 249 | None -> find_in_opts defaults 250 | Some sec -> 251 let sec_opts = List.find_opt (fun s -> 252 String.lowercase_ascii (fst s.Init.Repr.name) = 253 String.lowercase_ascii sec 254 ) sections in 255 match sec_opts with 256 | Some s -> 257 (match find_in_opts s.Init.Repr.options with 258 | Some x -> Some x 259 | None -> find_in_opts defaults) 260 | None -> find_in_opts defaults 261 in 262 match lookup_result with 263 | None -> 264 Error (Init.Error.make (Init.Error.Interpolation { 265 option = name; reason = "option not found" })) 266 | Some (_, iv) -> 267 (* Recursively interpolate the referenced value *) 268 match basic_interpolate ~section ~defaults ~sections iv.Init.Repr.raw (max_depth - 1) with 269 | Error e -> Error e 270 | Ok interpolated -> 271 Buffer.add_string buf interpolated; 272 scan (close_pos + 2) 273 end 274 else begin 275 Buffer.add_char buf value.[i]; 276 scan (i + 1) 277 end 278 in 279 scan 0 280 281let rec extended_interpolate ~section ~defaults ~sections value max_depth = 282 if max_depth <= 0 then 283 Error (Init.Error.make (Init.Error.Interpolation { 284 option = ""; reason = "recursion depth exceeded" })) 285 else 286 let buf = Buffer.create (String.length value) in 287 let len = String.length value in 288 let rec scan i = 289 if i >= len then Ok (Buffer.contents buf) 290 else if i + 1 < len && value.[i] = '$' && value.[i+1] = '$' then begin 291 Buffer.add_char buf '$'; 292 scan (i + 2) 293 end 294 else if value.[i] = '$' && i + 1 < len && value.[i+1] = '{' then begin 295 (* Find closing } *) 296 let rec find_close j = 297 if j >= len then None 298 else if value.[j] = '}' then Some j 299 else find_close (j + 1) 300 in 301 match find_close (i + 2) with 302 | None -> 303 Buffer.add_char buf value.[i]; 304 scan (i + 1) 305 | Some close_pos -> 306 let ref_str = String.sub value (i + 2) (close_pos - i - 2) in 307 (* Parse section:name or just name *) 308 let (ref_section, name) = 309 match String.index_opt ref_str ':' with 310 | None -> (section, String.lowercase_ascii ref_str) 311 | Some colon_pos -> 312 let sec = String.sub ref_str 0 colon_pos in 313 let n = String.sub ref_str (colon_pos + 1) (String.length ref_str - colon_pos - 1) in 314 (Some sec, String.lowercase_ascii n) 315 in 316 (* Look up value *) 317 let lookup_result = 318 let find_in_opts opts = 319 List.find_opt (fun ((n, _), _) -> 320 String.lowercase_ascii n = name) opts 321 in 322 match ref_section with 323 | None -> find_in_opts defaults 324 | Some sec -> 325 let lc_sec = String.lowercase_ascii sec in 326 if lc_sec = String.lowercase_ascii "default" then 327 find_in_opts defaults 328 else 329 let sec_opts = List.find_opt (fun s -> 330 String.lowercase_ascii (fst s.Init.Repr.name) = lc_sec 331 ) sections in 332 match sec_opts with 333 | Some s -> 334 (match find_in_opts s.Init.Repr.options with 335 | Some x -> Some x 336 | None -> find_in_opts defaults) 337 | None -> find_in_opts defaults 338 in 339 match lookup_result with 340 | None -> 341 Error (Init.Error.make (Init.Error.Interpolation { 342 option = name; reason = "option not found" })) 343 | Some (_, iv) -> 344 (* Recursively interpolate *) 345 match extended_interpolate ~section:ref_section ~defaults ~sections iv.Init.Repr.raw (max_depth - 1) with 346 | Error e -> Error e 347 | Ok interpolated -> 348 Buffer.add_string buf interpolated; 349 scan (close_pos + 1) 350 end 351 else begin 352 Buffer.add_char buf value.[i]; 353 scan (i + 1) 354 end 355 in 356 scan 0 357 358let interpolate config ~section ~defaults ~sections value = 359 match config.interpolation with 360 | `No_interpolation -> Ok value 361 | `Basic_interpolation -> basic_interpolate ~section ~defaults ~sections value 10 362 | `Extended_interpolation -> extended_interpolate ~section ~defaults ~sections value 10 363 364(* ---- Option Finalization ---- *) 365 366let finalize_current_option state = 367 match state.cur_option with 368 | None -> () 369 | Some (name, name_meta) -> 370 let raw_value = String.concat "\n" (List.rev state.cur_value) in 371 let value = strip raw_value in 372 let iv = { 373 Init.Repr.raw = value; 374 interpolated = value; (* Will be interpolated later *) 375 meta = state.cur_value_meta; 376 } in 377 let opt = ((name, name_meta), iv) in 378 (match state.cur_section with 379 | None -> 380 (* DEFAULT section *) 381 state.defaults <- opt :: state.defaults 382 | Some sec -> 383 (* Add to current section *) 384 match state.sections with 385 | [] -> 386 let new_sec = { 387 Init.Repr.name = (sec, Init.Meta.none); 388 options = [opt]; 389 meta = Init.Meta.none; 390 } in 391 state.sections <- [new_sec] 392 | sec_data :: rest when fst sec_data.name = sec -> 393 state.sections <- { sec_data with options = opt :: sec_data.options } :: rest 394 | _ -> 395 let new_sec = { 396 Init.Repr.name = (sec, Init.Meta.none); 397 options = [opt]; 398 meta = Init.Meta.none; 399 } in 400 state.sections <- new_sec :: state.sections); 401 state.cur_option <- None; 402 state.cur_value <- []; 403 state.cur_indent <- 0 404 405(* ---- Line Processing ---- *) 406 407let process_line state line = 408 let line_start = state.byte_pos in 409 let line_start_line = (state.line_num, state.line_start_byte) in 410 state.byte_pos <- state.byte_pos + String.length line + 1; (* +1 for newline *) 411 state.line_num <- state.line_num + 1; 412 state.line_start_byte <- state.byte_pos; 413 414 (* Check for empty line *) 415 if is_empty_line line then begin 416 if state.cur_option <> None && state.config.empty_lines_in_values then 417 state.cur_value <- "" :: state.cur_value 418 else begin 419 finalize_current_option state; 420 state.pending_ws <- state.pending_ws ^ line ^ "\n" 421 end; 422 Ok () 423 end 424 (* Check for comment *) 425 else if is_comment_line state.config line then begin 426 if state.cur_option <> None then 427 (* Comment within multiline - finalize. *) 428 finalize_current_option state; 429 state.pending_ws <- state.pending_ws ^ line ^ "\n"; 430 Ok () 431 end 432 (* Check for section header *) 433 else match parse_section_header line with 434 | Some sec_name -> 435 finalize_current_option state; 436 let lc_sec = sec_name in (* Keep original case for section names *) 437 if String.lowercase_ascii sec_name = String.lowercase_ascii state.config.default_section then begin 438 state.cur_section <- None; 439 Ok () 440 end 441 else if state.config.strict then begin 442 (* Check for duplicate section *) 443 let exists = List.exists (fun s -> 444 String.lowercase_ascii (fst s.Init.Repr.name) = String.lowercase_ascii sec_name 445 ) state.sections in 446 if exists then 447 Error (Init.Error.make 448 ~meta:(current_meta state line_start line_start_line) 449 (Init.Error.Duplicate_section sec_name)) 450 else begin 451 let sec_meta = current_meta state line_start line_start_line in 452 let sec_meta = Init.Meta.with_ws_before sec_meta state.pending_ws in 453 state.pending_ws <- ""; 454 let new_sec = { 455 Init.Repr.name = (lc_sec, sec_meta); 456 options = []; 457 meta = sec_meta; 458 } in 459 state.sections <- new_sec :: state.sections; 460 state.cur_section <- Some lc_sec; 461 Ok () 462 end 463 end 464 else begin 465 let sec_meta = current_meta state line_start line_start_line in 466 let sec_meta = Init.Meta.with_ws_before sec_meta state.pending_ws in 467 state.pending_ws <- ""; 468 let new_sec = { 469 Init.Repr.name = (lc_sec, sec_meta); 470 options = []; 471 meta = sec_meta; 472 } in 473 state.sections <- new_sec :: state.sections; 474 state.cur_section <- Some lc_sec; 475 Ok () 476 end 477 | None -> 478 (* Check for continuation of multiline value *) 479 let indent = count_indent line in 480 if state.cur_option <> None && indent > state.cur_indent then begin 481 (* Continuation line *) 482 let value_part = strip line in 483 state.cur_value <- value_part :: state.cur_value; 484 Ok () 485 end 486 else begin 487 (* New option or continuation *) 488 finalize_current_option state; 489 (* Try to parse as option = value *) 490 match find_delimiter state.config line with 491 | Some (delim, pos) -> 492 let stripped = lstrip line in 493 let key = String.sub stripped 0 pos in 494 let key = String.lowercase_ascii (rstrip key) in (* Case-fold option names *) 495 let value_start = pos + String.length delim in 496 let rest = String.sub stripped value_start (String.length stripped - value_start) in 497 let value = strip_inline_comment state.config (lstrip rest) in 498 if state.cur_section = None && state.sections = [] && state.defaults = [] then 499 (* No section header yet - this is DEFAULT section *) 500 (); 501 let opt_meta = current_meta state line_start line_start_line in 502 let opt_meta = Init.Meta.with_ws_before opt_meta state.pending_ws in 503 state.pending_ws <- ""; 504 state.cur_option <- Some (key, opt_meta); 505 state.cur_value <- [value]; 506 state.cur_indent <- count_indent line; 507 state.cur_value_meta <- opt_meta; 508 Ok () 509 | None -> 510 if state.config.allow_no_value then begin 511 (* Valueless option *) 512 let key = String.lowercase_ascii (strip line) in 513 let opt_meta = current_meta state line_start line_start_line in 514 let opt_meta = Init.Meta.with_ws_before opt_meta state.pending_ws in 515 state.pending_ws <- ""; 516 state.cur_option <- Some (key, opt_meta); 517 state.cur_value <- []; 518 state.cur_indent <- count_indent line; 519 state.cur_value_meta <- opt_meta; 520 Ok () 521 end 522 else 523 Error (Init.Error.make 524 ~meta:(current_meta state line_start line_start_line) 525 (Init.Error.Parse ("no delimiter found in line: " ^ line))) 526 end 527 528(* ---- Interpolation Pass ---- *) 529 530let perform_interpolation state = 531 let open Result_syntax in 532 let interpolate_value ~section iv = 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 }) 535 in 536 let interpolate_opts ~section opts = 537 let rec loop acc = function 538 | [] -> Ok (List.rev acc) 539 | ((name, meta), iv) :: rest -> 540 let* iv' = interpolate_value ~section iv in 541 loop (((name, meta), iv') :: acc) rest 542 in 543 loop [] opts 544 in 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 () 556 557(* ---- Line splitting ---- *) 558 559let split_lines s = 560 let len = String.length s in 561 if len = 0 then [] 562 else 563 let rec split acc start i = 564 if i >= len then 565 let last = String.sub s start (len - start) in 566 List.rev (if String.length last > 0 then last :: acc else acc) 567 else match s.[i] with 568 | '\n' -> 569 let line = String.sub s start (i - start) in 570 split (line :: acc) (i + 1) (i + 1) 571 | '\r' -> 572 let line = String.sub s start (i - start) in 573 let next = if i + 1 < len && s.[i + 1] = '\n' then i + 2 else i + 1 in 574 split (line :: acc) next next 575 | _ -> split acc start (i + 1) 576 in 577 split [] 0 0 578 579(* ---- Main Parse Functions ---- *) 580 581let parse_string_internal ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) s = 582 let open Result_syntax in 583 let _ = locs in (* TODO: Use locs to control location tracking *) 584 let _ = layout in (* TODO: Use layout to control whitespace preservation *) 585 let state = make_state config file in 586 let lines = split_lines s in 587 let rec process = function 588 | [] -> 589 finalize_current_option state; 590 Ok () 591 | line :: rest -> 592 let* () = process_line state line in 593 process rest 594 in 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 } 604 605let parse_reader ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) reader = 606 let s = read_all_to_string reader in 607 parse_string_internal ~config ~locs ~layout ~file s 608 609let parse_string ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) s = 610 parse_string_internal ~config ~locs ~layout ~file s 611 612(* ---- Decoding ---- *) 613 614let 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"))) 624 | None -> 625 Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 626 627let 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 631 632let decode ?config ?locs ?layout ?file codec reader = 633 decode' ?config ?locs ?layout ?file codec reader 634 |> Result.map_error Init.Error.to_string 635 636let decode_string' ?(config=default_config) ?(locs=false) ?(layout=false) ?(file=Init.Textloc.file_none) codec s = 637 let open Result_syntax in 638 let* doc = parse_string ~config ~locs ~layout ~file s in 639 decode_doc codec doc 640 641let decode_string ?config ?locs ?layout ?file codec s = 642 decode_string' ?config ?locs ?layout ?file codec s 643 |> Result.map_error Init.Error.to_string 644 645(* ---- Encoding ---- *) 646 647let encode_to_buffer buf codec value = 648 match Init.document_state codec with 649 | Some doc_state -> 650 let doc = doc_state.encode value in 651 (* Encode defaults *) 652 if doc.defaults <> [] then begin 653 Buffer.add_string buf "[DEFAULT]\n"; 654 List.iter (fun ((name, _), iv) -> 655 Buffer.add_string buf name; 656 Buffer.add_string buf " = "; 657 Buffer.add_string buf iv.Init.Repr.raw; 658 Buffer.add_char buf '\n' 659 ) doc.defaults; 660 Buffer.add_char buf '\n' 661 end; 662 (* Encode sections *) 663 List.iter (fun (sec : Init.Repr.ini_section) -> 664 Buffer.add_char buf '['; 665 Buffer.add_string buf (fst sec.name); 666 Buffer.add_string buf "]\n"; 667 List.iter (fun ((name, _), iv) -> 668 Buffer.add_string buf name; 669 Buffer.add_string buf " = "; 670 Buffer.add_string buf iv.Init.Repr.raw; 671 Buffer.add_char buf '\n' 672 ) sec.options; 673 Buffer.add_char buf '\n' 674 ) doc.sections; 675 Ok () 676 | None -> 677 match Init.section_state codec with 678 | Some sec_state -> 679 let sec = sec_state.encode value in 680 Buffer.add_char buf '['; 681 Buffer.add_string buf (fst sec.name); 682 Buffer.add_string buf "]\n"; 683 List.iter (fun ((name, _), iv) -> 684 Buffer.add_string buf name; 685 Buffer.add_string buf " = "; 686 Buffer.add_string buf iv.Init.Repr.raw; 687 Buffer.add_char buf '\n' 688 ) sec.options; 689 Ok () 690 | None -> 691 Error (Init.Error.make (Init.Error.Codec "codec is neither document nor section type")) 692 693let encode' ?buf:_ codec value ~eod writer = 694 let open Result_syntax in 695 let buffer = Buffer.create 1024 in 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 () 700 701let encode ?buf codec value ~eod writer = 702 encode' ?buf codec value ~eod writer 703 |> Result.map_error Init.Error.to_string 704 705let encode_string' ?buf:_ codec value = 706 let buffer = Buffer.create 1024 in 707 encode_to_buffer buffer codec value 708 |> Result.map (fun () -> Buffer.contents buffer) 709 710let encode_string ?buf codec value = 711 encode_string' ?buf codec value 712 |> Result.map_error Init.Error.to_string