Yaml encoder/decoder for OCaml jsont codecs
at main 8.7 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** YAML codec using Jsont type descriptions. 7 8 This module provides YAML streaming encode/decode that interprets {!Jsont.t} 9 type descriptions, allowing the same codec definitions to work for both JSON 10 and YAML. 11 12 {b Example:} 13 {[ 14 (* Define a codec once using Jsont *) 15 module Config = struct 16 type t = { name : string; port : int } 17 18 let make name port = { name; port } 19 20 let jsont = 21 Jsont.Object.map ~kind:"Config" make 22 |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name) 23 |> Jsont.Object.mem "port" Jsont.int ~enc:(fun c -> c.port) 24 |> Jsont.Object.finish 25 end 26 27 (* Use the same codec for both JSON and YAML *) 28 let from_json = Jsont_bytesrw.decode_string Config.jsont json_str 29 let from_yaml = Yamlt.decode_string Config.jsont yaml_str 30 ]} 31 32 See notes about {{!yaml_mapping}YAML to JSON mapping}, 33 {{!yaml_scalars}YAML scalar resolution}, and 34 {{!null_handling}null value handling}. *) 35 36open Bytesrw 37 38(** {1:decode Decode} *) 39 40val decode : 41 ?layout:bool -> 42 ?locs:bool -> 43 ?file:Jsont.Textloc.fpath -> 44 ?max_depth:int -> 45 ?max_nodes:int -> 46 'a Jsont.t -> 47 Bytes.Reader.t -> 48 ('a, string) result 49(** [decode t r] decodes a value from YAML reader [r] according to type [t]. 50 - If [layout] is [true], style information is preserved in {!Jsont.Meta.t} 51 values (for potential round-tripping). Defaults to [false]. 52 - If [locs] is [true], source locations are preserved in {!Jsont.Meta.t} 53 values and error messages are precisely located. Defaults to [false]. 54 - [file] is the file path for error messages. Defaults to 55 {!Jsont.Textloc.file_none}. 56 - [max_depth] limits nesting depth to prevent stack overflow (billion laughs 57 protection). Defaults to [100]. 58 - [max_nodes] limits total decoded nodes (billion laughs protection). 59 Defaults to [10_000_000]. 60 61 The YAML input must contain exactly one document. Multi-document streams are 62 not supported; use {!decode_all} for those. *) 63 64val decode' : 65 ?layout:bool -> 66 ?locs:bool -> 67 ?file:Jsont.Textloc.fpath -> 68 ?max_depth:int -> 69 ?max_nodes:int -> 70 'a Jsont.t -> 71 Bytes.Reader.t -> 72 ('a, Jsont.Error.t) result 73(** [decode'] is like {!val-decode} but preserves the error structure. *) 74 75val decode_all : 76 ?layout:bool -> 77 ?locs:bool -> 78 ?file:Jsont.Textloc.fpath -> 79 ?max_depth:int -> 80 ?max_nodes:int -> 81 'a Jsont.t -> 82 Bytes.Reader.t -> 83 ('a, string) result Seq.t 84(** [decode_all t r] decodes all documents from a multi-document YAML stream. 85 Returns a sequence where each element is a result of decoding one document. 86 Parameters are as in {!val-decode}. Use this for YAML streams containing 87 multiple documents separated by [---]. *) 88 89val decode_all' : 90 ?layout:bool -> 91 ?locs:bool -> 92 ?file:Jsont.Textloc.fpath -> 93 ?max_depth:int -> 94 ?max_nodes:int -> 95 'a Jsont.t -> 96 Bytes.Reader.t -> 97 ('a, Jsont.Error.t) result Seq.t 98(** [decode_all'] is like {!val-decode_all} but preserves the error structure. *) 99 100(** {1:encode Encode} *) 101 102(** YAML output format. *) 103type yaml_format = 104 | Block (** Block style (indented) - default. Clean, readable YAML. *) 105 | Flow (** Flow style (JSON-like). Compact, single-line collections. *) 106 | Layout (** Preserve layout from {!Jsont.Meta.t} when available. *) 107 108val encode : 109 ?buf:Stdlib.Bytes.t -> 110 ?format:yaml_format -> 111 ?indent:int -> 112 ?explicit_doc:bool -> 113 ?scalar_style:Yamlrw.Scalar_style.t -> 114 'a Jsont.t -> 115 'a -> 116 eod:bool -> 117 Bytes.Writer.t -> 118 (unit, string) result 119(** [encode t v w] encodes value [v] according to type [t] to YAML on [w]. 120 - If [buf] is specified, it is used as a buffer for output slices. Defaults 121 to a buffer of length {!Bytesrw.Bytes.Writer.slice_length}[ w]. 122 - [format] controls the output style. Defaults to {!Block}. 123 - [indent] is the indentation width in spaces. Defaults to [2]. 124 - [explicit_doc] if [true], emits explicit document markers ([---] and 125 [...]). Defaults to [false]. 126 - [scalar_style] is the preferred style for string scalars. Defaults to 127 [`Any] (auto-detect based on content). 128 - [eod] indicates whether {!Bytesrw.Bytes.Slice.eod} should be written on 129 [w] after encoding. *) 130 131val encode' : 132 ?buf:Stdlib.Bytes.t -> 133 ?format:yaml_format -> 134 ?indent:int -> 135 ?explicit_doc:bool -> 136 ?scalar_style:Yamlrw.Scalar_style.t -> 137 'a Jsont.t -> 138 'a -> 139 eod:bool -> 140 Bytes.Writer.t -> 141 (unit, Jsont.Error.t) result 142(** [encode'] is like {!val-encode} but preserves the error structure. *) 143 144(** {1:recode Recode} 145 146 The defaults in these functions are those of {!val-decode} and 147 {!val-encode}, except if [layout] is [true], [format] defaults to {!Layout} 148 and vice-versa. *) 149 150val recode : 151 ?layout:bool -> 152 ?locs:bool -> 153 ?file:Jsont.Textloc.fpath -> 154 ?max_depth:int -> 155 ?max_nodes:int -> 156 ?buf:Stdlib.Bytes.t -> 157 ?format:yaml_format -> 158 ?indent:int -> 159 ?explicit_doc:bool -> 160 ?scalar_style:Yamlrw.Scalar_style.t -> 161 'a Jsont.t -> 162 Bytes.Reader.t -> 163 Bytes.Writer.t -> 164 eod:bool -> 165 (unit, string) result 166(** [recode t r w] is {!val-decode} followed by {!val-encode}. *) 167 168(** {1:yaml_mapping YAML to JSON Mapping} 169 170 YAML is a superset of JSON. This module maps YAML structures to the JSON 171 data model that {!Jsont.t} describes: 172 173 - YAML scalars map to JSON null, boolean, number, or string depending on 174 content and the expected type 175 - YAML sequences map to JSON arrays 176 - YAML mappings map to JSON objects (keys must be strings) 177 - YAML aliases are resolved during decoding 178 - YAML tags are used to guide type resolution when present 179 180 {b Limitations:} 181 - Only string keys are supported in mappings (JSON object compatibility) 182 - Anchors and aliases are resolved; the alias structure is not preserved 183 - Multi-document streams require {!decode_all} *) 184 185(** {1:yaml_scalars YAML Scalar Resolution} 186 187 YAML scalars are resolved to JSON types as follows: 188 189 {b Null:} [null], [Null], [NULL], [~], or empty string 190 191 {b Boolean:} [true], [True], [TRUE], [false], [False], [FALSE], [yes], 192 [Yes], [YES], [no], [No], [NO], [on], [On], [ON], [off], [Off], [OFF] 193 194 {b Number:} Decimal integers, floats, hex ([0x...]), octal ([0o...]), 195 infinity ([.inf], [-.inf]), NaN ([.nan]) 196 197 {b String:} Anything else, or explicitly quoted scalars 198 199 When decoding against a specific {!Jsont.t} type, the expected type takes 200 precedence over automatic resolution. For example, decoding ["yes"] against 201 {!Jsont.string} yields the string ["yes"], not [true]. *) 202 203(** {1:null_handling Null Value Handling} 204 205 YAML null values are handled according to the expected type to provide 206 friendly defaults while maintaining type safety: 207 208 {b Collections (Arrays and Objects):} 209 210 Null values decode as empty collections when the codec expects a collection 211 type. This provides convenient defaults for optional collection fields in 212 YAML: 213 {[ 214 # YAML with null collection fields 215 config: 216 items: null # Decodes as [] 217 settings: ~ # Decodes as {} 218 tags: # Missing value = null, decodes as [] 219 ]} 220 221 For arrays, null decodes to an empty array. For objects, null decodes to an 222 object with all fields set to their [dec_absent] defaults. If any required 223 field lacks a default, decoding fails with a missing member error. 224 225 This behavior makes yamlt more forgiving for schemas with many optional 226 collection fields, where writing [field:] (which parses as null) is natural 227 and semantically equivalent to [field: []]. 228 229 {b Numbers:} 230 231 Null values decode to [Float.nan] when the codec expects a number. 232 233 {b Primitive Types (Int, Bool, String):} 234 235 Null values {e fail} when decoding into primitive scalar types ([int], 236 [bool], [string]). Null typically indicates genuinely missing or incorrect 237 data for these types, and silent conversion could clash with a manual 238 setting of the default value (e.g. 0 and [null] for an integer would be 239 indistinguishable). 240 241 To accept null for primitive fields, explicitly use {!Jsont.option}: 242 {[ 243 (* Accepts null, decodes as None *) 244 Jsont.Object.mem "count" (Jsont.option Jsont.int) ~dec_absent:None 245 246 (* Rejects null, requires a number *) 247 Jsont.Object.mem "count" Jsont.int ~dec_absent:0 248 ]} 249 250*)