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(** JSON-compatible YAML value representation *)
7
8type t = [
9 | `Null
10 | `Bool of bool
11 | `Float of float
12 | `String of string
13 | `A of t list
14 | `O of (string * t) list
15]
16
17(* Type equality is ensured by structural compatibility with Yamlrw.value *)
18
19(** Constructors *)
20
21let null : t = `Null
22let bool b : t = `Bool b
23let int n : t = `Float (Float.of_int n)
24let float f : t = `Float f
25let string s : t = `String s
26
27let list f xs : t = `A (List.map f xs)
28let obj pairs : t = `O pairs
29
30(** Type name for error messages *)
31let type_name : t -> string = function
32 | `Null -> "null"
33 | `Bool _ -> "bool"
34 | `Float _ -> "float"
35 | `String _ -> "string"
36 | `A _ -> "array"
37 | `O _ -> "object"
38
39(** Safe accessors (return option) *)
40
41let as_null = function `Null -> Some () | _ -> None
42let as_bool = function `Bool b -> Some b | _ -> None
43let as_float = function `Float f -> Some f | _ -> None
44let as_string = function `String s -> Some s | _ -> None
45let as_list = function `A l -> Some l | _ -> None
46let as_assoc = function `O o -> Some o | _ -> None
47
48let as_int = function
49 | `Float f ->
50 let i = Float.to_int f in
51 if Float.equal (Float.of_int i) f then Some i else None
52 | _ -> None
53
54(** Unsafe accessors (raise on type mismatch) *)
55
56let unwrap_or_type_error expected_type extractor v =
57 match extractor v with
58 | Some x -> x
59 | None -> Error.raise (Type_mismatch (expected_type, type_name v))
60
61let to_null v = unwrap_or_type_error "null" as_null v
62let to_bool v = unwrap_or_type_error "bool" as_bool v
63let to_float v = unwrap_or_type_error "float" as_float v
64let to_string v = unwrap_or_type_error "string" as_string v
65let to_list v = unwrap_or_type_error "array" as_list v
66let to_assoc v = unwrap_or_type_error "object" as_assoc v
67let to_int v = unwrap_or_type_error "int" as_int v
68
69(** Object access *)
70
71let mem key = function
72 | `O pairs -> List.exists (fun (k, _) -> k = key) pairs
73 | _ -> false
74
75let find key = function
76 | `O pairs -> List.assoc_opt key pairs
77 | _ -> None
78
79let get key v =
80 match find key v with
81 | Some v -> v
82 | None -> Error.raise (Key_not_found key)
83
84let keys = function
85 | `O pairs -> List.map fst pairs
86 | v -> Error.raise (Type_mismatch ("object", type_name v))
87
88let values = function
89 | `O pairs -> List.map snd pairs
90 | v -> Error.raise (Type_mismatch ("object", type_name v))
91
92(** Combinators *)
93
94let combine v1 v2 =
95 match v1, v2 with
96 | `O o1, `O o2 -> `O (o1 @ o2)
97 | v1, _ -> Error.raise (Type_mismatch ("object", type_name v1))
98
99let map f = function
100 | `A l -> `A (List.map f l)
101 | v -> Error.raise (Type_mismatch ("array", type_name v))
102
103let filter pred = function
104 | `A l -> `A (List.filter pred l)
105 | v -> Error.raise (Type_mismatch ("array", type_name v))
106
107(** Pretty printing *)
108
109let rec pp fmt (v : t) =
110 match v with
111 | `Null -> Format.pp_print_string fmt "null"
112 | `Bool b -> Format.pp_print_bool fmt b
113 | `Float f ->
114 if Float.is_integer f && Float.abs f < 1e15 then
115 Format.fprintf fmt "%.0f" f
116 else
117 Format.fprintf fmt "%g" f
118 | `String s -> Format.fprintf fmt "%S" s
119 | `A [] -> Format.pp_print_string fmt "[]"
120 | `A items ->
121 Format.fprintf fmt "@[<hv 2>[@,%a@]@,]"
122 (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ") pp)
123 items
124 | `O [] -> Format.pp_print_string fmt "{}"
125 | `O pairs ->
126 Format.fprintf fmt "@[<hv 2>{@,%a@]@,}"
127 (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ",@ ")
128 (fun fmt (k, v) -> Format.fprintf fmt "@[<hv 2>%S:@ %a@]" k pp v))
129 pairs
130
131(** Equality and comparison *)
132
133let rec equal (a : t) (b : t) =
134 match a, b with
135 | `Null, `Null -> true
136 | `Bool a, `Bool b -> a = b
137 | `Float a, `Float b -> Float.equal a b
138 | `String a, `String b -> String.equal a b
139 | `A a, `A b -> List.equal equal a b
140 | `O a, `O b ->
141 List.length a = List.length b &&
142 List.for_all2 (fun (k1, v1) (k2, v2) -> k1 = k2 && equal v1 v2) a b
143 | _ -> false
144
145let rec compare (a : t) (b : t) =
146 match a, b with
147 | `Null, `Null -> 0
148 | `Null, _ -> -1
149 | _, `Null -> 1
150 | `Bool a, `Bool b -> Bool.compare a b
151 | `Bool _, _ -> -1
152 | _, `Bool _ -> 1
153 | `Float a, `Float b -> Float.compare a b
154 | `Float _, _ -> -1
155 | _, `Float _ -> 1
156 | `String a, `String b -> String.compare a b
157 | `String _, _ -> -1
158 | _, `String _ -> 1
159 | `A a, `A b -> List.compare compare a b
160 | `A _, _ -> -1
161 | _, `A _ -> 1
162 | `O a, `O b ->
163 let cmp_pair (k1, v1) (k2, v2) =
164 let c = String.compare k1 k2 in
165 if c <> 0 then c else compare v1 v2
166 in
167 List.compare cmp_pair a b