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
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