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