Pure OCaml Yaml 1.2 reader and writer using Bytesrw
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** Serialize - high-level serialization to buffers and event streams
7
8 This module provides functions to convert YAML values to events and strings.
9 Both {!Emitter.t}-based and function-based emission APIs are provided. *)
10
11(** {1 Internal Helpers} *)
12
13(** Emit a YAML node using an emit function.
14 This is the core implementation used by both Emitter.t and function-based APIs. *)
15let rec emit_yaml_node_impl ~emit (yaml : Yaml.t) =
16 match yaml with
17 | `Scalar s ->
18 emit (Event.Scalar {
19 anchor = Scalar.anchor s;
20 tag = Scalar.tag s;
21 value = Scalar.value s;
22 plain_implicit = Scalar.plain_implicit s;
23 quoted_implicit = Scalar.quoted_implicit s;
24 style = Scalar.style s;
25 })
26
27 | `Alias name ->
28 emit (Event.Alias { anchor = name })
29
30 | `A seq ->
31 let members = Sequence.members seq in
32 let style =
33 (* Force flow style for empty sequences *)
34 if members = [] then `Flow
35 else Sequence.style seq
36 in
37 emit (Event.Sequence_start {
38 anchor = Sequence.anchor seq;
39 tag = Sequence.tag seq;
40 implicit = Sequence.implicit seq;
41 style;
42 });
43 List.iter (emit_yaml_node_impl ~emit) members;
44 emit Event.Sequence_end
45
46 | `O map ->
47 let members = Mapping.members map in
48 let style =
49 (* Force flow style for empty mappings *)
50 if members = [] then `Flow
51 else Mapping.style map
52 in
53 emit (Event.Mapping_start {
54 anchor = Mapping.anchor map;
55 tag = Mapping.tag map;
56 implicit = Mapping.implicit map;
57 style;
58 });
59 List.iter (fun (k, v) ->
60 emit_yaml_node_impl ~emit k;
61 emit_yaml_node_impl ~emit v
62 ) members;
63 emit Event.Mapping_end
64
65(** Emit a Value node using an emit function.
66 This is the core implementation used by both Emitter.t and function-based APIs. *)
67let rec emit_value_node_impl ~emit ~config (value : Value.t) =
68 match value with
69 | `Null ->
70 emit (Event.Scalar {
71 anchor = None; tag = None;
72 value = "null";
73 plain_implicit = true; quoted_implicit = false;
74 style = `Plain;
75 })
76
77 | `Bool b ->
78 emit (Event.Scalar {
79 anchor = None; tag = None;
80 value = if b then "true" else "false";
81 plain_implicit = true; quoted_implicit = false;
82 style = `Plain;
83 })
84
85 | `Float f ->
86 let value =
87 match Float.classify_float f with
88 | FP_nan -> ".nan"
89 | FP_infinite -> if f > 0.0 then ".inf" else "-.inf"
90 | _ ->
91 if Float.is_integer f && Float.abs f < 1e15 then
92 Printf.sprintf "%.0f" f
93 else
94 Printf.sprintf "%g" f
95 in
96 emit (Event.Scalar {
97 anchor = None; tag = None;
98 value;
99 plain_implicit = true; quoted_implicit = false;
100 style = `Plain;
101 })
102
103 | `String s ->
104 let style = Quoting.choose_style s in
105 emit (Event.Scalar {
106 anchor = None; tag = None;
107 value = s;
108 plain_implicit = style = `Plain;
109 quoted_implicit = style <> `Plain;
110 style;
111 })
112
113 | `A items ->
114 let style =
115 (* Force flow style for empty sequences *)
116 if items = [] then `Flow
117 else if config.Emitter.layout_style = `Flow then `Flow
118 else `Block
119 in
120 emit (Event.Sequence_start {
121 anchor = None; tag = None;
122 implicit = true;
123 style;
124 });
125 List.iter (emit_value_node_impl ~emit ~config) items;
126 emit Event.Sequence_end
127
128 | `O pairs ->
129 let style =
130 (* Force flow style for empty mappings *)
131 if pairs = [] then `Flow
132 else if config.Emitter.layout_style = `Flow then `Flow
133 else `Block
134 in
135 emit (Event.Mapping_start {
136 anchor = None; tag = None;
137 implicit = true;
138 style;
139 });
140 List.iter (fun (k, v) ->
141 let style = Quoting.choose_style k in
142 emit (Event.Scalar {
143 anchor = None; tag = None;
144 value = k;
145 plain_implicit = style = `Plain;
146 quoted_implicit = style <> `Plain;
147 style;
148 });
149 emit_value_node_impl ~emit ~config v
150 ) pairs;
151 emit Event.Mapping_end
152
153(** Strip anchors from a YAML tree (used when resolving aliases for output) *)
154let rec strip_anchors (yaml : Yaml.t) : Yaml.t =
155 match yaml with
156 | `Scalar s ->
157 if Option.is_none (Scalar.anchor s) then yaml
158 else
159 `Scalar (Scalar.make
160 ?tag:(Scalar.tag s)
161 ~plain_implicit:(Scalar.plain_implicit s)
162 ~quoted_implicit:(Scalar.quoted_implicit s)
163 ~style:(Scalar.style s)
164 (Scalar.value s))
165 | `Alias _ -> yaml
166 | `A seq ->
167 `A (Sequence.make
168 ?tag:(Sequence.tag seq)
169 ~implicit:(Sequence.implicit seq)
170 ~style:(Sequence.style seq)
171 (List.map strip_anchors (Sequence.members seq)))
172 | `O map ->
173 `O (Mapping.make
174 ?tag:(Mapping.tag map)
175 ~implicit:(Mapping.implicit map)
176 ~style:(Mapping.style map)
177 (List.map (fun (k, v) -> (strip_anchors k, strip_anchors v)) (Mapping.members map)))
178
179(** Emit a document using an emit function *)
180let emit_document_impl ?(resolve_aliases = true) ~emit doc =
181 emit (Event.Document_start {
182 version = Document.version doc;
183 implicit = Document.implicit_start doc;
184 });
185 (match Document.root doc with
186 | Some yaml ->
187 let yaml = if resolve_aliases then
188 yaml |> Yaml.resolve_aliases |> strip_anchors
189 else yaml in
190 emit_yaml_node_impl ~emit yaml
191 | None ->
192 emit (Event.Scalar {
193 anchor = None; tag = None;
194 value = "";
195 plain_implicit = true; quoted_implicit = false;
196 style = `Plain;
197 }));
198 emit (Event.Document_end { implicit = Document.implicit_end doc })
199
200(** {1 Emitter.t-based API} *)
201
202(** Emit a YAML node to an emitter *)
203let emit_yaml_node t yaml =
204 emit_yaml_node_impl ~emit:(Emitter.emit t) yaml
205
206(** Emit a complete YAML document to an emitter *)
207let emit_yaml t yaml =
208 let config = Emitter.config t in
209 Emitter.emit t (Event.Stream_start { encoding = config.encoding });
210 Emitter.emit t (Event.Document_start { version = None; implicit = true });
211 emit_yaml_node t yaml;
212 Emitter.emit t (Event.Document_end { implicit = true });
213 Emitter.emit t Event.Stream_end
214
215(** Emit a Value node to an emitter *)
216let emit_value_node t value =
217 let config = Emitter.config t in
218 emit_value_node_impl ~emit:(Emitter.emit t) ~config value
219
220(** Emit a complete Value document to an emitter *)
221let emit_value t value =
222 let config = Emitter.config t in
223 Emitter.emit t (Event.Stream_start { encoding = config.encoding });
224 Emitter.emit t (Event.Document_start { version = None; implicit = true });
225 emit_value_node t value;
226 Emitter.emit t (Event.Document_end { implicit = true });
227 Emitter.emit t Event.Stream_end
228
229(** Emit a document to an emitter *)
230let emit_document ?resolve_aliases t doc =
231 emit_document_impl ?resolve_aliases ~emit:(Emitter.emit t) doc
232
233(** {1 Buffer-based API} *)
234
235(** Serialize a Value to a buffer.
236
237 @param config Emitter configuration (default: {!Emitter.default_config})
238 @param buffer Optional buffer to append to; creates new one if not provided
239 @return The buffer containing serialized YAML *)
240let value_to_buffer ?(config = Emitter.default_config) ?buffer value =
241 let buf = Option.value buffer ~default:(Buffer.create 1024) in
242 let t = Emitter.create ~config () in
243 emit_value t value;
244 Buffer.add_string buf (Emitter.contents t);
245 buf
246
247(** Serialize a Yaml.t to a buffer.
248
249 @param config Emitter configuration (default: {!Emitter.default_config})
250 @param buffer Optional buffer to append to; creates new one if not provided
251 @return The buffer containing serialized YAML *)
252let yaml_to_buffer ?(config = Emitter.default_config) ?buffer yaml =
253 let buf = Option.value buffer ~default:(Buffer.create 1024) in
254 let t = Emitter.create ~config () in
255 emit_yaml t yaml;
256 Buffer.add_string buf (Emitter.contents t);
257 buf
258
259(** Serialize documents to a buffer.
260
261 @param config Emitter configuration (default: {!Emitter.default_config})
262 @param resolve_aliases Whether to resolve aliases before emission (default: true)
263 @param buffer Optional buffer to append to; creates new one if not provided
264 @return The buffer containing serialized YAML *)
265let documents_to_buffer ?(config = Emitter.default_config) ?(resolve_aliases = true) ?buffer documents =
266 let buf = Option.value buffer ~default:(Buffer.create 1024) in
267 let t = Emitter.create ~config () in
268 Emitter.emit t (Event.Stream_start { encoding = config.encoding });
269 List.iter (emit_document ~resolve_aliases t) documents;
270 Emitter.emit t Event.Stream_end;
271 Buffer.add_string buf (Emitter.contents t);
272 buf
273
274(** {1 String-based API} *)
275
276(** Serialize a Value to a string.
277
278 @param config Emitter configuration (default: {!Emitter.default_config}) *)
279let value_to_string ?(config = Emitter.default_config) value =
280 Buffer.contents (value_to_buffer ~config value)
281
282(** Serialize a Yaml.t to a string.
283
284 @param config Emitter configuration (default: {!Emitter.default_config}) *)
285let yaml_to_string ?(config = Emitter.default_config) yaml =
286 Buffer.contents (yaml_to_buffer ~config yaml)
287
288(** Serialize documents to a string.
289
290 @param config Emitter configuration (default: {!Emitter.default_config})
291 @param resolve_aliases Whether to resolve aliases before emission (default: true) *)
292let documents_to_string ?(config = Emitter.default_config) ?(resolve_aliases = true) documents =
293 Buffer.contents (documents_to_buffer ~config ~resolve_aliases documents)
294
295(** {1 Writer-based API}
296
297 These functions write directly to a bytesrw [Bytes.Writer.t],
298 enabling true streaming output without intermediate string allocation.
299 Uses the emitter's native Writer support for efficiency. *)
300
301(** Serialize a Value directly to a Bytes.Writer.
302
303 @param config Emitter configuration (default: {!Emitter.default_config})
304 @param eod Whether to write end-of-data after serialization (default: true) *)
305let value_to_writer ?(config = Emitter.default_config) ?(eod = true) writer value =
306 let t = Emitter.of_writer ~config writer in
307 emit_value t value;
308 if eod then Emitter.flush t
309
310(** Serialize a Yaml.t directly to a Bytes.Writer.
311
312 @param config Emitter configuration (default: {!Emitter.default_config})
313 @param eod Whether to write end-of-data after serialization (default: true) *)
314let yaml_to_writer ?(config = Emitter.default_config) ?(eod = true) writer yaml =
315 let t = Emitter.of_writer ~config writer in
316 emit_yaml t yaml;
317 if eod then Emitter.flush t
318
319(** Serialize documents directly to a Bytes.Writer.
320
321 @param config Emitter configuration (default: {!Emitter.default_config})
322 @param resolve_aliases Whether to resolve aliases before emission (default: true)
323 @param eod Whether to write end-of-data after serialization (default: true) *)
324let documents_to_writer ?(config = Emitter.default_config) ?(resolve_aliases = true) ?(eod = true) writer documents =
325 let t = Emitter.of_writer ~config writer in
326 Emitter.emit t (Event.Stream_start { encoding = config.encoding });
327 List.iter (emit_document ~resolve_aliases t) documents;
328 Emitter.emit t Event.Stream_end;
329 if eod then Emitter.flush t
330
331(** {1 Function-based API}
332
333 These functions accept an emit function [Event.t -> unit] instead of
334 an {!Emitter.t}, allowing them to work with any event sink
335 (e.g., streaming writers, custom processors). *)
336
337(** Emit a YAML node using an emitter function *)
338let emit_yaml_node_fn ~emitter yaml =
339 emit_yaml_node_impl ~emit:emitter yaml
340
341(** Emit a complete YAML stream using an emitter function *)
342let emit_yaml ~emitter ~config yaml =
343 emitter (Event.Stream_start { encoding = config.Emitter.encoding });
344 emitter (Event.Document_start { version = None; implicit = true });
345 emit_yaml_node_fn ~emitter yaml;
346 emitter (Event.Document_end { implicit = true });
347 emitter Event.Stream_end
348
349(** Emit a Value node using an emitter function *)
350let emit_value_node_fn ~emitter ~config value =
351 emit_value_node_impl ~emit:emitter ~config value
352
353(** Emit a complete Value stream using an emitter function *)
354let emit_value ~emitter ~config value =
355 emitter (Event.Stream_start { encoding = config.Emitter.encoding });
356 emitter (Event.Document_start { version = None; implicit = true });
357 emit_value_node_fn ~emitter ~config value;
358 emitter (Event.Document_end { implicit = true });
359 emitter Event.Stream_end
360
361(** Emit a document using an emitter function *)
362let emit_document_fn ?resolve_aliases ~emitter doc =
363 emit_document_impl ?resolve_aliases ~emit:emitter doc
364
365(** Emit multiple documents using an emitter function *)
366let emit_documents ~emitter ~config ?(resolve_aliases = true) documents =
367 emitter (Event.Stream_start { encoding = config.Emitter.encoding });
368 List.iter (emit_document_fn ~resolve_aliases ~emitter) documents;
369 emitter Event.Stream_end