Yaml encoder/decoder for OCaml jsont codecs

Compare changes

Choose any two refs to compare.

+19 -1
.gitignore
···
-
_build
+
# OCaml build artifacts
+
_build/
+
*.install
+
*.merlin
+
+
# Third-party sources (fetch locally with opam source)
+
third_party/
+
+
# Symlinked dependencies
ocaml-yamlrw
+
+
# Editor and OS files
+
.DS_Store
+
*.swp
+
*~
+
.vscode/
+
.idea/
+
+
# Opam local switch
+
_opam/
+1
.ocamlformat
···
+
version=0.28.1
+53
.tangled/workflows/build.yml
···
+
when:
+
- event: ["push", "pull_request"]
+
branch: ["main"]
+
+
engine: nixery
+
+
dependencies:
+
nixpkgs:
+
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
+
- gcc
+
- ocaml
+
- pkg-config
+
+
steps:
+
- name: opam
+
command: |
+
opam init --disable-sandboxing -a -y
+
- name: repo
+
command: |
+
opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git
+
- name: switch
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only
+
- name: build
+
command: |
+
opam exec -- dune build
+
- name: switch-test
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only --with-test
+
- name: test
+
command: |
+
opam exec -- dune runtest --verbose
+
- name: doc
+
command: |
+
opam install -y odoc
+
opam exec -- dune build @doc
+15
LICENSE.md
···
+
ISC License
+
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
+
Permission to use, copy, modify, and distribute this software for any
+
purpose with or without fee is hereby granted, provided that the above
+
copyright notice and this permission notice appear in all copies.
+
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+62
README.md
···
+
# yamlt - YAML codec using Jsont type descriptions
+
+
Yamlt provides YAML streaming encode/decode that interprets Jsont.t type descriptions, allowing the same codec definitions to work for both JSON and YAML.
+
+
## Key Features
+
+
- Use the same Jsont.t codec for both JSON and YAML formats
+
- Streaming encode/decode with configurable depth and node limits
+
- Support for YAML-specific features (scalars, sequences, mappings)
+
- Billion laughs protection with configurable limits
+
- Multiple output formats (block, flow, layout preservation)
+
+
## Usage
+
+
```ocaml
+
(* Define a codec once using Jsont *)
+
module Config = struct
+
type t = { name: string; port: int }
+
let make name port = { name; port }
+
let jsont =
+
Jsont.Object.map ~kind:"Config" make
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
+
|> Jsont.Object.mem "port" Jsont.int ~enc:(fun c -> c.port)
+
|> Jsont.Object.finish
+
end
+
+
(* Use the same codec for both JSON and YAML *)
+
let from_json = Jsont_bytesrw.decode_string Config.jsont json_str
+
let from_yaml = Yamlt.decode_string Config.jsont yaml_str
+
```
+
+
For encoding:
+
+
```ocaml
+
(* Encode to YAML with different formats *)
+
let config = Config.make "server" 8080
+
+
(* Block style (default) *)
+
let yaml_block = Yamlt.encode_string Config.jsont config
+
+
(* Flow style (JSON-like) *)
+
let yaml_flow = Yamlt.encode_string ~format:Flow Config.jsont config
+
```
+
+
## Installation
+
+
```
+
opam install yamlt
+
```
+
+
## Documentation
+
+
API documentation is available at https://tangled.org/@anil.recoil.org/ocaml-yamlt or via:
+
+
```
+
opam install yamlt
+
odig doc yamlt
+
```
+
+
## License
+
+
ISC
+5
dune
···
+
; Root dune file
+
+
; Ignore third_party directory (for fetched dependency sources)
+
+
(data_only_dirs third_party)
+10 -1
dune-project
···
(lang dune 3.18)
+
(name yamlt)
(generate_opam_files true)
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.org/@anil.recoil.org/ocaml-yamlt")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-yamlt/issues")
+
(maintenance_intent "(latest)")
+
(package
(name yamlt)
(synopsis "YAML codec using Jsont type descriptions")
···
(ocaml (>= 4.14.0))
yamlrw
jsont
-
bytesrw))
+
bytesrw
+
(odoc :with-doc)))
+549 -450
lib/yamlt.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
-
open Bytesrw
open Jsont.Repr
open Yamlrw
···
meta_none : Jsont.Meta.t;
}
-
let make_decoder
-
?(locs = false) ?(layout = false) ?(file = "-")
+
let make_decoder ?(locs = false) ?(layout = false) ?(file = "-")
?(max_depth = 100) ?(max_nodes = 10_000_000) parser =
let meta_none = Jsont.Meta.make (Jsont.Textloc.(set_file none) file) in
-
{ parser; file; locs; _layout = layout; max_depth; max_nodes;
-
node_count = 0; current = None;
-
_anchors = Hashtbl.create 16; meta_none }
+
{
+
parser;
+
file;
+
locs;
+
_layout = layout;
+
max_depth;
+
max_nodes;
+
node_count = 0;
+
current = None;
+
_anchors = Hashtbl.create 16;
+
meta_none;
+
}
(* Decoder helpers *)
+
(* Local helper to reduce Jsont.Error.msgf boilerplate *)
+
let err_msg meta fmt = Jsont.Error.msgf meta fmt
+
let err_msg_none fmt = Jsont.Error.msgf Jsont.Meta.none fmt
+
let check_depth d ~nest =
if nest > d.max_depth then
-
Jsont.Error.msgf Jsont.Meta.none "Maximum nesting depth %d exceeded" d.max_depth
+
err_msg_none "Maximum nesting depth %d exceeded" d.max_depth
let check_nodes d =
d.node_count <- d.node_count + 1;
if d.node_count > d.max_nodes then
-
Jsont.Error.msgf Jsont.Meta.none "Maximum node count %d exceeded" d.max_nodes
+
err_msg_none "Maximum node count %d exceeded" d.max_nodes
let meta_of_span d span =
-
if not d.locs then d.meta_none else
-
let start = span.Span.start and stop = span.Span.stop in
-
let first_byte = start.Position.index in
-
let last_byte = max first_byte (stop.Position.index - 1) in
-
(* line_pos is (line_number, byte_position_of_line_start) *)
-
let first_line = (start.Position.line, start.Position.index - start.Position.column + 1) in
-
let last_line = (stop.Position.line, stop.Position.index - stop.Position.column + 1) in
-
let textloc = Jsont.Textloc.make ~file:d.file
-
~first_byte ~last_byte ~first_line ~last_line in
-
Jsont.Meta.make textloc
+
if not d.locs then d.meta_none
+
else
+
let start = span.Span.start and stop = span.Span.stop in
+
let first_byte = start.Position.index in
+
let last_byte = max first_byte (stop.Position.index - 1) in
+
(* line_pos is (line_number, byte_position_of_line_start) *)
+
let first_line =
+
(start.Position.line, start.Position.index - start.Position.column + 1)
+
in
+
(* Handle case where stop is at the start of a new line (column 1)
+
This happens when the span includes a trailing newline.
+
The last_byte is on the previous line, so we need to calculate
+
the line start position based on last_byte, not stop. *)
+
let last_line =
+
if stop.Position.column = 1 && stop.Position.line > start.Position.line then
+
(* last_byte is on the previous line (stop.line - 1)
+
We need to estimate where that line starts. Since we don't have
+
the full text, we can't calculate it exactly, but we can use:
+
last_byte - (estimated_column - 1)
+
For now, we'll use the same line as start if they're close,
+
or just report it as the previous line. *)
+
let last_line_num = stop.Position.line - 1 in
+
(* Estimate: assume last_byte is somewhere on the previous line.
+
We'll use the byte position minus a reasonable offset.
+
This is approximate but better than wrapping to the next line. *)
+
if last_line_num = start.Position.line then
+
(* Same line as start - use start's line position *)
+
first_line
+
else
+
(* Different line - estimate line start as last_byte minus some offset
+
Since we subtracted 1 from stop.index to get last_byte, and stop.column was 1,
+
last_byte should be the newline character on the previous line.
+
The line likely started much earlier, but we'll estimate conservatively. *)
+
(last_line_num, last_byte)
+
else
+
(stop.Position.line, stop.Position.index - stop.Position.column + 1)
+
in
+
let textloc =
+
Jsont.Textloc.make ~file:d.file ~first_byte ~last_byte ~first_line
+
~last_line
+
in
+
Jsont.Meta.make textloc
let next_event d =
d.current <- Parser.next d.parser;
d.current
let peek_event d =
-
match d.current with
-
| Some _ -> d.current
-
| None -> next_event d
+
match d.current with Some _ -> d.current | None -> next_event d
-
let skip_event d =
-
d.current <- None
+
let skip_event d = d.current <- None
let _expect_event d pred name =
match peek_event d with
-
| Some ev when pred ev.Event.event -> skip_event d; ev
+
| Some ev when pred ev.Event.event ->
+
skip_event d;
+
ev
| Some ev ->
let span = ev.Event.span in
let meta = meta_of_span d span in
-
Jsont.Error.msgf meta "Expected %s but found %a" name Event.pp ev.Event.event
-
| None ->
-
Jsont.Error.msgf Jsont.Meta.none "Expected %s but reached end of stream" name
+
err_msg meta "Expected %s but found %a" name Event.pp ev.Event.event
+
| None -> err_msg_none "Expected %s but reached end of stream" name
(* Error helpers *)
let _err_expected_scalar d ev =
let meta = meta_of_span d ev.Event.span in
-
Jsont.Error.msgf meta "Expected scalar but found %a" Event.pp ev.Event.event
+
err_msg meta "Expected scalar but found %a" Event.pp ev.Event.event
let err_type_mismatch d span t ~fnd =
+
let open Jsont.Repr in
let meta = meta_of_span d span in
-
Jsont.Error.msgf meta "Expected %s but found %s"
-
(Jsont.Repr.kinded_sort t) fnd
+
err_msg meta "Expected %s but found %s" (kinded_sort t) fnd
(* YAML scalar resolution *)
let is_null_scalar s =
-
s = "" || s = "~" ||
-
s = "null" || s = "Null" || s = "NULL"
+
s = "" || s = "~" || s = "null" || s = "Null" || s = "NULL"
let bool_of_scalar_opt s =
match s with
-
| "true" | "True" | "TRUE"
-
| "yes" | "Yes" | "YES"
-
| "on" | "On" | "ON" -> Some true
-
| "false" | "False" | "FALSE"
-
| "no" | "No" | "NO"
-
| "off" | "Off" | "OFF" -> Some false
+
| "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" ->
+
Some true
+
| "false" | "False" | "FALSE" | "no" | "No" | "NO" | "off" | "Off" | "OFF" ->
+
Some false
| _ -> None
let float_of_scalar_opt s =
···
| "+.inf" | "+.Inf" | "+.INF" -> Some Float.infinity
| "-.inf" | "-.Inf" | "-.INF" -> Some Float.neg_infinity
| ".nan" | ".NaN" | ".NAN" -> Some Float.nan
-
| _ ->
+
| _ -> (
(* Try parsing as number, allowing underscores *)
let s' = String.concat "" (String.split_on_char '_' s) in
(* Try int first (supports 0o, 0x, 0b) then float *)
match int_of_string_opt s' with
| Some i -> Some (float_of_int i)
-
| None -> float_of_string_opt s'
+
| None -> float_of_string_opt s')
let _int_of_scalar_opt s =
(* Handle hex, octal, and regular integers with underscores *)
···
int_of_string_opt s'
(* Decode a scalar value according to expected type *)
-
let rec decode_scalar_as :
-
type a. decoder -> Event.spanned -> string -> Scalar_style.t -> a t -> a =
-
fun d ev value style t ->
+
let rec decode_scalar_as : type a.
+
decoder -> Event.spanned -> string -> Scalar_style.t -> a t -> a =
+
fun d ev value style t ->
check_nodes d;
let meta = meta_of_span d ev.Event.span in
match t with
| Null map ->
if is_null_scalar value then map.dec meta ()
else err_type_mismatch d ev.span t ~fnd:("scalar " ^ value)
-
| Bool map ->
-
(match bool_of_scalar_opt value with
-
| Some b -> map.dec meta b
-
| None ->
-
(* For explicitly quoted strings, fail *)
-
if style <> `Plain then
-
err_type_mismatch d ev.span t ~fnd:("string " ^ value)
-
else
-
err_type_mismatch d ev.span t ~fnd:("scalar " ^ value))
-
| Number map ->
-
(* Handle null -> nan mapping like jsont *)
-
if is_null_scalar value then map.dec meta Float.nan
+
| Bool map -> (
+
match bool_of_scalar_opt value with
+
| Some b -> map.dec meta b
+
| None ->
+
(* For explicitly quoted strings, fail *)
+
if style <> `Plain then
+
err_type_mismatch d ev.span t ~fnd:("string " ^ value)
+
else err_type_mismatch d ev.span t ~fnd:("scalar " ^ value))
+
| Number map -> (
+
if
+
(* Handle null -> nan mapping like jsont *)
+
is_null_scalar value
+
then map.dec meta Float.nan
else
-
(match float_of_scalar_opt value with
-
| Some f -> map.dec meta f
-
| None -> err_type_mismatch d ev.span t ~fnd:("scalar " ^ value))
+
match float_of_scalar_opt value with
+
| Some f -> map.dec meta f
+
| None -> err_type_mismatch d ev.span t ~fnd:("scalar " ^ value))
| String map ->
(* Don't decode null values as strings - they should fail so outer combinators
-
like 'option' or 'any' can handle them properly *)
-
if is_null_scalar value then
+
like 'option' or 'any' can handle them properly.
+
BUT: quoted strings should always be treated as strings, even if they
+
look like null (e.g., "" or "null") *)
+
if style = `Plain && is_null_scalar value then
err_type_mismatch d ev.span t ~fnd:"null"
else
-
(* Strings accept any non-null scalar value *)
+
(* Strings accept quoted scalars or non-null plain scalars *)
map.dec meta value
+
| Array map ->
+
(* Treat null as an empty array for convenience *)
+
if is_null_scalar value then
+
let end_meta = meta_of_span d ev.Event.span in
+
map.dec_finish end_meta 0 (map.dec_empty ())
+
else
+
err_type_mismatch d ev.span t ~fnd:"scalar"
+
| Object map ->
+
(* Treat null as an empty object for convenience *)
+
if is_null_scalar value then
+
(* Build a dict with all default values from absent members *)
+
let add_default _ (Mem_dec mem_map) dict =
+
match mem_map.dec_absent with
+
| Some v -> Dict.add mem_map.id v dict
+
| None ->
+
(* Required field without default - error *)
+
let exp = String_map.singleton mem_map.name (Mem_dec mem_map) in
+
missing_mems_error meta map ~exp ~fnd:[]
+
in
+
let dict = String_map.fold add_default map.mem_decs Dict.empty in
+
let dict = Dict.add object_meta_arg meta dict in
+
apply_dict map.dec dict
+
else
+
err_type_mismatch d ev.span t ~fnd:"scalar"
| Map m ->
(* Handle Map combinators (e.g., from Jsont.option) *)
m.dec (decode_scalar_as d ev value style m.dom)
| Rec lazy_t ->
(* Handle recursive types *)
decode_scalar_as d ev value style (Lazy.force lazy_t)
-
| _ ->
-
err_type_mismatch d ev.span t ~fnd:"scalar"
+
| _ -> err_type_mismatch d ev.span t ~fnd:"scalar"
(* Forward declaration for mutual recursion *)
let rec decode : type a. decoder -> nest:int -> a t -> a =
-
fun d ~nest t ->
+
fun d ~nest t ->
check_depth d ~nest;
match peek_event d with
-
| None -> Jsont.Error.msgf Jsont.Meta.none "Unexpected end of YAML stream"
-
| Some ev ->
-
match ev.Event.event, t with
+
| None -> err_msg_none "Unexpected end of YAML stream"
+
| Some ev -> (
+
match (ev.Event.event, t) with
(* Scalar events *)
| Event.Scalar { value; style; anchor; _ }, _ ->
skip_event d;
let result = decode_scalar d ~nest ev value style t in
(* Store anchor if present - TODO: implement anchor storage *)
(match anchor with
-
| Some _name ->
-
(* We need generic JSON for anchors - decode as json and convert back *)
-
()
-
| None -> ());
+
| Some _name ->
+
(* We need generic JSON for anchors - decode as json and convert back *)
+
()
+
| None -> ());
result
-
(* Alias *)
| Event.Alias { anchor }, _ ->
skip_event d;
decode_alias d ev anchor t
-
(* Map combinator - must come before specific event matches *)
-
| _, Map m ->
-
m.dec (decode d ~nest m.dom)
-
+
| _, Map m -> m.dec (decode d ~nest m.dom)
(* Recursive types - must come before specific event matches *)
-
| _, Rec lazy_t ->
-
decode d ~nest (Lazy.force lazy_t)
-
+
| _, Rec lazy_t -> decode d ~nest (Lazy.force lazy_t)
(* Sequence -> Array *)
-
| Event.Sequence_start _, Array map ->
-
decode_array d ~nest ev map
-
-
| Event.Sequence_start _, Any map ->
-
decode_any_sequence d ~nest ev t map
-
+
| Event.Sequence_start _, Array map -> decode_array d ~nest ev map
+
| Event.Sequence_start _, Any map -> decode_any_sequence d ~nest ev t map
| Event.Sequence_start _, _ ->
err_type_mismatch d ev.span t ~fnd:"sequence"
-
(* Mapping -> Object *)
-
| Event.Mapping_start _, Object map ->
-
decode_object d ~nest ev map
-
-
| Event.Mapping_start _, Any map ->
-
decode_any_mapping d ~nest ev t map
-
-
| Event.Mapping_start _, _ ->
-
err_type_mismatch d ev.span t ~fnd:"mapping"
-
+
| Event.Mapping_start _, Object map -> decode_object d ~nest ev map
+
| Event.Mapping_start _, Any map -> decode_any_mapping d ~nest ev t map
+
| Event.Mapping_start _, _ -> err_type_mismatch d ev.span t ~fnd:"mapping"
(* Unexpected events *)
| Event.Sequence_end, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected sequence end"
+
err_msg (meta_of_span d ev.span) "Unexpected sequence end"
| Event.Mapping_end, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected mapping end"
+
err_msg (meta_of_span d ev.span) "Unexpected mapping end"
| Event.Document_start _, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected document start"
+
err_msg (meta_of_span d ev.span) "Unexpected document start"
| Event.Document_end _, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected document end"
+
err_msg (meta_of_span d ev.span) "Unexpected document end"
| Event.Stream_start _, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected stream start"
+
err_msg (meta_of_span d ev.span) "Unexpected stream start"
| Event.Stream_end, _ ->
-
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected stream end"
+
err_msg (meta_of_span d ev.span) "Unexpected stream end")
-
and decode_scalar : type a. decoder -> nest:int -> Event.spanned -> string -> Scalar_style.t -> a t -> a =
-
fun d ~nest ev value style t ->
+
and decode_scalar : type a.
+
decoder -> nest:int -> Event.spanned -> string -> Scalar_style.t -> a t -> a
+
=
+
fun d ~nest ev value style t ->
match t with
| Any map -> decode_any_scalar d ev value style t map
| Map m -> m.dec (decode_scalar d ~nest ev value style m.dom)
| Rec lazy_t -> decode_scalar d ~nest ev value style (Lazy.force lazy_t)
| _ -> decode_scalar_as d ev value style t
-
and decode_any_scalar : type a. decoder -> Event.spanned -> string -> Scalar_style.t -> a t -> a any_map -> a =
-
fun d ev value style t map ->
+
and decode_any_scalar : type a.
+
decoder ->
+
Event.spanned ->
+
string ->
+
Scalar_style.t ->
+
a t ->
+
a any_map ->
+
a =
+
fun d ev value style t map ->
check_nodes d;
+
let meta = meta_of_span d ev.span in
+
let type_err fnd = Jsont.Repr.type_error meta t ~fnd in
(* Determine which decoder to use based on scalar content *)
if is_null_scalar value then
match map.dec_null with
| Some t' -> decode_scalar_as d ev value style t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Null
+
| None -> type_err Jsont.Sort.Null
else if style = `Plain then
(* Try bool, then number, then string *)
match bool_of_scalar_opt value with
-
| Some _ ->
-
(match map.dec_bool with
-
| Some t' -> decode_scalar_as d ev value style t'
-
| None ->
-
match map.dec_string with
-
| Some t' -> decode_scalar_as d ev value style t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Bool)
-
| None ->
+
| Some _ -> (
+
match map.dec_bool with
+
| Some t' -> decode_scalar_as d ev value style t'
+
| None -> (
+
match map.dec_string with
+
| Some t' -> decode_scalar_as d ev value style t'
+
| None -> type_err Jsont.Sort.Bool))
+
| None -> (
match float_of_scalar_opt value with
-
| Some _ ->
-
(match map.dec_number with
-
| Some t' -> decode_scalar_as d ev value style t'
-
| None ->
-
match map.dec_string with
-
| Some t' -> decode_scalar_as d ev value style t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Number)
-
| None ->
+
| Some _ -> (
+
match map.dec_number with
+
| Some t' -> decode_scalar_as d ev value style t'
+
| None -> (
+
match map.dec_string with
+
| Some t' -> decode_scalar_as d ev value style t'
+
| None -> type_err Jsont.Sort.Number))
+
| None -> (
(* Plain scalar that's not bool/number -> string *)
match map.dec_string with
| Some t' -> decode_scalar_as d ev value style t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.String
+
| None -> type_err Jsont.Sort.String))
else
(* Quoted scalars are strings *)
match map.dec_string with
| Some t' -> decode_scalar_as d ev value style t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.String
+
| None -> type_err Jsont.Sort.String
and decode_alias : type a. decoder -> Event.spanned -> string -> a t -> a =
-
fun d ev anchor t ->
+
fun d ev anchor t ->
check_nodes d;
match Hashtbl.find_opt d._anchors anchor with
| None ->
let meta = meta_of_span d ev.span in
-
Jsont.Error.msgf meta "Unknown anchor: %s" anchor
-
| Some json ->
+
err_msg meta "Unknown anchor: %s" anchor
+
| Some json_value ->
(* Decode the stored JSON value through the type *)
let t' = Jsont.Repr.unsafe_to_t t in
-
match Jsont.Json.decode' t' json with
+
match Jsont.Json.decode' t' json_value with
| Ok v -> v
| Error e -> raise (Jsont.Error e)
-
and decode_array : type a elt b. decoder -> nest:int -> Event.spanned -> (a, elt, b) array_map -> a =
-
fun d ~nest start_ev map ->
-
skip_event d; (* consume Sequence_start *)
+
and decode_array : type a elt b.
+
decoder -> nest:int -> Event.spanned -> (a, elt, b) array_map -> a =
+
fun d ~nest start_ev array_map ->
+
skip_event d;
+
(* consume Sequence_start *)
check_nodes d;
let meta = meta_of_span d start_ev.span in
-
let builder = ref (map.dec_empty ()) in
+
let builder = ref (array_map.dec_empty ()) in
let idx = ref 0 in
let rec loop () =
match peek_event d with
| Some { Event.event = Event.Sequence_end; span } ->
skip_event d;
let end_meta = meta_of_span d span in
-
map.dec_finish end_meta !idx !builder
+
array_map.dec_finish end_meta !idx !builder
| Some _ ->
let i = !idx in
(try
-
if map.dec_skip i !builder then begin
+
if array_map.dec_skip i !builder then begin
(* Skip this element by decoding as ignore *)
-
let _ : unit = decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.ignore) in
+
let _ : unit =
+
decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.ignore)
+
in
()
-
end else begin
-
let elt = decode d ~nest:(nest + 1) map.elt in
-
builder := map.dec_add i elt !builder
+
end
+
else begin
+
let elt = decode d ~nest:(nest + 1) array_map.elt in
+
builder := array_map.dec_add i elt !builder
end
with Jsont.Error e ->
-
let imeta = Jsont.Meta.none in
-
Jsont.Repr.error_push_array meta map (i, imeta) e);
+
Jsont.Repr.error_push_array meta array_map (i, Jsont.Meta.none) e);
incr idx;
loop ()
-
| None ->
-
Jsont.Error.msgf meta "Unclosed sequence"
+
| None -> err_msg meta "Unclosed sequence"
in
loop ()
-
and decode_any_sequence : type a. decoder -> nest:int -> Event.spanned -> a t -> a any_map -> a =
-
fun d ~nest ev t map ->
+
and decode_any_sequence : type a.
+
decoder -> nest:int -> Event.spanned -> a t -> a any_map -> a =
+
fun d ~nest ev t map ->
match map.dec_array with
-
| Some t' ->
+
| Some t' -> (
(* The t' decoder might be wrapped (e.g., Map for option types)
Directly decode the array and let the wrapper handle it *)
-
(match t' with
-
| Array array_map ->
-
decode_array d ~nest ev array_map
-
| _ ->
-
(* For wrapped types like Map (Array ...), use full decode *)
-
decode d ~nest t')
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Array
+
match t' with
+
| Array array_map -> decode_array d ~nest ev array_map
+
| _ ->
+
(* For wrapped types like Map (Array ...), use full decode *)
+
decode d ~nest t')
+
| None ->
+
Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Array
-
and decode_object : type o. decoder -> nest:int -> Event.spanned -> (o, o) object_map -> o =
-
fun d ~nest start_ev map ->
-
skip_event d; (* consume Mapping_start *)
+
and decode_object : type o.
+
decoder -> nest:int -> Event.spanned -> (o, o) object_map -> o =
+
fun d ~nest start_ev map ->
+
skip_event d;
+
(* consume Mapping_start *)
check_nodes d;
let meta = meta_of_span d start_ev.span in
-
let dict = decode_object_members d ~nest meta map String_map.empty Dict.empty in
+
let dict =
+
decode_object_members d ~nest meta map String_map.empty Dict.empty
+
in
let dict = Dict.add object_meta_arg meta dict in
apply_dict map.dec dict
and decode_object_members : type o.
-
decoder -> nest:int -> Jsont.Meta.t -> (o, o) object_map ->
-
mem_dec String_map.t -> Dict.t -> Dict.t =
-
fun d ~nest obj_meta map mem_miss dict ->
+
decoder ->
+
nest:int ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
mem_dec String_map.t ->
+
Dict.t ->
+
Dict.t =
+
fun d ~nest obj_meta map mem_miss dict ->
(* Merge expected member decoders *)
let u _ _ _ = assert false in
let mem_miss = String_map.union u mem_miss map.mem_decs in
···
decode_object_cases d ~nest obj_meta map umems cases mem_miss [] dict
and decode_object_basic : type o mems builder.
-
decoder -> nest:int -> Jsont.Meta.t -> (o, o) object_map ->
-
(o, mems, builder) unknown_mems ->
-
mem_dec String_map.t -> Dict.t -> Dict.t =
-
fun d ~nest obj_meta map umems mem_miss dict ->
-
let ubuilder = ref (match umems with
-
| Unknown_skip | Unknown_error -> Obj.magic ()
-
| Unknown_keep (mmap, _) -> mmap.dec_empty ()) in
+
decoder ->
+
nest:int ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
(o, mems, builder) unknown_mems ->
+
mem_dec String_map.t ->
+
Dict.t ->
+
Dict.t =
+
fun d ~nest obj_meta object_map umems mem_miss dict ->
+
let ubuilder =
+
ref
+
(match umems with
+
| Unknown_skip | Unknown_error -> Obj.magic ()
+
| Unknown_keep (mmap, _) -> mmap.dec_empty ())
+
in
let mem_miss = ref mem_miss in
let dict = ref dict in
let rec loop () =
···
| Some { Event.event = Event.Mapping_end; _ } ->
skip_event d;
(* Finalize *)
-
finish_object obj_meta map umems !ubuilder !mem_miss !dict
+
finish_object obj_meta object_map umems !ubuilder !mem_miss !dict
| Some ev ->
(* Expect a scalar key *)
let name, name_meta = decode_mapping_key d ev in
(* Look up member decoder *)
-
(match String_map.find_opt name map.mem_decs with
-
| Some (Mem_dec mem) ->
-
mem_miss := String_map.remove name !mem_miss;
-
(try
-
let v = decode d ~nest:(nest + 1) mem.type' in
-
dict := Dict.add mem.id v !dict
-
with Jsont.Error e ->
-
Jsont.Repr.error_push_object obj_meta map (name, name_meta) e)
-
| None ->
-
(* Unknown member *)
-
match umems with
-
| Unknown_skip ->
-
let _ : unit = decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.ignore) in
-
()
-
| Unknown_error ->
-
Jsont.Repr.unexpected_mems_error obj_meta map ~fnd:[(name, name_meta)]
-
| Unknown_keep (mmap, _) ->
-
(try
-
let v = decode d ~nest:(nest + 1) mmap.mems_type in
-
ubuilder := mmap.dec_add name_meta name v !ubuilder
-
with Jsont.Error e ->
-
Jsont.Repr.error_push_object obj_meta map (name, name_meta) e));
+
(match String_map.find_opt name object_map.mem_decs with
+
| Some (Mem_dec mem) -> (
+
mem_miss := String_map.remove name !mem_miss;
+
try
+
let v = decode d ~nest:(nest + 1) mem.type' in
+
dict := Dict.add mem.id v !dict
+
with Jsont.Error e ->
+
Jsont.Repr.error_push_object obj_meta object_map (name, name_meta)
+
e)
+
| None -> (
+
(* Unknown member *)
+
match umems with
+
| Unknown_skip ->
+
let _ : unit =
+
decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.ignore)
+
in
+
()
+
| Unknown_error ->
+
Jsont.Repr.unexpected_mems_error obj_meta object_map
+
~fnd:[ (name, name_meta) ]
+
| Unknown_keep (mmap, _) -> (
+
try
+
let v = decode d ~nest:(nest + 1) mmap.mems_type in
+
ubuilder := mmap.dec_add name_meta name v !ubuilder
+
with Jsont.Error e ->
+
Jsont.Repr.error_push_object obj_meta object_map
+
(name, name_meta) e)));
loop ()
-
| None ->
-
Jsont.Error.msgf obj_meta "Unclosed mapping"
+
| None -> err_msg obj_meta "Unclosed mapping"
in
loop ()
and finish_object : type o mems builder.
-
Jsont.Meta.t -> (o, o) object_map -> (o, mems, builder) unknown_mems ->
-
builder -> mem_dec String_map.t -> Dict.t -> Dict.t =
-
fun meta map umems ubuilder mem_miss dict ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
(o, mems, builder) unknown_mems ->
+
builder ->
+
mem_dec String_map.t ->
+
Dict.t ->
+
Dict.t =
+
fun meta map umems ubuilder mem_miss dict ->
+
let open Jsont.Repr in
let dict = Dict.add object_meta_arg meta dict in
-
let dict = match umems with
+
let dict =
+
match umems with
| Unknown_skip | Unknown_error -> dict
-
| Unknown_keep (mmap, _) -> Dict.add mmap.id (mmap.dec_finish meta ubuilder) dict
+
| Unknown_keep (mmap, _) ->
+
Dict.add mmap.id (mmap.dec_finish meta ubuilder) dict
in
(* Check for missing required members *)
let add_default _ (Mem_dec mem_map) dict =
···
with Exit ->
let no_default _ (Mem_dec mm) = Option.is_none mm.dec_absent in
let exp = String_map.filter no_default mem_miss in
-
Jsont.Repr.missing_mems_error meta map ~exp ~fnd:[]
+
missing_mems_error meta map ~exp ~fnd:[]
and decode_object_cases : type o cases tag.
-
decoder -> nest:int -> Jsont.Meta.t -> (o, o) object_map ->
-
unknown_mems_option ->
-
(o, cases, tag) object_cases ->
-
mem_dec String_map.t -> (Jsont.name * Jsont.json) list -> Dict.t -> Dict.t =
-
fun d ~nest obj_meta map umems cases mem_miss delayed dict ->
+
decoder ->
+
nest:int ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
unknown_mems_option ->
+
(o, cases, tag) object_cases ->
+
mem_dec String_map.t ->
+
(Jsont.name * Jsont.json) list ->
+
Dict.t ->
+
Dict.t =
+
fun d ~nest obj_meta object_map umems cases mem_miss delayed dict ->
match peek_event d with
-
| Some { Event.event = Event.Mapping_end; _ } ->
+
| Some { Event.event = Event.Mapping_end; _ } -> (
skip_event d;
(* No tag found - use dec_absent if available *)
-
(match cases.tag.dec_absent with
-
| Some tag ->
-
decode_with_case_tag d ~nest obj_meta map umems cases tag mem_miss delayed dict
-
| None ->
-
(* Missing required case tag *)
-
let exp = String_map.singleton cases.tag.name (Mem_dec cases.tag) in
-
let fnd = List.map (fun ((n, _), _) -> n) delayed in
-
Jsont.Repr.missing_mems_error obj_meta map ~exp ~fnd)
+
match cases.tag.dec_absent with
+
| Some tag ->
+
decode_with_case_tag d ~nest obj_meta object_map umems cases tag
+
mem_miss delayed dict
+
| None ->
+
(* Missing required case tag *)
+
let exp = String_map.singleton cases.tag.name (Mem_dec cases.tag) in
+
let fnd = List.map (fun ((n, _), _) -> n) delayed in
+
Jsont.Repr.missing_mems_error obj_meta object_map ~exp ~fnd)
| Some ev ->
let name, name_meta = decode_mapping_key d ev in
if String.equal name cases.tag.name then begin
(* Found the case tag *)
let tag = decode d ~nest:(nest + 1) cases.tag.type' in
-
decode_with_case_tag d ~nest obj_meta map umems cases tag mem_miss delayed dict
-
end else begin
+
decode_with_case_tag d ~nest obj_meta object_map umems cases tag
+
mem_miss delayed dict
+
end
+
else begin
(* Not the case tag - check if known member or delay *)
-
match String_map.find_opt name map.mem_decs with
-
| Some (Mem_dec mem) ->
+
match String_map.find_opt name object_map.mem_decs with
+
| Some (Mem_dec mem) -> (
let mem_miss = String_map.remove name mem_miss in
-
(try
-
let v = decode d ~nest:(nest + 1) mem.type' in
-
let dict = Dict.add mem.id v dict in
-
decode_object_cases d ~nest obj_meta map umems cases mem_miss delayed dict
-
with Jsont.Error e ->
-
Jsont.Repr.error_push_object obj_meta map (name, name_meta) e)
+
try
+
let v = decode d ~nest:(nest + 1) mem.type' in
+
let dict = Dict.add mem.id v dict in
+
decode_object_cases d ~nest obj_meta object_map umems cases
+
mem_miss delayed dict
+
with Jsont.Error e ->
+
Jsont.Repr.error_push_object obj_meta object_map (name, name_meta)
+
e)
| None ->
(* Unknown member - decode as generic JSON and delay *)
-
let v = decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.json) in
+
let v =
+
decode d ~nest:(nest + 1) (Jsont.Repr.of_t Jsont.json)
+
in
let delayed = ((name, name_meta), v) :: delayed in
-
decode_object_cases d ~nest obj_meta map umems cases mem_miss delayed dict
+
decode_object_cases d ~nest obj_meta object_map umems cases
+
mem_miss delayed dict
end
-
| None ->
-
Jsont.Error.msgf obj_meta "Unclosed mapping"
+
| None -> err_msg obj_meta "Unclosed mapping"
and decode_with_case_tag : type o cases tag.
-
decoder -> nest:int -> Jsont.Meta.t -> (o, o) object_map ->
-
unknown_mems_option ->
-
(o, cases, tag) object_cases -> tag ->
-
mem_dec String_map.t -> (Jsont.name * Jsont.json) list -> Dict.t -> Dict.t =
-
fun d ~nest obj_meta map umems cases tag mem_miss delayed dict ->
+
decoder ->
+
nest:int ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
unknown_mems_option ->
+
(o, cases, tag) object_cases ->
+
tag ->
+
mem_dec String_map.t ->
+
(Jsont.name * Jsont.json) list ->
+
Dict.t ->
+
Dict.t =
+
fun d ~nest obj_meta map umems cases tag mem_miss delayed dict ->
+
let open Jsont.Repr in
let eq_tag (Case c) = cases.tag_compare c.tag tag = 0 in
match List.find_opt eq_tag cases.cases with
-
| None ->
-
Jsont.Repr.unexpected_case_tag_error obj_meta map cases tag
+
| None -> unexpected_case_tag_error obj_meta map cases tag
| Some (Case case) ->
(* Continue decoding with the case's object map *)
-
let case_dict = decode_case_remaining d ~nest obj_meta case.object_map
-
umems mem_miss delayed dict in
+
let case_dict =
+
decode_case_remaining d ~nest obj_meta case.object_map umems mem_miss
+
delayed dict
+
in
let case_value = apply_dict case.object_map.dec case_dict in
Dict.add cases.id (case.dec case_value) dict
and decode_case_remaining : type o.
-
decoder -> nest:int -> Jsont.Meta.t -> (o, o) object_map ->
-
unknown_mems_option ->
-
mem_dec String_map.t -> (Jsont.name * Jsont.json) list -> Dict.t -> Dict.t =
-
fun d ~nest obj_meta case_map _umems mem_miss delayed dict ->
+
decoder ->
+
nest:int ->
+
Jsont.Meta.t ->
+
(o, o) object_map ->
+
unknown_mems_option ->
+
mem_dec String_map.t ->
+
(Jsont.name * Jsont.json) list ->
+
Dict.t ->
+
Dict.t =
+
fun d ~nest obj_meta case_map _umems mem_miss delayed dict ->
(* First, process delayed members against the case map *)
let u _ _ _ = assert false in
let mem_miss = String_map.union u mem_miss case_map.mem_decs in
-
let dict, mem_miss = List.fold_left (fun (dict, mem_miss) ((name, meta), json) ->
-
match String_map.find_opt name case_map.mem_decs with
-
| Some (Mem_dec mem) ->
-
let t' = Jsont.Repr.unsafe_to_t mem.type' in
-
(match Jsont.Json.decode' t' json with
-
| Ok v ->
-
let dict = Dict.add mem.id v dict in
-
let mem_miss = String_map.remove name mem_miss in
-
(dict, mem_miss)
-
| Error e ->
-
Jsont.Repr.error_push_object obj_meta case_map (name, meta) e)
-
| None ->
-
(* Unknown for case too - skip them *)
-
(dict, mem_miss)
-
) (dict, mem_miss) delayed in
+
let dict, mem_miss =
+
List.fold_left
+
(fun (dict, mem_miss) ((name, meta), json_value) ->
+
match String_map.find_opt name case_map.mem_decs with
+
| Some (Mem_dec mem) -> (
+
let t' = Jsont.Repr.unsafe_to_t mem.type' in
+
match Jsont.Json.decode' t' json_value with
+
| Ok v ->
+
let dict = Dict.add mem.id v dict in
+
let mem_miss = String_map.remove name mem_miss in
+
(dict, mem_miss)
+
| Error e ->
+
Jsont.Repr.error_push_object obj_meta case_map (name, meta) e)
+
| None ->
+
(* Unknown for case too - skip them *)
+
(dict, mem_miss))
+
(dict, mem_miss) delayed
+
in
(* Then continue reading remaining members using case's own unknown handling *)
match case_map.shape with
| Object_basic case_umems ->
···
(* Nested cases shouldn't happen - use skip for safety *)
decode_object_basic d ~nest obj_meta case_map Unknown_skip mem_miss dict
-
and decode_any_mapping : type a. decoder -> nest:int -> Event.spanned -> a t -> a any_map -> a =
-
fun d ~nest ev t map ->
+
and decode_any_mapping : type a.
+
decoder -> nest:int -> Event.spanned -> a t -> a any_map -> a =
+
fun d ~nest ev t map ->
match map.dec_object with
| Some t' -> decode d ~nest t'
-
| None -> Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Object
+
| None ->
+
Jsont.Repr.type_error (meta_of_span d ev.span) t ~fnd:Jsont.Sort.Object
and decode_mapping_key : decoder -> Event.spanned -> string * Jsont.Meta.t =
-
fun d ev ->
+
fun d ev ->
match ev.Event.event with
| Event.Scalar { value; _ } ->
skip_event d;
···
(value, meta)
| _ ->
let meta = meta_of_span d ev.span in
-
Jsont.Error.msgf meta "Mapping keys must be scalars (strings), found %a"
-
Event.pp ev.event
+
err_msg meta "Mapping keys must be scalars (strings), found %a" Event.pp
+
ev.event
(* Skip stream/document wrappers *)
let skip_to_content d =
let rec loop () =
match peek_event d with
-
| Some { Event.event = Event.Stream_start _; _ } -> skip_event d; loop ()
-
| Some { Event.event = Event.Document_start _; _ } -> skip_event d; loop ()
+
| Some { Event.event = Event.Stream_start _; _ } ->
+
skip_event d;
+
loop ()
+
| Some { Event.event = Event.Document_start _; _ } ->
+
skip_event d;
+
loop ()
| _ -> ()
in
loop ()
···
let skip_end_wrappers d =
let rec loop () =
match peek_event d with
-
| Some { Event.event = Event.Document_end _; _ } -> skip_event d; loop ()
-
| Some { Event.event = Event.Stream_end; _ } -> skip_event d; loop ()
+
| Some { Event.event = Event.Document_end _; _ } ->
+
skip_event d;
+
loop ()
+
| Some { Event.event = Event.Stream_end; _ } ->
+
skip_event d;
+
loop ()
| None -> ()
| Some ev ->
let meta = meta_of_span d ev.span in
-
Jsont.Error.msgf meta "Expected end of document but found %a" Event.pp ev.event
+
err_msg meta "Expected end of document but found %a" Event.pp ev.event
in
loop ()
+
(* Skip to the end of the current document after an error *)
+
let skip_to_document_end d =
+
let rec loop depth =
+
match peek_event d with
+
| None -> ()
+
| Some { Event.event = Event.Stream_end; _ } -> ()
+
| Some { Event.event = Event.Document_end _; _ } ->
+
skip_event d;
+
if depth = 0 then () else loop (depth - 1)
+
| Some { Event.event = Event.Document_start _; _ } ->
+
skip_event d;
+
loop (depth + 1)
+
| Some _ ->
+
skip_event d;
+
loop depth
+
in
+
loop 0
+
(* Public decode API *)
+
(* Decode all documents from a multi-document YAML stream *)
+
let decode_all' ?(layout = false) ?(locs = false) ?(file = "-")
+
?(max_depth = 100) ?(max_nodes = 10_000_000) t reader =
+
let parser = Parser.of_reader reader in
+
let d = make_decoder ~layout ~locs ~file ~max_depth ~max_nodes parser in
+
let t' = Jsont.Repr.of_t t in
+
let rec next_doc () =
+
match peek_event d with
+
| None -> Seq.Nil
+
| Some { Event.event = Event.Stream_end; _ } ->
+
skip_event d;
+
Seq.Nil
+
| Some _ -> (
+
try
+
skip_to_content d;
+
(* Reset node count for each document *)
+
d.node_count <- 0;
+
let v = decode d ~nest:0 t' in
+
(* Skip document end marker if present *)
+
(match peek_event d with
+
| Some { Event.event = Event.Document_end _; _ } -> skip_event d
+
| _ -> ());
+
Seq.Cons (Ok v, next_doc)
+
with
+
| Jsont.Error e ->
+
skip_to_document_end d;
+
Seq.Cons (Error e, next_doc)
+
| Error.Yamlrw_error err ->
+
skip_to_document_end d;
+
let msg = Error.to_string err in
+
let e = Jsont.(Error.make_msg Error.Context.empty Meta.none msg) in
+
Seq.Cons (Error e, next_doc))
+
in
+
next_doc
+
+
let decode_all ?layout ?locs ?file ?max_depth ?max_nodes t reader =
+
decode_all' ?layout ?locs ?file ?max_depth ?max_nodes t reader
+
|> Seq.map (Result.map_error Jsont.Error.to_string)
+
let decode' ?layout ?locs ?file ?max_depth ?max_nodes t reader =
let parser = Parser.of_reader reader in
let d = make_decoder ?layout ?locs ?file ?max_depth ?max_nodes parser in
···
| Jsont.Error e -> Error e
| Error.Yamlrw_error err ->
let msg = Error.to_string err in
-
Error (Jsont.Error.make_msg Jsont.Error.Context.empty Jsont.Meta.none msg)
+
Error Jsont.(Error.make_msg Error.Context.empty Meta.none msg)
let decode ?layout ?locs ?file ?max_depth ?max_nodes t reader =
Result.map_error Jsont.Error.to_string
(decode' ?layout ?locs ?file ?max_depth ?max_nodes t reader)
-
let decode_string' ?layout ?locs ?file ?max_depth ?max_nodes t s =
-
decode' ?layout ?locs ?file ?max_depth ?max_nodes t (Bytes.Reader.of_string s)
-
-
let decode_string ?layout ?locs ?file ?max_depth ?max_nodes t s =
-
decode ?layout ?locs ?file ?max_depth ?max_nodes t (Bytes.Reader.of_string s)
-
(* Encoder *)
type encoder = {
···
scalar_style : Scalar_style.t;
}
-
let make_encoder
-
?(format = Block) ?(indent = 2) ?(explicit_doc = false)
+
let make_encoder ?(format = Block) ?(indent = 2) ?(explicit_doc = false)
?(scalar_style = `Any) emitter =
{ emitter; format; _indent = indent; explicit_doc; scalar_style }
···
else if String.length s > 80 then `Folded
else `Plain
+
(* Helper to create scalar events with common defaults *)
+
let scalar_event ?(anchor = None) ?(tag = None) ~value ~style () =
+
Event.Scalar
+
{
+
anchor;
+
tag;
+
value;
+
plain_implicit = true;
+
quoted_implicit = true;
+
style;
+
}
+
+
(* Helper to emit events *)
+
let emit e = Emitter.emit e.emitter
+
(* Encode null *)
-
let encode_null e _meta =
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = "null";
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
})
+
let encode_null e _meta = emit e (scalar_event ~value:"null" ~style:`Plain ())
(* Encode boolean *)
let encode_bool e _meta b =
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = if b then "true" else "false";
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
})
+
emit e (scalar_event ~value:(if b then "true" else "false") ~style:`Plain ())
(* Encode number *)
let encode_number e _meta f =
let value =
-
if Float.is_nan f then ".nan"
-
else if f = Float.infinity then ".inf"
-
else if f = Float.neg_infinity then "-.inf"
-
else
-
let s = Printf.sprintf "%.17g" f in
-
(* Ensure it looks like a number *)
-
if String.contains s '.' || String.contains s 'e' || String.contains s 'E'
-
then s
-
else s ^ ".0"
+
match Float.classify_float f with
+
| FP_nan -> ".nan"
+
| FP_infinite -> if f > 0.0 then ".inf" else "-.inf"
+
| _ ->
+
if Float.is_integer f && Float.abs f < 1e15 then Printf.sprintf "%.0f" f
+
else Printf.sprintf "%g" f
in
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value;
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
})
+
emit e (scalar_event ~value ~style:`Plain ())
(* Encode string *)
let encode_string e _meta s =
let style = choose_scalar_style ~preferred:e.scalar_style s in
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = s;
-
plain_implicit = true;
-
quoted_implicit = true;
-
style;
-
})
+
emit e (scalar_event ~value:s ~style ())
let rec encode : type a. encoder -> a t -> a -> unit =
-
fun e t v ->
+
fun e t v ->
match t with
| Null map ->
let meta = map.enc_meta v in
let () = map.enc v in
encode_null e meta
-
| Bool map ->
let meta = map.enc_meta v in
let b = map.enc v in
encode_bool e meta b
-
| Number map ->
let meta = map.enc_meta v in
let f = map.enc v in
encode_number e meta f
-
| String map ->
let meta = map.enc_meta v in
let s = map.enc v in
encode_string e meta s
-
-
| Array map ->
-
encode_array e map v
-
-
| Object map ->
-
encode_object e map v
-
+
| Array map -> encode_array e map v
+
| Object map -> encode_object e map v
| Any map ->
let t' = map.enc v in
encode e t' v
-
-
| Map m ->
-
encode e m.dom (m.enc v)
-
-
| Rec lazy_t ->
-
encode e (Lazy.force lazy_t) v
+
| Map m -> encode e m.dom (m.enc v)
+
| Rec lazy_t -> encode e (Lazy.force lazy_t) v
and encode_array : type a elt b. encoder -> (a, elt, b) array_map -> a -> unit =
-
fun e map v ->
+
fun e map v ->
let style = layout_style_of_format e.format in
-
Emitter.emit e.emitter (Event.Sequence_start {
-
anchor = None;
-
tag = None;
-
implicit = true;
-
style;
-
});
-
let _ = map.enc (fun () _idx elt ->
-
encode e map.elt elt;
-
()
-
) () v in
-
Emitter.emit e.emitter Event.Sequence_end
+
emit e
+
(Event.Sequence_start { anchor = None; tag = None; implicit = true; style });
+
let _ =
+
map.enc
+
(fun () _idx elt ->
+
encode e map.elt elt;
+
())
+
() v
+
in
+
emit e Event.Sequence_end
and encode_object : type o. encoder -> (o, o) object_map -> o -> unit =
-
fun e map v ->
+
fun e map v ->
let style = layout_style_of_format e.format in
-
Emitter.emit e.emitter (Event.Mapping_start {
-
anchor = None;
-
tag = None;
-
implicit = true;
-
style;
-
});
+
emit e (Event.Mapping_start { anchor = None; tag = None; implicit = true; style });
(* Encode each member *)
-
List.iter (fun (Mem_enc mem) ->
-
let mem_v = mem.enc v in
-
if not (mem.enc_omit mem_v) then begin
-
(* Emit key *)
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = mem.name;
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
});
-
(* Emit value *)
-
encode e mem.type' mem_v
-
end
-
) map.mem_encs;
+
List.iter
+
(fun (Mem_enc mem) ->
+
let mem_v = mem.enc v in
+
if not (mem.enc_omit mem_v) then begin
+
(* Emit key *)
+
emit e (scalar_event ~value:mem.name ~style:`Plain ());
+
(* Emit value *)
+
encode e mem.type' mem_v
+
end)
+
map.mem_encs;
(* Handle case objects *)
(match map.shape with
-
| Object_basic _ -> ()
-
| Object_cases (_, cases) ->
-
let Case_value (case_map, case_v) = cases.enc_case (cases.enc v) in
-
(* Emit case tag *)
-
if not (cases.tag.enc_omit (case_map.tag)) then begin
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = cases.tag.name;
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
});
-
encode e cases.tag.type' case_map.tag
-
end;
-
(* Emit case members *)
-
List.iter (fun (Mem_enc mem) ->
-
let mem_v = mem.enc case_v in
-
if not (mem.enc_omit mem_v) then begin
-
Emitter.emit e.emitter (Event.Scalar {
-
anchor = None;
-
tag = None;
-
value = mem.name;
-
plain_implicit = true;
-
quoted_implicit = true;
-
style = `Plain;
-
});
-
encode e mem.type' mem_v
-
end
-
) case_map.object_map.mem_encs);
-
Emitter.emit e.emitter Event.Mapping_end
+
| Object_basic _ -> ()
+
| Object_cases (_, cases) ->
+
let (Case_value (case_map, case_v)) = cases.enc_case (cases.enc v) in
+
(* Emit case tag *)
+
if not (cases.tag.enc_omit case_map.tag) then begin
+
emit e (scalar_event ~value:cases.tag.name ~style:`Plain ());
+
encode e cases.tag.type' case_map.tag
+
end;
+
(* Emit case members *)
+
List.iter
+
(fun (Mem_enc mem) ->
+
let mem_v = mem.enc case_v in
+
if not (mem.enc_omit mem_v) then begin
+
emit e (scalar_event ~value:mem.name ~style:`Plain ());
+
encode e mem.type' mem_v
+
end)
+
case_map.object_map.mem_encs);
+
emit e Event.Mapping_end
(* Public encode API *)
let encode' ?buf:_ ?format ?indent ?explicit_doc ?scalar_style t v ~eod writer =
-
let config = {
-
Emitter.default_config with
-
indent = Option.value ~default:2 indent;
-
layout_style = (match format with
-
| Some Flow -> `Flow
-
| _ -> `Block);
-
} in
+
let config =
+
{
+
Emitter.default_config with
+
indent = Option.value ~default:2 indent;
+
layout_style = (match format with Some Flow -> `Flow | _ -> `Block);
+
}
+
in
let emitter = Emitter.of_writer ~config writer in
let e = make_encoder ?format ?indent ?explicit_doc ?scalar_style emitter in
try
-
Emitter.emit e.emitter (Event.Stream_start { encoding = `Utf8 });
-
Emitter.emit e.emitter (Event.Document_start {
-
version = None;
-
implicit = not e.explicit_doc;
-
});
+
emit e (Event.Stream_start { encoding = `Utf8 });
+
emit e (Event.Document_start { version = None; implicit = not e.explicit_doc });
let t' = Jsont.Repr.of_t t in
encode e t' v;
-
Emitter.emit e.emitter (Event.Document_end { implicit = not e.explicit_doc });
-
Emitter.emit e.emitter Event.Stream_end;
+
emit e (Event.Document_end { implicit = not e.explicit_doc });
+
emit e Event.Stream_end;
if eod then Emitter.flush e.emitter;
Ok ()
with
| Jsont.Error err -> Error err
| Error.Yamlrw_error err ->
let msg = Error.to_string err in
-
Error (Jsont.Error.make_msg Jsont.Error.Context.empty Jsont.Meta.none msg)
+
Error Jsont.(Error.make_msg Error.Context.empty Meta.none msg)
let encode ?buf ?format ?indent ?explicit_doc ?scalar_style t v ~eod writer =
Result.map_error Jsont.Error.to_string
(encode' ?buf ?format ?indent ?explicit_doc ?scalar_style t v ~eod writer)
-
-
let encode_string' ?buf ?format ?indent ?explicit_doc ?scalar_style t v =
-
let b = Buffer.create 256 in
-
let writer = Bytes.Writer.of_buffer b in
-
match encode' ?buf ?format ?indent ?explicit_doc ?scalar_style t v ~eod:true writer with
-
| Ok () -> Ok (Buffer.contents b)
-
| Error e -> Error e
-
-
let encode_string ?buf ?format ?indent ?explicit_doc ?scalar_style t v =
-
Result.map_error Jsont.Error.to_string
-
(encode_string' ?buf ?format ?indent ?explicit_doc ?scalar_style t v)
(* Recode *)
-
let recode ?layout ?locs ?file ?max_depth ?max_nodes
-
?buf ?format ?indent ?explicit_doc ?scalar_style t reader writer ~eod =
-
let format = match layout, format with
-
| Some true, None -> Some Layout
-
| _, f -> f
+
let recode ?layout ?locs ?file ?max_depth ?max_nodes ?buf ?format ?indent
+
?explicit_doc ?scalar_style t reader writer ~eod =
+
let format =
+
match (layout, format) with Some true, None -> Some Layout | _, f -> f
in
-
let layout = match layout, format with
-
| None, Some Layout -> Some true
-
| l, _ -> l
+
let layout =
+
match (layout, format) with None, Some Layout -> Some true | l, _ -> l
in
match decode' ?layout ?locs ?file ?max_depth ?max_nodes t reader with
-
| Ok v -> encode ?buf ?format ?indent ?explicit_doc ?scalar_style t v ~eod writer
-
| Error e -> Error (Jsont.Error.to_string e)
-
-
let recode_string ?layout ?locs ?file ?max_depth ?max_nodes
-
?buf ?format ?indent ?explicit_doc ?scalar_style t s =
-
let format = match layout, format with
-
| Some true, None -> Some Layout
-
| _, f -> f
-
in
-
let layout = match layout, format with
-
| None, Some Layout -> Some true
-
| l, _ -> l
-
in
-
match decode_string' ?layout ?locs ?file ?max_depth ?max_nodes t s with
-
| Ok v -> encode_string ?buf ?format ?indent ?explicit_doc ?scalar_style t v
+
| Ok v ->
+
encode ?buf ?format ?indent ?explicit_doc ?scalar_style t v ~eod writer
| Error e -> Error (Jsont.Error.to_string e)
+176 -104
lib/yamlt.mli
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** YAML codec using Jsont type descriptions.
-
This module provides YAML streaming encode/decode that interprets
-
{!Jsont.t} type descriptions, allowing the same codec definitions
-
to work for both JSON and YAML.
+
This module provides YAML streaming encode/decode that interprets {!Jsont.t}
+
type descriptions, allowing the same codec definitions to work for both JSON
+
and YAML.
{b Example:}
{[
(* Define a codec once using Jsont *)
module Config = struct
-
type t = { name: string; port: int }
+
type t = { name : string; port : int }
+
let make name port = { name; port }
+
let jsont =
Jsont.Object.map ~kind:"Config" make
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
···
let from_yaml = Yamlt.decode_string Config.jsont yaml_str
]}
-
See notes about {{!yaml_mapping}YAML to JSON mapping} and
-
{{!yaml_scalars}YAML scalar resolution}.
-
*)
+
See notes about {{!yaml_mapping}YAML to JSON mapping},
+
{{!yaml_scalars}YAML scalar resolution}, and
+
{{!null_handling}null value handling}. *)
open Bytesrw
(** {1:decode Decode} *)
val decode :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
'a Jsont.t -> Bytes.Reader.t -> ('a, string) result
+
?layout:bool ->
+
?locs:bool ->
+
?file:Jsont.Textloc.fpath ->
+
?max_depth:int ->
+
?max_nodes:int ->
+
'a Jsont.t ->
+
Bytes.Reader.t ->
+
('a, string) result
(** [decode t r] decodes a value from YAML reader [r] according to type [t].
-
{ul
-
{- If [layout] is [true], style information is preserved in {!Jsont.Meta.t}
-
values (for potential round-tripping). Defaults to [false].}
-
{- If [locs] is [true], source locations are preserved in {!Jsont.Meta.t}
-
values and error messages are precisely located. Defaults to [false].}
-
{- [file] is the file path for error messages.
-
Defaults to {!Jsont.Textloc.file_none}.}
-
{- [max_depth] limits nesting depth to prevent stack overflow
-
(billion laughs protection). Defaults to [100].}
-
{- [max_nodes] limits total decoded nodes
-
(billion laughs protection). Defaults to [10_000_000].}}
+
- If [layout] is [true], style information is preserved in {!Jsont.Meta.t}
+
values (for potential round-tripping). Defaults to [false].
+
- If [locs] is [true], source locations are preserved in {!Jsont.Meta.t}
+
values and error messages are precisely located. Defaults to [false].
+
- [file] is the file path for error messages. Defaults to
+
{!Jsont.Textloc.file_none}.
+
- [max_depth] limits nesting depth to prevent stack overflow (billion laughs
+
protection). Defaults to [100].
+
- [max_nodes] limits total decoded nodes (billion laughs protection).
+
Defaults to [10_000_000].
-
The YAML input must contain exactly one document. Multi-document
-
streams are not supported; use {!decode_all} for those. *)
+
The YAML input must contain exactly one document. Multi-document streams are
+
not supported; use {!decode_all} for those. *)
val decode' :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
'a Jsont.t -> Bytes.Reader.t -> ('a, Jsont.Error.t) result
+
?layout:bool ->
+
?locs:bool ->
+
?file:Jsont.Textloc.fpath ->
+
?max_depth:int ->
+
?max_nodes:int ->
+
'a Jsont.t ->
+
Bytes.Reader.t ->
+
('a, Jsont.Error.t) result
(** [decode'] is like {!val-decode} but preserves the error structure. *)
-
val decode_string :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
'a Jsont.t -> string -> ('a, string) result
-
(** [decode_string] is like {!val-decode} but decodes directly from a string. *)
+
val decode_all :
+
?layout:bool ->
+
?locs:bool ->
+
?file:Jsont.Textloc.fpath ->
+
?max_depth:int ->
+
?max_nodes:int ->
+
'a Jsont.t ->
+
Bytes.Reader.t ->
+
('a, string) result Seq.t
+
(** [decode_all t r] decodes all documents from a multi-document YAML stream.
+
Returns a sequence where each element is a result of decoding one document.
+
Parameters are as in {!val-decode}. Use this for YAML streams containing
+
multiple documents separated by [---]. *)
-
val decode_string' :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
'a Jsont.t -> string -> ('a, Jsont.Error.t) result
-
(** [decode_string'] is like {!val-decode'} but decodes directly from a string. *)
+
val decode_all' :
+
?layout:bool ->
+
?locs:bool ->
+
?file:Jsont.Textloc.fpath ->
+
?max_depth:int ->
+
?max_nodes:int ->
+
'a Jsont.t ->
+
Bytes.Reader.t ->
+
('a, Jsont.Error.t) result Seq.t
+
(** [decode_all'] is like {!val-decode_all} but preserves the error structure. *)
(** {1:encode Encode} *)
(** YAML output format. *)
type yaml_format =
-
| Block (** Block style (indented) - default. Clean, readable YAML. *)
-
| Flow (** Flow style (JSON-like). Compact, single-line collections. *)
+
| Block (** Block style (indented) - default. Clean, readable YAML. *)
+
| Flow (** Flow style (JSON-like). Compact, single-line collections. *)
| Layout (** Preserve layout from {!Jsont.Meta.t} when available. *)
val encode :
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> 'a -> eod:bool -> Bytes.Writer.t -> (unit, string) result
+
?buf:Stdlib.Bytes.t ->
+
?format:yaml_format ->
+
?indent:int ->
+
?explicit_doc:bool ->
+
?scalar_style:Yamlrw.Scalar_style.t ->
+
'a Jsont.t ->
+
'a ->
+
eod:bool ->
+
Bytes.Writer.t ->
+
(unit, string) result
(** [encode t v w] encodes value [v] according to type [t] to YAML on [w].
-
{ul
-
{- If [buf] is specified, it is used as a buffer for output slices.
-
Defaults to a buffer of length {!Bytesrw.Bytes.Writer.slice_length}[ w].}
-
{- [format] controls the output style. Defaults to {!Block}.}
-
{- [indent] is the indentation width in spaces. Defaults to [2].}
-
{- [explicit_doc] if [true], emits explicit document markers
-
([---] and [...]). Defaults to [false].}
-
{- [scalar_style] is the preferred style for string scalars.
-
Defaults to [`Any] (auto-detect based on content).}
-
{- [eod] indicates whether {!Bytesrw.Bytes.Slice.eod} should be
-
written on [w] after encoding.}} *)
+
- If [buf] is specified, it is used as a buffer for output slices. Defaults
+
to a buffer of length {!Bytesrw.Bytes.Writer.slice_length}[ w].
+
- [format] controls the output style. Defaults to {!Block}.
+
- [indent] is the indentation width in spaces. Defaults to [2].
+
- [explicit_doc] if [true], emits explicit document markers ([---] and
+
[...]). Defaults to [false].
+
- [scalar_style] is the preferred style for string scalars. Defaults to
+
[`Any] (auto-detect based on content).
+
- [eod] indicates whether {!Bytesrw.Bytes.Slice.eod} should be written on
+
[w] after encoding. *)
val encode' :
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> 'a -> eod:bool -> Bytes.Writer.t -> (unit, Jsont.Error.t) result
+
?buf:Stdlib.Bytes.t ->
+
?format:yaml_format ->
+
?indent:int ->
+
?explicit_doc:bool ->
+
?scalar_style:Yamlrw.Scalar_style.t ->
+
'a Jsont.t ->
+
'a ->
+
eod:bool ->
+
Bytes.Writer.t ->
+
(unit, Jsont.Error.t) result
(** [encode'] is like {!val-encode} but preserves the error structure. *)
-
val encode_string :
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> 'a -> (string, string) result
-
(** [encode_string] is like {!val-encode} but writes to a string. *)
-
-
val encode_string' :
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> 'a -> (string, Jsont.Error.t) result
-
(** [encode_string'] is like {!val-encode'} but writes to a string. *)
-
(** {1:recode Recode}
The defaults in these functions are those of {!val-decode} and
-
{!val-encode}, except if [layout] is [true], [format] defaults to
-
{!Layout} and vice-versa. *)
+
{!val-encode}, except if [layout] is [true], [format] defaults to {!Layout}
+
and vice-versa. *)
val recode :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> Bytes.Reader.t -> Bytes.Writer.t -> eod:bool ->
+
?layout:bool ->
+
?locs:bool ->
+
?file:Jsont.Textloc.fpath ->
+
?max_depth:int ->
+
?max_nodes:int ->
+
?buf:Stdlib.Bytes.t ->
+
?format:yaml_format ->
+
?indent:int ->
+
?explicit_doc:bool ->
+
?scalar_style:Yamlrw.Scalar_style.t ->
+
'a Jsont.t ->
+
Bytes.Reader.t ->
+
Bytes.Writer.t ->
+
eod:bool ->
(unit, string) result
(** [recode t r w] is {!val-decode} followed by {!val-encode}. *)
-
val recode_string :
-
?layout:bool -> ?locs:bool -> ?file:Jsont.Textloc.fpath ->
-
?max_depth:int -> ?max_nodes:int ->
-
?buf:Stdlib.Bytes.t -> ?format:yaml_format -> ?indent:int ->
-
?explicit_doc:bool -> ?scalar_style:Yamlrw.Scalar_style.t ->
-
'a Jsont.t -> string -> (string, string) result
-
(** [recode_string] is like {!val-recode} but operates on strings. *)
-
(** {1:yaml_mapping YAML to JSON Mapping}
-
YAML is a superset of JSON. This module maps YAML structures to
-
the JSON data model that {!Jsont.t} describes:
+
YAML is a superset of JSON. This module maps YAML structures to the JSON
+
data model that {!Jsont.t} describes:
-
{ul
-
{- YAML scalars map to JSON null, boolean, number, or string
-
depending on content and the expected type}
-
{- YAML sequences map to JSON arrays}
-
{- YAML mappings map to JSON objects (keys must be strings)}
-
{- YAML aliases are resolved during decoding}
-
{- YAML tags are used to guide type resolution when present}}
+
- YAML scalars map to JSON null, boolean, number, or string depending on
+
content and the expected type
+
- YAML sequences map to JSON arrays
+
- YAML mappings map to JSON objects (keys must be strings)
+
- YAML aliases are resolved during decoding
+
- YAML tags are used to guide type resolution when present
{b Limitations:}
-
{ul
-
{- Only string keys are supported in mappings (JSON object compatibility)}
-
{- Anchors and aliases are resolved; the alias structure is not preserved}
-
{- Multi-document streams require {!decode_all}}} *)
+
- Only string keys are supported in mappings (JSON object compatibility)
+
- Anchors and aliases are resolved; the alias structure is not preserved
+
- Multi-document streams require {!decode_all} *)
(** {1:yaml_scalars YAML Scalar Resolution}
···
{b Null:} [null], [Null], [NULL], [~], or empty string
-
{b Boolean:} [true], [True], [TRUE], [false], [False], [FALSE],
-
[yes], [Yes], [YES], [no], [No], [NO], [on], [On], [ON],
-
[off], [Off], [OFF]
+
{b Boolean:} [true], [True], [TRUE], [false], [False], [FALSE], [yes],
+
[Yes], [YES], [no], [No], [NO], [on], [On], [ON], [off], [Off], [OFF]
{b Number:} Decimal integers, floats, hex ([0x...]), octal ([0o...]),
infinity ([.inf], [-.inf]), NaN ([.nan])
{b String:} Anything else, or explicitly quoted scalars
-
When decoding against a specific {!Jsont.t} type, the expected type
-
takes precedence over automatic resolution. For example, decoding
-
["yes"] against {!Jsont.string} yields the string ["yes"], not [true]. *)
+
When decoding against a specific {!Jsont.t} type, the expected type takes
+
precedence over automatic resolution. For example, decoding ["yes"] against
+
{!Jsont.string} yields the string ["yes"], not [true]. *)
+
+
(** {1:null_handling Null Value Handling}
+
+
YAML null values are handled according to the expected type to provide
+
friendly defaults while maintaining type safety:
+
+
{b Collections (Arrays and Objects):}
+
+
Null values decode as empty collections when the codec expects a collection
+
type. This provides convenient defaults for optional collection fields in
+
YAML:
+
{[
+
# YAML with null collection fields
+
config:
+
items: null # Decodes as []
+
settings: ~ # Decodes as {}
+
tags: # Missing value = null, decodes as []
+
]}
+
+
For arrays, null decodes to an empty array. For objects, null decodes to an
+
object with all fields set to their [dec_absent] defaults. If any required
+
field lacks a default, decoding fails with a missing member error.
+
+
This behavior makes yamlt more forgiving for schemas with many optional
+
collection fields, where writing [field:] (which parses as null) is natural
+
and semantically equivalent to [field: []].
+
+
{b Numbers:}
+
+
Null values decode to [Float.nan] when the codec expects a number.
+
+
{b Primitive Types (Int, Bool, String):}
+
+
Null values {e fail} when decoding into primitive scalar types ([int],
+
[bool], [string]). Null typically indicates genuinely missing or incorrect
+
data for these types, and silent conversion could clash with a manual
+
setting of the default value (e.g. 0 and [null] for an integer would be
+
indistinguishable).
+
+
To accept null for primitive fields, explicitly use {!Jsont.option}:
+
{[
+
(* Accepts null, decodes as None *)
+
Jsont.Object.mem "count" (Jsont.option Jsont.int) ~dec_absent:None
+
+
(* Rejects null, requires a number *)
+
Jsont.Object.mem "count" Jsont.int ~dec_absent:0
+
]}
+
+
*)
+27 -1
tests/bin/dune
···
(name test_edge)
(public_name test_edge)
(libraries yamlt jsont jsont.bytesrw bytesrw))
-
(executable (name test_null_fix) (libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_null_fix)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
(executable
(name test_null_complete)
(libraries yamlt jsont jsont.bytesrw bytesrw))
(executable
+
(name test_null_collections)
+
(public_name test_null_collections)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
(name test_opt_array)
(libraries yamlt jsont jsont.bytesrw bytesrw))
···
(executable
(name test_some_vs_option)
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_comprehensive)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_flow_newline)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_locations)
+
(public_name test_locations)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_multidoc)
+
(public_name test_multidoc)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+16 -11
tests/bin/test_array_variants.ml
···
+
open Bytesrw
+
let () =
let codec1 =
Jsont.Object.map ~kind:"Test" (fun arr -> arr)
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.string) ~enc:(fun arr -> arr)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.string) ~enc:(fun arr ->
+
arr)
|> Jsont.Object.finish
in
let yaml1 = "values: [a, b, c]" in
Printf.printf "Test 1: Non-optional array:\n";
-
(match Yamlt.decode_string codec1 yaml1 with
-
| Ok arr -> Printf.printf "Result: [%d items]\n" (Array.length arr)
-
| Error e -> Printf.printf "Error: %s\n" e);
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml1) with
+
| Ok arr -> Printf.printf "Result: [%d items]\n" (Array.length arr)
+
| Error e -> Printf.printf "Error: %s\n" e);
let codec2 =
Jsont.Object.map ~kind:"Test" (fun arr -> arr)
-
|> Jsont.Object.mem "values" (Jsont.option (Jsont.array Jsont.string)) ~enc:(fun arr -> arr)
+
|> Jsont.Object.mem "values"
+
(Jsont.option (Jsont.array Jsont.string))
+
~enc:(fun arr -> arr)
|> Jsont.Object.finish
in
Printf.printf "\nTest 2: Jsont.option (Jsont.array):\n";
-
(match Yamlt.decode_string codec2 yaml1 with
-
| Ok arr ->
-
(match arr with
-
| None -> Printf.printf "Result: None\n"
-
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
-
| Error e -> Printf.printf "Error: %s\n" e)
+
match Yamlt.decode codec2 (Bytes.Reader.of_string yaml1) with
+
| Ok arr -> (
+
match arr with
+
| None -> Printf.printf "Result: None\n"
+
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
+
| Error e -> Printf.printf "Error: %s\n" e
+88 -92
tests/bin/test_arrays.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test array codec functionality with Yamlt *)
+
open Bytesrw
(* Helper to read file *)
let read_file path =
···
(* Test: Simple int array *)
let test_int_array file =
let module M = struct
-
type numbers = { values: int array }
+
type numbers = { values : int array }
let numbers_codec =
Jsont.Object.map ~kind:"Numbers" (fun values -> { values })
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun n -> n.values)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun n ->
+
n.values)
|> Jsont.Object.finish
let show n =
-
Printf.sprintf "[%s]" (String.concat "; " (Array.to_list (Array.map string_of_int n.values)))
+
Printf.sprintf "[%s]"
+
(String.concat "; " (Array.to_list (Array.map string_of_int n.values)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.numbers_codec json in
-
let yaml_result = Yamlt.decode_string M.numbers_codec yaml in
+
let yaml_result = Yamlt.decode M.numbers_codec (Bytes.Reader.of_string yaml) in
show_result_both "int_array"
(Result.map M.show json_result)
···
(* Test: String array *)
let test_string_array file =
let module M = struct
-
type tags = { items: string array }
+
type tags = { items : string array }
let tags_codec =
Jsont.Object.map ~kind:"Tags" (fun items -> { items })
-
|> Jsont.Object.mem "items" (Jsont.array Jsont.string) ~enc:(fun t -> t.items)
+
|> Jsont.Object.mem "items" (Jsont.array Jsont.string) ~enc:(fun t ->
+
t.items)
|> Jsont.Object.finish
let show t =
-
Printf.sprintf "[%s]" (String.concat "; " (Array.to_list (Array.map (Printf.sprintf "%S") t.items)))
+
Printf.sprintf "[%s]"
+
(String.concat "; "
+
(Array.to_list (Array.map (Printf.sprintf "%S") t.items)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.tags_codec json in
-
let yaml_result = Yamlt.decode_string M.tags_codec yaml in
+
let yaml_result = Yamlt.decode M.tags_codec (Bytes.Reader.of_string yaml) in
show_result_both "string_array"
(Result.map M.show json_result)
···
(* Test: Float/number array *)
let test_float_array file =
let module M = struct
-
type measurements = { values: float array }
+
type measurements = { values : float array }
let measurements_codec =
Jsont.Object.map ~kind:"Measurements" (fun values -> { values })
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.number) ~enc:(fun m -> m.values)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.number) ~enc:(fun m ->
+
m.values)
|> Jsont.Object.finish
let show m =
Printf.sprintf "[%s]"
-
(String.concat "; " (Array.to_list (Array.map (Printf.sprintf "%.2f") m.values)))
+
(String.concat "; "
+
(Array.to_list (Array.map (Printf.sprintf "%.2f") m.values)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.measurements_codec json in
-
let yaml_result = Yamlt.decode_string M.measurements_codec yaml in
+
let yaml_result = Yamlt.decode M.measurements_codec (Bytes.Reader.of_string yaml) in
show_result_both "float_array"
(Result.map M.show json_result)
···
(* Test: Empty array *)
let test_empty_array file =
let module M = struct
-
type empty = { items: int array }
+
type empty = { items : int array }
let empty_codec =
Jsont.Object.map ~kind:"Empty" (fun items -> { items })
-
|> Jsont.Object.mem "items" (Jsont.array Jsont.int) ~enc:(fun e -> e.items)
+
|> Jsont.Object.mem "items" (Jsont.array Jsont.int) ~enc:(fun e ->
+
e.items)
|> Jsont.Object.finish
-
let show e =
-
Printf.sprintf "length=%d" (Stdlib.Array.length e.items)
+
let show e = Printf.sprintf "length=%d" (Stdlib.Array.length e.items)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.empty_codec json in
-
let yaml_result = Yamlt.decode_string M.empty_codec yaml in
+
let yaml_result = Yamlt.decode M.empty_codec (Bytes.Reader.of_string yaml) in
show_result_both "empty_array"
(Result.map M.show json_result)
···
(* Test: Array of objects *)
let test_object_array file =
let module M = struct
-
type person = { name: string; age: int }
-
type people = { persons: person array }
+
type person = { name : string; age : int }
+
type people = { persons : person array }
let person_codec =
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
···
let people_codec =
Jsont.Object.map ~kind:"People" (fun persons -> { persons })
-
|> Jsont.Object.mem "persons" (Jsont.array person_codec) ~enc:(fun p -> p.persons)
+
|> Jsont.Object.mem "persons" (Jsont.array person_codec) ~enc:(fun p ->
+
p.persons)
|> Jsont.Object.finish
let show_person p = Printf.sprintf "{%s,%d}" p.name p.age
+
let show ps =
Printf.sprintf "[%s]"
(String.concat "; " (Array.to_list (Array.map show_person ps.persons)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.people_codec json in
-
let yaml_result = Yamlt.decode_string M.people_codec yaml in
+
let yaml_result = Yamlt.decode M.people_codec (Bytes.Reader.of_string yaml) in
show_result_both "object_array"
(Result.map M.show json_result)
···
(* Test: Nested arrays *)
let test_nested_arrays file =
let module M = struct
-
type matrix = { data: int array array }
+
type matrix = { data : int array array }
let matrix_codec =
Jsont.Object.map ~kind:"Matrix" (fun data -> { data })
-
|> Jsont.Object.mem "data" (Jsont.array (Jsont.array Jsont.int))
-
~enc:(fun m -> m.data)
+
|> Jsont.Object.mem "data"
+
(Jsont.array (Jsont.array Jsont.int))
+
~enc:(fun m -> m.data)
|> Jsont.Object.finish
let show_row row =
-
Printf.sprintf "[%s]" (String.concat "; " (Array.to_list (Array.map string_of_int row)))
+
Printf.sprintf "[%s]"
+
(String.concat "; " (Array.to_list (Array.map string_of_int row)))
let show m =
-
Printf.sprintf "[%s]" (String.concat "; " (Array.to_list (Array.map show_row m.data)))
+
Printf.sprintf "[%s]"
+
(String.concat "; " (Array.to_list (Array.map show_row m.data)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.matrix_codec json in
-
let yaml_result = Yamlt.decode_string M.matrix_codec yaml in
+
let yaml_result = Yamlt.decode M.matrix_codec (Bytes.Reader.of_string yaml) in
show_result_both "nested_arrays"
(Result.map M.show json_result)
···
(* Test: Mixed types in array (should fail with homogeneous codec) *)
let test_type_mismatch file =
let module M = struct
-
type numbers = { values: int array }
+
type numbers = { values : int array }
let numbers_codec =
Jsont.Object.map ~kind:"Numbers" (fun values -> { values })
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun n -> n.values)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun n ->
+
n.values)
|> Jsont.Object.finish
end in
-
let yaml = read_file file in
-
let result = Yamlt.decode_string M.numbers_codec yaml in
+
let result = Yamlt.decode M.numbers_codec (Bytes.Reader.of_string yaml) in
match result with
| Ok _ -> Printf.printf "Unexpected success\n"
| Error e -> Printf.printf "Expected error: %s\n" e
···
(* Test: Bool array *)
let test_bool_array file =
let module M = struct
-
type flags = { values: bool array }
+
type flags = { values : bool array }
let flags_codec =
Jsont.Object.map ~kind:"Flags" (fun values -> { values })
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.bool) ~enc:(fun f -> f.values)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.bool) ~enc:(fun f ->
+
f.values)
|> Jsont.Object.finish
let show f =
Printf.sprintf "[%s]"
(String.concat "; " (Array.to_list (Array.map string_of_bool f.values)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.flags_codec json in
-
let yaml_result = Yamlt.decode_string M.flags_codec yaml in
+
let yaml_result = Yamlt.decode M.flags_codec (Bytes.Reader.of_string yaml) in
show_result_both "bool_array"
(Result.map M.show json_result)
···
(* Test: Array with nulls *)
let test_nullable_array file =
let module M = struct
-
type nullable = { values: string option array }
+
type nullable = { values : string option array }
let nullable_codec =
Jsont.Object.map ~kind:"Nullable" (fun values -> { values })
-
|> Jsont.Object.mem "values" (Jsont.array (Jsont.some Jsont.string))
-
~enc:(fun n -> n.values)
+
|> Jsont.Object.mem "values"
+
(Jsont.array (Jsont.some Jsont.string))
+
~enc:(fun n -> n.values)
|> Jsont.Object.finish
-
let show_opt = function
-
| None -> "null"
-
| Some s -> Printf.sprintf "%S" s
+
let show_opt = function None -> "null" | Some s -> Printf.sprintf "%S" s
let show n =
-
Printf.sprintf "[%s]" (String.concat "; " (Array.to_list (Array.map show_opt n.values)))
+
Printf.sprintf "[%s]"
+
(String.concat "; " (Array.to_list (Array.map show_opt n.values)))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.nullable_codec json in
-
let yaml_result = Yamlt.decode_string M.nullable_codec yaml in
+
let yaml_result = Yamlt.decode M.nullable_codec (Bytes.Reader.of_string yaml) in
show_result_both "nullable_array"
(Result.map M.show json_result)
···
(* Test: Encoding arrays to different formats *)
let test_encode_arrays () =
let module M = struct
-
type data = { numbers: int array; strings: string array }
+
type data = { numbers : int array; strings : string array }
let data_codec =
-
Jsont.Object.map ~kind:"Data" (fun numbers strings -> { numbers; strings })
-
|> Jsont.Object.mem "numbers" (Jsont.array Jsont.int) ~enc:(fun d -> d.numbers)
-
|> Jsont.Object.mem "strings" (Jsont.array Jsont.string) ~enc:(fun d -> d.strings)
+
Jsont.Object.map ~kind:"Data" (fun numbers strings ->
+
{ numbers; strings })
+
|> Jsont.Object.mem "numbers" (Jsont.array Jsont.int) ~enc:(fun d ->
+
d.numbers)
+
|> Jsont.Object.mem "strings" (Jsont.array Jsont.string) ~enc:(fun d ->
+
d.strings)
|> Jsont.Object.finish
end in
-
-
let data = { M.numbers = [|1; 2; 3; 4; 5|]; strings = [|"hello"; "world"|] } in
+
let data =
+
{ M.numbers = [| 1; 2; 3; 4; 5 |]; strings = [| "hello"; "world" |] }
+
in
(* Encode to JSON *)
(match Jsont_bytesrw.encode_string M.data_codec data with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
(* Encode to YAML Block *)
-
(match Yamlt.encode_string ~format:Yamlt.Block M.data_codec data with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block M.data_codec data ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
(* Encode to YAML Flow *)
-
(match Yamlt.encode_string ~format:Yamlt.Flow M.data_codec data with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow M.data_codec data ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e
let () =
let usage = "Usage: test_arrays <command> [args...]" in
···
end;
match Sys.argv.(1) with
-
| "int" when Array.length Sys.argv = 3 ->
-
test_int_array Sys.argv.(2)
-
-
| "string" when Array.length Sys.argv = 3 ->
-
test_string_array Sys.argv.(2)
-
-
| "float" when Array.length Sys.argv = 3 ->
-
test_float_array Sys.argv.(2)
-
-
| "empty" when Array.length Sys.argv = 3 ->
-
test_empty_array Sys.argv.(2)
-
-
| "objects" when Array.length Sys.argv = 3 ->
-
test_object_array Sys.argv.(2)
-
-
| "nested" when Array.length Sys.argv = 3 ->
-
test_nested_arrays Sys.argv.(2)
-
+
| "int" when Array.length Sys.argv = 3 -> test_int_array Sys.argv.(2)
+
| "string" when Array.length Sys.argv = 3 -> test_string_array Sys.argv.(2)
+
| "float" when Array.length Sys.argv = 3 -> test_float_array Sys.argv.(2)
+
| "empty" when Array.length Sys.argv = 3 -> test_empty_array Sys.argv.(2)
+
| "objects" when Array.length Sys.argv = 3 -> test_object_array Sys.argv.(2)
+
| "nested" when Array.length Sys.argv = 3 -> test_nested_arrays Sys.argv.(2)
| "type-mismatch" when Array.length Sys.argv = 3 ->
test_type_mismatch Sys.argv.(2)
-
-
| "bool" when Array.length Sys.argv = 3 ->
-
test_bool_array Sys.argv.(2)
-
+
| "bool" when Array.length Sys.argv = 3 -> test_bool_array Sys.argv.(2)
| "nullable" when Array.length Sys.argv = 3 ->
test_nullable_array Sys.argv.(2)
-
-
| "encode" when Array.length Sys.argv = 2 ->
-
test_encode_arrays ()
-
+
| "encode" when Array.length Sys.argv = 2 -> test_encode_arrays ()
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
+58 -45
tests/bin/test_complex.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test complex nested types with Yamlt *)
+
+
open Bytesrw
(* Helper to read file *)
let read_file path =
···
(* Test: Deeply nested objects *)
let test_deep_nesting file =
let module M = struct
-
type level3 = { value: int }
-
type level2 = { data: level3 }
-
type level1 = { nested: level2 }
-
type root = { top: level1 }
+
type level3 = { value : int }
+
type level2 = { data : level3 }
+
type level1 = { nested : level2 }
+
type root = { top : level1 }
let level3_codec =
Jsont.Object.map ~kind:"Level3" (fun value -> { value })
···
let show r = Printf.sprintf "depth=4, value=%d" r.top.nested.data.value
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.root_codec json in
-
let yaml_result = Yamlt.decode_string M.root_codec yaml in
+
let yaml_result = Yamlt.decode M.root_codec (Bytes.Reader.of_string yaml) in
show_result_both "deep_nesting"
(Result.map M.show json_result)
···
(* Test: Array of objects with nested arrays *)
let test_mixed_structure file =
let module M = struct
-
type item = { id: int; tags: string array }
-
type collection = { name: string; items: item array }
+
type item = { id : int; tags : string array }
+
type collection = { name : string; items : item array }
let item_codec =
Jsont.Object.map ~kind:"Item" (fun id tags -> { id; tags })
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun i -> i.id)
-
|> Jsont.Object.mem "tags" (Jsont.array Jsont.string) ~enc:(fun i -> i.tags)
+
|> Jsont.Object.mem "tags" (Jsont.array Jsont.string) ~enc:(fun i ->
+
i.tags)
|> Jsont.Object.finish
let collection_codec =
Jsont.Object.map ~kind:"Collection" (fun name items -> { name; items })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name)
-
|> Jsont.Object.mem "items" (Jsont.array item_codec) ~enc:(fun c -> c.items)
+
|> Jsont.Object.mem "items" (Jsont.array item_codec) ~enc:(fun c ->
+
c.items)
|> Jsont.Object.finish
let show c =
-
let total_tags = Stdlib.Array.fold_left (fun acc item ->
-
acc + Stdlib.Array.length item.tags) 0 c.items in
-
Printf.sprintf "name=%S, items=%d, total_tags=%d"
-
c.name (Stdlib.Array.length c.items) total_tags
+
let total_tags =
+
Stdlib.Array.fold_left
+
(fun acc item -> acc + Stdlib.Array.length item.tags)
+
0 c.items
+
in
+
Printf.sprintf "name=%S, items=%d, total_tags=%d" c.name
+
(Stdlib.Array.length c.items)
+
total_tags
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.collection_codec json in
-
let yaml_result = Yamlt.decode_string M.collection_codec yaml in
+
let yaml_result = Yamlt.decode M.collection_codec (Bytes.Reader.of_string yaml) in
show_result_both "mixed_structure"
(Result.map M.show json_result)
···
let test_complex_optional file =
let module M = struct
type config = {
-
host: string;
-
port: int option;
-
ssl: bool option;
-
cert_path: string option;
-
fallback_hosts: string array option;
+
host : string;
+
port : int option;
+
ssl : bool option;
+
cert_path : string option;
+
fallback_hosts : string array option;
}
let config_codec =
···
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> c.host)
|> Jsont.Object.opt_mem "port" Jsont.int ~enc:(fun c -> c.port)
|> Jsont.Object.opt_mem "ssl" Jsont.bool ~enc:(fun c -> c.ssl)
-
|> Jsont.Object.opt_mem "cert_path" Jsont.string ~enc:(fun c -> c.cert_path)
+
|> Jsont.Object.opt_mem "cert_path" Jsont.string ~enc:(fun c ->
+
c.cert_path)
|> Jsont.Object.opt_mem "fallback_hosts" (Jsont.array Jsont.string)
-
~enc:(fun c -> c.fallback_hosts)
+
~enc:(fun c -> c.fallback_hosts)
|> Jsont.Object.finish
let show c =
-
let port_str = match c.port with None -> "None" | Some p -> string_of_int p in
-
let ssl_str = match c.ssl with None -> "None" | Some b -> string_of_bool b in
-
let fallbacks = match c.fallback_hosts with
+
let port_str =
+
match c.port with None -> "None" | Some p -> string_of_int p
+
in
+
let ssl_str =
+
match c.ssl with None -> "None" | Some b -> string_of_bool b
+
in
+
let fallbacks =
+
match c.fallback_hosts with
| None -> 0
-
| Some arr -> Stdlib.Array.length arr in
-
Printf.sprintf "host=%S, port=%s, ssl=%s, fallbacks=%d"
-
c.host port_str ssl_str fallbacks
+
| Some arr -> Stdlib.Array.length arr
+
in
+
Printf.sprintf "host=%S, port=%s, ssl=%s, fallbacks=%d" c.host port_str
+
ssl_str fallbacks
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.config_codec json in
-
let yaml_result = Yamlt.decode_string M.config_codec yaml in
+
let yaml_result = Yamlt.decode M.config_codec (Bytes.Reader.of_string yaml) in
show_result_both "complex_optional"
(Result.map M.show json_result)
···
(* Test: Heterogeneous data via any type *)
let test_heterogeneous file =
let module M = struct
-
type data = { mixed: Jsont.json array }
+
type data = { mixed : Jsont.json array }
let data_codec =
Jsont.Object.map ~kind:"Data" (fun mixed -> { mixed })
-
|> Jsont.Object.mem "mixed" (Jsont.array (Jsont.any ())) ~enc:(fun d -> d.mixed)
+
|> Jsont.Object.mem "mixed"
+
(Jsont.array (Jsont.any ()))
+
~enc:(fun d -> d.mixed)
|> Jsont.Object.finish
let show d = Printf.sprintf "items=%d" (Stdlib.Array.length d.mixed)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.data_codec json in
-
let yaml_result = Yamlt.decode_string M.data_codec yaml in
+
let yaml_result = Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml) in
show_result_both "heterogeneous"
(Result.map M.show json_result)
···
match Sys.argv.(1) with
| "deep-nesting" when Stdlib.Array.length Sys.argv = 3 ->
test_deep_nesting Sys.argv.(2)
-
| "mixed-structure" when Stdlib.Array.length Sys.argv = 3 ->
test_mixed_structure Sys.argv.(2)
-
| "complex-optional" when Stdlib.Array.length Sys.argv = 3 ->
test_complex_optional Sys.argv.(2)
-
| "heterogeneous" when Stdlib.Array.length Sys.argv = 3 ->
test_heterogeneous Sys.argv.(2)
-
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
prerr_endline " deep-nesting <file> - Test deeply nested objects";
-
prerr_endline " mixed-structure <file> - Test arrays of objects with nested arrays";
-
prerr_endline " complex-optional <file> - Test complex optional/nullable combinations";
-
prerr_endline " heterogeneous <file> - Test heterogeneous data via any type";
+
prerr_endline
+
" mixed-structure <file> - Test arrays of objects with nested arrays";
+
prerr_endline
+
" complex-optional <file> - Test complex optional/nullable \
+
combinations";
+
prerr_endline
+
" heterogeneous <file> - Test heterogeneous data via any type";
exit 1
+96
tests/bin/test_comprehensive.ml
···
+
open Bytesrw
+
+
let () =
+
(* Test 1: Null handling with option types *)
+
Printf.printf "=== NULL HANDLING ===\n";
+
let opt_codec =
+
Jsont.Object.map ~kind:"Test" (fun v -> v)
+
|> Jsont.Object.mem "value" (Jsont.option Jsont.string) ~enc:(fun v -> v)
+
|> Jsont.Object.finish
+
in
+
+
(match Yamlt.decode opt_codec (Bytes.Reader.of_string "value: null") with
+
| Ok None -> Printf.printf "โœ“ Plain 'null' with option codec: None\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode opt_codec (Bytes.Reader.of_string "value: hello") with
+
| Ok (Some "hello") ->
+
Printf.printf "โœ“ Plain 'hello' with option codec: Some(hello)\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
let string_codec =
+
Jsont.Object.map ~kind:"Test" (fun v -> v)
+
|> Jsont.Object.mem "value" Jsont.string ~enc:(fun v -> v)
+
|> Jsont.Object.finish
+
in
+
+
(match Yamlt.decode string_codec (Bytes.Reader.of_string "value: null") with
+
| Error _ ->
+
Printf.printf "โœ“ Plain 'null' with string codec: ERROR (expected)\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode string_codec (Bytes.Reader.of_string "value: \"\"") with
+
| Ok "" -> Printf.printf "โœ“ Quoted empty string: \"\"\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode string_codec (Bytes.Reader.of_string "value: \"null\"") with
+
| Ok "null" -> Printf.printf "โœ“ Quoted 'null': \"null\"\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(* Test 2: Number formats *)
+
Printf.printf "\n=== NUMBER FORMATS ===\n";
+
let num_codec =
+
Jsont.Object.map ~kind:"Test" (fun v -> v)
+
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun v -> v)
+
|> Jsont.Object.finish
+
in
+
+
(match Yamlt.decode num_codec (Bytes.Reader.of_string "value: 0xFF") with
+
| Ok 255. -> Printf.printf "โœ“ Hex 0xFF: 255\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode num_codec (Bytes.Reader.of_string "value: 0o77") with
+
| Ok 63. -> Printf.printf "โœ“ Octal 0o77: 63\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode num_codec (Bytes.Reader.of_string "value: 0b1010") with
+
| Ok 10. -> Printf.printf "โœ“ Binary 0b1010: 10\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(* Test 3: Optional arrays *)
+
Printf.printf "\n=== OPTIONAL ARRAYS ===\n";
+
let opt_array_codec =
+
Jsont.Object.map ~kind:"Test" (fun v -> v)
+
|> Jsont.Object.opt_mem "values" (Jsont.array Jsont.string) ~enc:(fun v ->
+
v)
+
|> Jsont.Object.finish
+
in
+
+
(match Yamlt.decode opt_array_codec (Bytes.Reader.of_string "values: [a, b, c]") with
+
| Ok (Some arr) when Array.length arr = 3 ->
+
Printf.printf "โœ“ Optional array [a, b, c]: Some([3 items])\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(match Yamlt.decode opt_array_codec (Bytes.Reader.of_string "{}") with
+
| Ok None -> Printf.printf "โœ“ Missing optional array: None\n"
+
| _ -> Printf.printf "โœ— FAIL\n");
+
+
(* Test 4: Flow encoding *)
+
Printf.printf "\n=== FLOW ENCODING ===\n";
+
let encode_codec =
+
Jsont.Object.map ~kind:"Test" (fun name values -> (name, values))
+
|> Jsont.Object.mem "name" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.number) ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match
+
Yamlt.encode ~format:Flow encode_codec ("test", [| 1.; 2.; 3. |]) ~eod:true writer
+
with
+
| Ok ()
+
when String.equal (Buffer.contents b) "{name: test, values: [1.0, 2.0, 3.0]}\n" ->
+
Printf.printf "โœ“ Flow encoding with comma separator\n"
+
| Ok () -> Printf.printf "โœ— FAIL: %S\n" (Buffer.contents b)
+
| Error e -> Printf.printf "โœ— ERROR: %s\n" e
+40 -38
tests/bin/test_edge.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test edge cases with Yamlt *)
+
+
open Bytesrw
(* Helper to read file *)
let read_file path =
···
(* Test: Very large numbers *)
let test_large_numbers file =
let module M = struct
-
type numbers = { large_int: float; large_float: float; small_float: float }
+
type numbers = {
+
large_int : float;
+
large_float : float;
+
small_float : float;
+
}
let numbers_codec =
Jsont.Object.map ~kind:"Numbers" (fun large_int large_float small_float ->
-
{ large_int; large_float; small_float })
+
{ large_int; large_float; small_float })
|> Jsont.Object.mem "large_int" Jsont.number ~enc:(fun n -> n.large_int)
-
|> Jsont.Object.mem "large_float" Jsont.number ~enc:(fun n -> n.large_float)
-
|> Jsont.Object.mem "small_float" Jsont.number ~enc:(fun n -> n.small_float)
+
|> Jsont.Object.mem "large_float" Jsont.number ~enc:(fun n ->
+
n.large_float)
+
|> Jsont.Object.mem "small_float" Jsont.number ~enc:(fun n ->
+
n.small_float)
|> Jsont.Object.finish
let show n =
Printf.sprintf "large_int=%.0f, large_float=%e, small_float=%e"
n.large_int n.large_float n.small_float
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.numbers_codec json in
-
let yaml_result = Yamlt.decode_string M.numbers_codec yaml in
+
let yaml_result = Yamlt.decode M.numbers_codec (Bytes.Reader.of_string yaml) in
show_result_both "large_numbers"
(Result.map M.show json_result)
···
(* Test: Special characters in strings *)
let test_special_chars file =
let module M = struct
-
type text = { content: string }
+
type text = { content : string }
let text_codec =
Jsont.Object.map ~kind:"Text" (fun content -> { content })
···
(String.contains t.content '\n')
(String.contains t.content '\t')
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.text_codec json in
-
let yaml_result = Yamlt.decode_string M.text_codec yaml in
+
let yaml_result = Yamlt.decode M.text_codec (Bytes.Reader.of_string yaml) in
show_result_both "special_chars"
(Result.map M.show json_result)
···
(* Test: Unicode strings *)
let test_unicode file =
let module M = struct
-
type text = { emoji: string; chinese: string; rtl: string }
+
type text = { emoji : string; chinese : string; rtl : string }
let text_codec =
-
Jsont.Object.map ~kind:"Text" (fun emoji chinese rtl -> { emoji; chinese; rtl })
+
Jsont.Object.map ~kind:"Text" (fun emoji chinese rtl ->
+
{ emoji; chinese; rtl })
|> Jsont.Object.mem "emoji" Jsont.string ~enc:(fun t -> t.emoji)
|> Jsont.Object.mem "chinese" Jsont.string ~enc:(fun t -> t.chinese)
|> Jsont.Object.mem "rtl" Jsont.string ~enc:(fun t -> t.rtl)
···
let show t =
Printf.sprintf "emoji=%S, chinese=%S, rtl=%S" t.emoji t.chinese t.rtl
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.text_codec json in
-
let yaml_result = Yamlt.decode_string M.text_codec yaml in
+
let yaml_result = Yamlt.decode M.text_codec (Bytes.Reader.of_string yaml) in
show_result_both "unicode"
(Result.map M.show json_result)
···
(* Test: Empty collections *)
let test_empty_collections file =
let module M = struct
-
type data = { empty_array: int array; empty_object_array: unit array }
+
type data = { empty_array : int array; empty_object_array : unit array }
let data_codec =
Jsont.Object.map ~kind:"Data" (fun empty_array empty_object_array ->
-
{ empty_array; empty_object_array })
-
|> Jsont.Object.mem "empty_array" (Jsont.array Jsont.int) ~enc:(fun d -> d.empty_array)
-
|> Jsont.Object.mem "empty_object_array" (Jsont.array (Jsont.null ())) ~enc:(fun d -> d.empty_object_array)
+
{ empty_array; empty_object_array })
+
|> Jsont.Object.mem "empty_array" (Jsont.array Jsont.int) ~enc:(fun d ->
+
d.empty_array)
+
|> Jsont.Object.mem "empty_object_array"
+
(Jsont.array (Jsont.null ()))
+
~enc:(fun d -> d.empty_object_array)
|> Jsont.Object.finish
let show d =
···
(Stdlib.Array.length d.empty_array)
(Stdlib.Array.length d.empty_object_array)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.data_codec json in
-
let yaml_result = Yamlt.decode_string M.data_codec yaml in
+
let yaml_result = Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml) in
show_result_both "empty_collections"
(Result.map M.show json_result)
···
| Ok _ -> "not_object"
| Error _ -> "decode_error"
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string (Jsont.any ()) json in
-
let yaml_result = Yamlt.decode_string (Jsont.any ()) yaml in
+
let yaml_result = Yamlt.decode (Jsont.any ()) (Bytes.Reader.of_string yaml) in
show_result_both "special_keys"
(Result.map M.show json_result)
···
(* Test: Single-element arrays *)
let test_single_element file =
let module M = struct
-
type data = { single: int array }
+
type data = { single : int array }
let data_codec =
Jsont.Object.map ~kind:"Data" (fun single -> { single })
-
|> Jsont.Object.mem "single" (Jsont.array Jsont.int) ~enc:(fun d -> d.single)
+
|> Jsont.Object.mem "single" (Jsont.array Jsont.int) ~enc:(fun d ->
+
d.single)
|> Jsont.Object.finish
let show d =
···
(Stdlib.Array.length d.single)
(if Stdlib.Array.length d.single > 0 then d.single.(0) else 0)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.data_codec json in
-
let yaml_result = Yamlt.decode_string M.data_codec yaml in
+
let yaml_result = Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml) in
show_result_both "single_element"
(Result.map M.show json_result)
···
match Sys.argv.(1) with
| "large-numbers" when Stdlib.Array.length Sys.argv = 3 ->
test_large_numbers Sys.argv.(2)
-
| "special-chars" when Stdlib.Array.length Sys.argv = 3 ->
test_special_chars Sys.argv.(2)
-
-
| "unicode" when Stdlib.Array.length Sys.argv = 3 ->
-
test_unicode Sys.argv.(2)
-
+
| "unicode" when Stdlib.Array.length Sys.argv = 3 -> test_unicode Sys.argv.(2)
| "empty-collections" when Stdlib.Array.length Sys.argv = 3 ->
test_empty_collections Sys.argv.(2)
-
| "special-keys" when Stdlib.Array.length Sys.argv = 3 ->
test_special_keys Sys.argv.(2)
-
| "single-element" when Stdlib.Array.length Sys.argv = 3 ->
test_single_element Sys.argv.(2)
-
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
prerr_endline " large-numbers <file> - Test very large numbers";
-
prerr_endline " special-chars <file> - Test special characters in strings";
+
prerr_endline
+
" special-chars <file> - Test special characters in strings";
prerr_endline " unicode <file> - Test Unicode strings";
prerr_endline " empty-collections <file> - Test empty collections";
-
prerr_endline " special-keys <file> - Test special characters in keys";
+
prerr_endline
+
" special-keys <file> - Test special characters in keys";
prerr_endline " single-element <file> - Test single-element arrays";
exit 1
+21
tests/bin/test_flow_newline.ml
···
+
open Bytesrw
+
+
let () =
+
let encode_codec =
+
Jsont.Object.map ~kind:"Test" (fun name values -> (name, values))
+
|> Jsont.Object.mem "name" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.number) ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match
+
Yamlt.encode ~format:Flow encode_codec ("test", [| 1.; 2.; 3. |]) ~eod:true writer
+
with
+
| Ok () ->
+
let yaml_flow = Buffer.contents b in
+
Printf.printf "Length: %d\n" (String.length yaml_flow);
+
Printf.printf "Repr: %S\n" yaml_flow;
+
Printf.printf "Output:\n%s" yaml_flow
+
| Error e -> Printf.printf "Error: %s\n" e
+52 -58
tests/bin/test_formats.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test format-specific features with Yamlt *)
+
open Bytesrw
+
(* Helper to read file *)
let read_file path =
let ic = open_in path in
···
(* Test: Multi-line strings - literal style *)
let test_literal_string file =
let module M = struct
-
type text = { content: string }
+
type text = { content : string }
let text_codec =
Jsont.Object.map ~kind:"Text" (fun content -> { content })
···
(List.length (String.split_on_char '\n' t.content))
(String.length t.content)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.text_codec json in
-
let yaml_result = Yamlt.decode_string M.text_codec yaml in
+
let yaml_result = Yamlt.decode M.text_codec (Bytes.Reader.of_string yaml) in
show_result_both "literal_string"
(Result.map M.show json_result)
···
(* Test: Multi-line strings - folded style *)
let test_folded_string file =
let module M = struct
-
type text = { content: string }
+
type text = { content : string }
let text_codec =
Jsont.Object.map ~kind:"Text" (fun content -> { content })
···
|> Jsont.Object.finish
let show t =
-
Printf.sprintf "length=%d, newlines=%d"
-
(String.length t.content)
-
(List.length (List.filter (fun c -> c = '\n')
-
(List.init (String.length t.content) (String.get t.content))))
+
Printf.sprintf "length=%d, newlines=%d" (String.length t.content)
+
(List.length
+
(List.filter
+
(fun c -> c = '\n')
+
(List.init (String.length t.content) (String.get t.content))))
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.text_codec json in
-
let yaml_result = Yamlt.decode_string M.text_codec yaml in
+
let yaml_result = Yamlt.decode M.text_codec (Bytes.Reader.of_string yaml) in
show_result_both "folded_string"
(Result.map M.show json_result)
···
(* Test: Number formats - hex, octal, binary *)
let test_number_formats file =
let module M = struct
-
type numbers = { hex: float; octal: float; binary: float }
+
type numbers = { hex : float; octal : float; binary : float }
let numbers_codec =
-
Jsont.Object.map ~kind:"Numbers" (fun hex octal binary -> { hex; octal; binary })
+
Jsont.Object.map ~kind:"Numbers" (fun hex octal binary ->
+
{ hex; octal; binary })
|> Jsont.Object.mem "hex" Jsont.number ~enc:(fun n -> n.hex)
|> Jsont.Object.mem "octal" Jsont.number ~enc:(fun n -> n.octal)
|> Jsont.Object.mem "binary" Jsont.number ~enc:(fun n -> n.binary)
···
let show n =
Printf.sprintf "hex=%.0f, octal=%.0f, binary=%.0f" n.hex n.octal n.binary
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.numbers_codec json in
-
let yaml_result = Yamlt.decode_string M.numbers_codec yaml in
+
let yaml_result = Yamlt.decode M.numbers_codec (Bytes.Reader.of_string yaml) in
show_result_both "number_formats"
(Result.map M.show json_result)
···
(* Test: Block vs Flow style encoding *)
let test_encode_styles () =
let module M = struct
-
type data = {
-
name: string;
-
values: int array;
-
nested: nested_data;
-
}
-
and nested_data = {
-
enabled: bool;
-
count: int;
-
}
+
type data = { name : string; values : int array; nested : nested_data }
+
and nested_data = { enabled : bool; count : int }
let nested_codec =
Jsont.Object.map ~kind:"Nested" (fun enabled count -> { enabled; count })
···
|> Jsont.Object.finish
let data_codec =
-
Jsont.Object.map ~kind:"Data" (fun name values nested -> { name; values; nested })
+
Jsont.Object.map ~kind:"Data" (fun name values nested ->
+
{ name; values; nested })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun d -> d.name)
-
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun d -> d.values)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun d ->
+
d.values)
|> Jsont.Object.mem "nested" nested_codec ~enc:(fun d -> d.nested)
|> Jsont.Object.finish
end in
-
-
let data = {
-
M.name = "test";
-
values = [|1; 2; 3|];
-
nested = { enabled = true; count = 5 };
-
} in
+
let data =
+
{
+
M.name = "test";
+
values = [| 1; 2; 3 |];
+
nested = { enabled = true; count = 5 };
+
}
+
in
(* Encode to YAML Block style *)
-
(match Yamlt.encode_string ~format:Yamlt.Block M.data_codec data with
-
| Ok s -> Printf.printf "YAML Block:\n%s\n" s
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block M.data_codec data ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s\n" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
(* Encode to YAML Flow style *)
-
(match Yamlt.encode_string ~format:Yamlt.Flow M.data_codec data with
-
| Ok s -> Printf.printf "YAML Flow:\n%s\n" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow M.data_codec data ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow:\n%s\n" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e
(* Test: Comments in YAML (should be ignored) *)
let test_comments file =
let module M = struct
-
type config = { host: string; port: int; debug: bool }
+
type config = { host : string; port : int; debug : bool }
let config_codec =
-
Jsont.Object.map ~kind:"Config" (fun host port debug -> { host; port; debug })
+
Jsont.Object.map ~kind:"Config" (fun host port debug ->
+
{ host; port; debug })
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> c.host)
|> Jsont.Object.mem "port" Jsont.int ~enc:(fun c -> c.port)
|> Jsont.Object.mem "debug" Jsont.bool ~enc:(fun c -> c.debug)
···
let show c =
Printf.sprintf "host=%S, port=%d, debug=%b" c.host c.port c.debug
end in
-
let yaml = read_file file in
-
let yaml_result = Yamlt.decode_string M.config_codec yaml in
+
let yaml_result = Yamlt.decode M.config_codec (Bytes.Reader.of_string yaml) in
match yaml_result with
| Ok v -> Printf.printf "YAML (with comments): %s\n" (M.show v)
···
(* Test: Empty documents and null documents *)
let test_empty_document file =
let module M = struct
-
type wrapper = { value: string option }
+
type wrapper = { value : string option }
let wrapper_codec =
Jsont.Object.map ~kind:"Wrapper" (fun value -> { value })
-
|> Jsont.Object.mem "value" (Jsont.some Jsont.string) ~enc:(fun w -> w.value)
+
|> Jsont.Object.mem "value" (Jsont.some Jsont.string) ~enc:(fun w ->
+
w.value)
|> Jsont.Object.finish
let show w =
···
| None -> "value=None"
| Some s -> Printf.sprintf "value=Some(%S)" s
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.wrapper_codec json in
-
let yaml_result = Yamlt.decode_string M.wrapper_codec yaml in
+
let yaml_result = Yamlt.decode M.wrapper_codec (Bytes.Reader.of_string yaml) in
show_result_both "empty_document"
(Result.map M.show json_result)
···
(* Test: Explicit typing with tags (if supported) *)
let test_explicit_tags file =
let module M = struct
-
type value_holder = { data: string }
+
type value_holder = { data : string }
let value_codec =
Jsont.Object.map ~kind:"ValueHolder" (fun data -> { data })
···
let show v = Printf.sprintf "data=%S" v.data
end in
-
let yaml = read_file file in
-
let yaml_result = Yamlt.decode_string M.value_codec yaml in
+
let yaml_result = Yamlt.decode M.value_codec (Bytes.Reader.of_string yaml) in
match yaml_result with
| Ok v -> Printf.printf "YAML (with tags): %s\n" (M.show v)
···
match Sys.argv.(1) with
| "literal" when Stdlib.Array.length Sys.argv = 3 ->
test_literal_string Sys.argv.(2)
-
| "folded" when Stdlib.Array.length Sys.argv = 3 ->
test_folded_string Sys.argv.(2)
-
| "number-formats" when Stdlib.Array.length Sys.argv = 3 ->
test_number_formats Sys.argv.(2)
-
| "encode-styles" when Stdlib.Array.length Sys.argv = 2 ->
test_encode_styles ()
-
| "comments" when Stdlib.Array.length Sys.argv = 3 ->
test_comments Sys.argv.(2)
-
| "empty-doc" when Stdlib.Array.length Sys.argv = 3 ->
test_empty_document Sys.argv.(2)
-
| "explicit-tags" when Stdlib.Array.length Sys.argv = 3 ->
test_explicit_tags Sys.argv.(2)
-
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
prerr_endline " literal <file> - Test literal multi-line strings";
prerr_endline " folded <file> - Test folded multi-line strings";
-
prerr_endline " number-formats <file> - Test hex/octal/binary number formats";
+
prerr_endline
+
" number-formats <file> - Test hex/octal/binary number formats";
prerr_endline " encode-styles - Test block vs flow encoding";
prerr_endline " comments <file> - Test YAML with comments";
prerr_endline " empty-doc <file> - Test empty documents";
+270
tests/bin/test_locations.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test location and layout preservation options with Yamlt codec *)
+
+
(* Helper to read file *)
+
open Bytesrw
+
+
let read_file path =
+
let ic = open_in path in
+
let len = in_channel_length ic in
+
let s = really_input_string ic len in
+
close_in ic;
+
s
+
+
(* Helper to show results *)
+
let show_result label = function
+
| Ok _ -> Printf.printf "%s: OK\n" label
+
| Error e -> Printf.printf "%s:\n%s\n" label e
+
+
(* Test: Compare error messages with and without locs *)
+
let test_error_precision file =
+
let yaml = read_file file in
+
+
(* Define a codec that will fail on type mismatch *)
+
let codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> (name, age))
+
|> Jsont.Object.mem "name" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "age" Jsont.int ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without locs (default) ===\n";
+
let result_no_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false in
+
show_result "Error message" result_no_locs;
+
+
Printf.printf "\n=== With locs=true ===\n";
+
let result_with_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true in
+
show_result "Error message" result_with_locs
+
+
(* Test: Show error locations for nested structures *)
+
let test_nested_error file =
+
let yaml = read_file file in
+
+
(* Nested object codec *)
+
let address_codec =
+
Jsont.Object.map ~kind:"Address" (fun street city zip ->
+
(street, city, zip))
+
|> Jsont.Object.mem "street" Jsont.string ~enc:(fun (s, _, _) -> s)
+
|> Jsont.Object.mem "city" Jsont.string ~enc:(fun (_, c, _) -> c)
+
|> Jsont.Object.mem "zip" Jsont.int ~enc:(fun (_, _, z) -> z)
+
|> Jsont.Object.finish
+
in
+
+
let codec =
+
Jsont.Object.map ~kind:"Employee" (fun name address -> (name, address))
+
|> Jsont.Object.mem "name" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "address" address_codec ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without locs (default) ===\n";
+
let result_no_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false in
+
show_result "Nested error" result_no_locs;
+
+
Printf.printf "\n=== With locs=true ===\n";
+
let result_with_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true in
+
show_result "Nested error" result_with_locs
+
+
(* Test: Array element error locations *)
+
let test_array_error file =
+
let yaml = read_file file in
+
+
(* Array codec *)
+
let codec =
+
Jsont.Object.map ~kind:"Numbers" (fun nums -> nums)
+
|> Jsont.Object.mem "values" (Jsont.array Jsont.int) ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without locs (default) ===\n";
+
let result_no_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false in
+
show_result "Array error" result_no_locs;
+
+
Printf.printf "\n=== With locs=true ===\n";
+
let result_with_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true in
+
show_result "Array error" result_with_locs
+
+
(* Test: Layout preservation - check if we can decode with layout info *)
+
let test_layout_preservation file =
+
let yaml = read_file file in
+
+
let codec =
+
Jsont.Object.map ~kind:"Config" (fun host port -> (host, port))
+
|> Jsont.Object.mem "host" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "port" Jsont.int ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without layout (default) ===\n";
+
(match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~layout:false with
+
| Ok (host, port) ->
+
Printf.printf "Decoded: host=%s, port=%d\n" host port;
+
Printf.printf "Meta preserved: no\n"
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== With layout=true ===\n";
+
match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~layout:true with
+
| Ok (host, port) ->
+
Printf.printf "Decoded: host=%s, port=%d\n" host port;
+
Printf.printf
+
"Meta preserved: yes (style info available for round-tripping)\n"
+
| Error e -> Printf.printf "Error: %s\n" e
+
+
(* Test: Round-trip with layout preservation *)
+
let test_roundtrip_layout file =
+
let yaml = read_file file in
+
+
let codec =
+
Jsont.Object.map ~kind:"Data" (fun items -> items)
+
|> Jsont.Object.mem "items" (Jsont.array Jsont.string) ~enc:(fun x -> x)
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Original YAML ===\n";
+
Printf.printf "%s\n" (String.trim yaml);
+
+
Printf.printf "\n=== Decode without layout, re-encode ===\n";
+
(match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~layout:false with
+
| Ok items -> (
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block codec items ~eod:true writer with
+
| Ok () -> Printf.printf "%s" (Buffer.contents b)
+
| Error e -> Printf.printf "Encode error: %s\n" e)
+
| Error e -> Printf.printf "Decode error: %s\n" e);
+
+
Printf.printf
+
"\n=== Decode with layout=true, re-encode with Layout format ===\n";
+
match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~layout:true with
+
| Ok items -> (
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Layout codec items ~eod:true writer with
+
| Ok () -> Printf.printf "%s" (Buffer.contents b)
+
| Error e -> Printf.printf "Encode error: %s\n" e)
+
| Error e -> Printf.printf "Decode error: %s\n" e
+
+
(* Test: File path in error messages *)
+
let test_file_path () =
+
let yaml = "name: Alice\nage: not-a-number\n" in
+
+
let codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> (name, age))
+
|> Jsont.Object.mem "name" Jsont.string ~enc:fst
+
|> Jsont.Object.mem "age" Jsont.int ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without file path ===\n";
+
let result1 = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true in
+
show_result "Error" result1;
+
+
Printf.printf "\n=== With file path ===\n";
+
let result2 = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true ~file:"test.yml" in
+
show_result "Error" result2
+
+
(* Test: Missing field error with locs *)
+
let test_missing_field file =
+
let yaml = read_file file in
+
+
let codec =
+
Jsont.Object.map ~kind:"Complete" (fun a b c -> (a, b, c))
+
|> Jsont.Object.mem "field_a" Jsont.string ~enc:(fun (a, _, _) -> a)
+
|> Jsont.Object.mem "field_b" Jsont.int ~enc:(fun (_, b, _) -> b)
+
|> Jsont.Object.mem "field_c" Jsont.bool ~enc:(fun (_, _, c) -> c)
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== Without locs ===\n";
+
let result_no_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false in
+
show_result "Missing field" result_no_locs;
+
+
Printf.printf "\n=== With locs=true ===\n";
+
let result_with_locs = Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true in
+
show_result "Missing field" result_with_locs
+
+
(* Test: Both locs and layout together *)
+
let test_combined_options file =
+
let yaml = read_file file in
+
+
let codec =
+
Jsont.Object.map ~kind:"Settings" (fun timeout retries ->
+
(timeout, retries))
+
|> Jsont.Object.mem "timeout" Jsont.int ~enc:fst
+
|> Jsont.Object.mem "retries" Jsont.int ~enc:snd
+
|> Jsont.Object.finish
+
in
+
+
Printf.printf "=== locs=false, layout=false (defaults) ===\n";
+
(match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false ~layout:false with
+
| Ok (timeout, retries) ->
+
Printf.printf "OK: timeout=%d, retries=%d\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== locs=true, layout=false ===\n";
+
(match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true ~layout:false with
+
| Ok (timeout, retries) ->
+
Printf.printf "OK: timeout=%d, retries=%d (with precise locations)\n"
+
timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== locs=false, layout=true ===\n";
+
(match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:false ~layout:true with
+
| Ok (timeout, retries) ->
+
Printf.printf "OK: timeout=%d, retries=%d (with layout metadata)\n"
+
timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== locs=true, layout=true (both enabled) ===\n";
+
match Yamlt.decode codec (Bytes.Reader.of_string yaml) ~locs:true ~layout:true with
+
| Ok (timeout, retries) ->
+
Printf.printf "OK: timeout=%d, retries=%d (with locations and layout)\n"
+
timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e
+
+
let () =
+
let usage = "Usage: test_locations <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
match Sys.argv.(1) with
+
| "error-precision" when Array.length Sys.argv = 3 ->
+
test_error_precision Sys.argv.(2)
+
| "nested-error" when Array.length Sys.argv = 3 ->
+
test_nested_error Sys.argv.(2)
+
| "array-error" when Array.length Sys.argv = 3 ->
+
test_array_error Sys.argv.(2)
+
| "layout" when Array.length Sys.argv = 3 ->
+
test_layout_preservation Sys.argv.(2)
+
| "roundtrip" when Array.length Sys.argv = 3 ->
+
test_roundtrip_layout Sys.argv.(2)
+
| "file-path" -> test_file_path ()
+
| "missing-field" when Array.length Sys.argv = 3 ->
+
test_missing_field Sys.argv.(2)
+
| "combined" when Array.length Sys.argv = 3 ->
+
test_combined_options Sys.argv.(2)
+
| _ ->
+
prerr_endline usage;
+
prerr_endline "Commands:";
+
prerr_endline
+
" error-precision <file> - Compare error messages with/without locs";
+
prerr_endline
+
" nested-error <file> - Test error locations in nested objects";
+
prerr_endline
+
" array-error <file> - Test error locations in arrays";
+
prerr_endline " layout <file> - Test layout preservation";
+
prerr_endline
+
" roundtrip <file> - Test round-tripping with layout";
+
prerr_endline
+
" file-path - Test file path in error messages";
+
prerr_endline
+
" missing-field <file> - Test missing field errors with locs";
+
prerr_endline " combined <file> - Test locs and layout together";
+
exit 1
+240
tests/bin/test_multidoc.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test multi-document YAML streams with decode_all *)
+
+
open Bytesrw
+
+
(* Helper to read file *)
+
let read_file path =
+
let ic = open_in path in
+
let len = in_channel_length ic in
+
let s = really_input_string ic len in
+
close_in ic;
+
s
+
+
(* Helper to show results *)
+
let show_result label = function
+
| Ok v -> Printf.printf "%s: %s\n" label v
+
| Error e -> Printf.printf "%s: ERROR: %s\n" label e
+
+
(* Test: Simple multi-document stream *)
+
let test_simple file =
+
let module M = struct
+
type person = { name : string; age : int }
+
+
let person_codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
+
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
+
|> Jsont.Object.finish
+
+
let show p = Printf.sprintf "%s (age %d)" p.name p.age
+
end in
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all M.person_codec reader in
+
Printf.printf "Documents:\n";
+
seq |> Seq.iteri (fun i result ->
+
Printf.printf " [%d] " i;
+
show_result "" (Result.map M.show result)
+
)
+
+
(* Test: Count documents *)
+
let test_count file =
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all Jsont.json reader in
+
let count = Seq.fold_left (fun acc _ -> acc + 1) 0 seq in
+
Printf.printf "Document count: %d\n" count
+
+
(* Test: Error tracking - show which documents succeed and which fail *)
+
let test_errors file =
+
let module M = struct
+
type person = { name : string; age : int }
+
+
let person_codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
+
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
+
|> Jsont.Object.finish
+
+
let show p = Printf.sprintf "%s (age %d)" p.name p.age
+
end in
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all M.person_codec reader in
+
Printf.printf "Document results:\n";
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok p -> Printf.printf " [%d] OK: %s\n" i (M.show p)
+
| Error e -> Printf.printf " [%d] ERROR: %s\n" i (String.trim e)
+
)
+
+
(* Test: Location tracking with locs=true *)
+
let test_locations file =
+
let module M = struct
+
type person = { name : string; age : int }
+
+
let person_codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
+
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
+
|> Jsont.Object.finish
+
end in
+
let yaml = read_file file in
+
+
Printf.printf "=== Without locs (default) ===\n";
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all ~locs:false M.person_codec reader in
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok _ -> Printf.printf " [%d] OK\n" i
+
| Error e -> Printf.printf " [%d] ERROR:\n%s\n" i (String.trim e)
+
);
+
+
Printf.printf "\n=== With locs=true ===\n";
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all ~locs:true ~file:"test.yml" M.person_codec reader in
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok _ -> Printf.printf " [%d] OK\n" i
+
| Error e -> Printf.printf " [%d] ERROR:\n%s\n" i (String.trim e)
+
)
+
+
(* Test: Roundtrip to JSON - decode YAML multidoc, encode each to JSON *)
+
let test_json_roundtrip file =
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all Jsont.json reader in
+
Printf.printf "JSON outputs:\n";
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok json_val ->
+
(match Jsont_bytesrw.encode_string Jsont.json json_val with
+
| Ok json_str -> Printf.printf " [%d] %s\n" i (String.trim json_str)
+
| Error e -> Printf.printf " [%d] ENCODE ERROR: %s\n" i e)
+
| Error e -> Printf.printf " [%d] DECODE ERROR: %s\n" i (String.trim e)
+
)
+
+
(* Test: Nested objects in multidoc *)
+
let test_nested file =
+
let module M = struct
+
type address = { street : string; city : string }
+
type person = { name : string; age : int; address : address }
+
+
let address_codec =
+
Jsont.Object.map ~kind:"Address" (fun street city -> { street; city })
+
|> Jsont.Object.mem "street" Jsont.string ~enc:(fun a -> a.street)
+
|> Jsont.Object.mem "city" Jsont.string ~enc:(fun a -> a.city)
+
|> Jsont.Object.finish
+
+
let person_codec =
+
Jsont.Object.map ~kind:"Person" (fun name age address ->
+
{ name; age; address })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
+
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
+
|> Jsont.Object.mem "address" address_codec ~enc:(fun p -> p.address)
+
|> Jsont.Object.finish
+
+
let show p =
+
Printf.sprintf "%s (age %d) from %s, %s" p.name p.age p.address.street
+
p.address.city
+
end in
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all M.person_codec reader in
+
Printf.printf "Nested documents:\n";
+
seq |> Seq.iteri (fun i result ->
+
Printf.printf " [%d] " i;
+
show_result "" (Result.map M.show result)
+
)
+
+
(* Test: Arrays in multidoc *)
+
let test_arrays file =
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all Jsont.json reader in
+
Printf.printf "Array documents:\n";
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok json_val ->
+
(match Jsont_bytesrw.encode_string Jsont.json json_val with
+
| Ok json_str -> Printf.printf " [%d] %s\n" i (String.trim json_str)
+
| Error e -> Printf.printf " [%d] ERROR: %s\n" i e)
+
| Error e -> Printf.printf " [%d] ERROR: %s\n" i (String.trim e)
+
)
+
+
(* Test: Scalars in multidoc *)
+
let test_scalars file =
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all Jsont.json reader in
+
Printf.printf "Scalar documents:\n";
+
seq |> Seq.iteri (fun i result ->
+
match result with
+
| Ok json_val ->
+
(match Jsont_bytesrw.encode_string Jsont.json json_val with
+
| Ok json_str -> Printf.printf " [%d] %s\n" i (String.trim json_str)
+
| Error e -> Printf.printf " [%d] ERROR: %s\n" i e)
+
| Error e -> Printf.printf " [%d] ERROR: %s\n" i (String.trim e)
+
)
+
+
(* Test: Summary stats - count successes vs failures *)
+
let test_summary file =
+
let module M = struct
+
type person = { name : string; age : int }
+
+
let person_codec =
+
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
+
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
+
|> Jsont.Object.finish
+
end in
+
let yaml = read_file file in
+
let reader = Bytes.Reader.of_string yaml in
+
let seq = Yamlt.decode_all M.person_codec reader in
+
let success = ref 0 in
+
let failure = ref 0 in
+
seq |> Seq.iter (fun result ->
+
match result with
+
| Ok _ -> incr success
+
| Error _ -> incr failure
+
);
+
Printf.printf "Summary: %d documents (%d ok, %d error)\n"
+
(!success + !failure) !success !failure
+
+
let () =
+
let usage = "Usage: test_multidoc <command> <file>" in
+
if Array.length Sys.argv < 3 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
let test = Sys.argv.(1) in
+
let file = Sys.argv.(2) in
+
match test with
+
| "simple" -> test_simple file
+
| "count" -> test_count file
+
| "errors" -> test_errors file
+
| "locations" -> test_locations file
+
| "json" -> test_json_roundtrip file
+
| "nested" -> test_nested file
+
| "arrays" -> test_arrays file
+
| "scalars" -> test_scalars file
+
| "summary" -> test_summary file
+
| _ ->
+
prerr_endline usage;
+
prerr_endline "Commands:";
+
prerr_endline " simple <file> - Decode person documents";
+
prerr_endline " count <file> - Count documents";
+
prerr_endline " errors <file> - Show success/error for each document";
+
prerr_endline " locations <file> - Test location tracking with locs=true";
+
prerr_endline " json <file> - Roundtrip to JSON";
+
prerr_endline " nested <file> - Decode nested objects";
+
prerr_endline " arrays <file> - Decode arrays";
+
prerr_endline " scalars <file> - Decode scalars";
+
prerr_endline " summary <file> - Show success/failure summary";
+
exit 1
+89
tests/bin/test_null_collections.ml
···
+
open Bytesrw
+
+
let () =
+
Printf.printf "=== Test 1: Explicit null as empty array ===\n";
+
let yaml1 = "values: null" in
+
let codec1 =
+
let open Jsont in
+
Object.map ~kind:"Test" (fun v -> v)
+
|> Object.mem "values" (list int) ~dec_absent:[] ~enc:(fun v -> v)
+
|> Object.finish
+
in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml1) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 2: Tilde as empty array ===\n";
+
let yaml2 = "values: ~" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml2) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 3: Empty array syntax ===\n";
+
let yaml3 = "values: []" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml3) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 4: Array with values ===\n";
+
let yaml4 = "values: [1, 2, 3]" in
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml4) with
+
| Ok v ->
+
Printf.printf "Result: [%s]\n"
+
(String.concat "; " (List.map string_of_int v))
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 5: Explicit null as empty object ===\n";
+
let yaml5 = "config: null" in
+
let codec2 =
+
let open Jsont in
+
let config_codec =
+
Object.map ~kind:"Config" (fun timeout retries -> (timeout, retries))
+
|> Object.mem "timeout" int ~dec_absent:30 ~enc:fst
+
|> Object.mem "retries" int ~dec_absent:3 ~enc:snd
+
|> Object.finish
+
in
+
Object.map ~kind:"Test" (fun c -> c)
+
|> Object.mem "config" config_codec ~dec_absent:(30, 3) ~enc:(fun c -> c)
+
|> Object.finish
+
in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml5) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 6: Empty object syntax ===\n";
+
let yaml6 = "config: {}" in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml6) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 7: Object with values ===\n";
+
let yaml7 = "config:\n timeout: 60\n retries: 5" in
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string yaml7) with
+
| Ok (timeout, retries) ->
+
Printf.printf "Result: {timeout=%d; retries=%d}\n" timeout retries
+
| Error e -> Printf.printf "Error: %s\n" e);
+
+
Printf.printf "\n=== Test 8: Nested null arrays ===\n";
+
let yaml8 = "name: test\nitems: null\ntags: ~" in
+
let codec3 =
+
let open Jsont in
+
Object.map ~kind:"Nested" (fun name items tags -> (name, items, tags))
+
|> Object.mem "name" string ~enc:(fun (n, _, _) -> n)
+
|> Object.mem "items" (list int) ~dec_absent:[] ~enc:(fun (_, i, _) -> i)
+
|> Object.mem "tags" (list string) ~dec_absent:[] ~enc:(fun (_, _, t) -> t)
+
|> Object.finish
+
in
+
match Yamlt.decode codec3 (Bytes.Reader.of_string yaml8) with
+
| Ok (name, items, tags) ->
+
Printf.printf "Result: {name=%s; items_count=%d; tags_count=%d}\n"
+
name (List.length items) (List.length tags)
+
| Error e -> Printf.printf "Error: %s\n" e
+18 -12
tests/bin/test_null_complete.ml
···
+
open Bytesrw
+
let () =
Printf.printf "=== Test 1: Jsont.option with YAML null ===\n";
let yaml1 = "value: null" in
···
|> Object.mem "value" (option string) ~enc:(fun v -> v)
|> Object.finish
in
-
(match Yamlt.decode_string codec1 yaml1 with
-
| Ok v -> Printf.printf "Result: %s\n" (match v with None -> "None" | Some s -> "Some(" ^ s ^ ")")
-
| Error e -> Printf.printf "Error: %s\n" e);
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml1) with
+
| Ok v ->
+
Printf.printf "Result: %s\n"
+
(match v with None -> "None" | Some s -> "Some(" ^ s ^ ")")
+
| Error e -> Printf.printf "Error: %s\n" e);
Printf.printf "\n=== Test 2: Jsont.option with YAML string ===\n";
-
(match Yamlt.decode_string codec1 "value: hello" with
-
| Ok v -> Printf.printf "Result: %s\n" (match v with None -> "None" | Some s -> "Some(" ^ s ^ ")")
-
| Error e -> Printf.printf "Error: %s\n" e);
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string "value: hello") with
+
| Ok v ->
+
Printf.printf "Result: %s\n"
+
(match v with None -> "None" | Some s -> "Some(" ^ s ^ ")")
+
| Error e -> Printf.printf "Error: %s\n" e);
Printf.printf "\n=== Test 3: Jsont.string with YAML null (should error) ===\n";
let codec2 =
···
|> Object.mem "value" string ~enc:(fun v -> v)
|> Object.finish
in
-
(match Yamlt.decode_string codec2 "value: null" with
-
| Ok v -> Printf.printf "Result: %s\n" v
-
| Error e -> Printf.printf "Error (expected): %s\n" e);
+
(match Yamlt.decode codec2 (Bytes.Reader.of_string "value: null") with
+
| Ok v -> Printf.printf "Result: %s\n" v
+
| Error e -> Printf.printf "Error (expected): %s\n" e);
Printf.printf "\n=== Test 4: Jsont.string with YAML string ===\n";
-
(match Yamlt.decode_string codec2 "value: hello" with
-
| Ok v -> Printf.printf "Result: %s\n" v
-
| Error e -> Printf.printf "Error: %s\n" e)
+
match Yamlt.decode codec2 (Bytes.Reader.of_string "value: hello") with
+
| Ok v -> Printf.printf "Result: %s\n" v
+
| Error e -> Printf.printf "Error: %s\n" e
+19 -18
tests/bin/test_null_fix.ml
···
-
open Jsont
+
open Bytesrw
let () =
let module M = struct
-
type data = { value: string option }
-
+
type data = { value : string option }
+
let data_codec =
Jsont.Object.map ~kind:"Data" (fun value -> { value })
-
|> Jsont.Object.mem "value" (Jsont.option Jsont.string) ~enc:(fun d -> d.value)
+
|> Jsont.Object.mem "value" (Jsont.option Jsont.string) ~enc:(fun d ->
+
d.value)
|> Jsont.Object.finish
end in
-
let yaml_null = "value: null" in
Printf.printf "Testing YAML null handling with Jsont.option Jsont.string:\n\n";
-
match Yamlt.decode_string M.data_codec yaml_null with
-
| Ok data ->
-
(match data.M.value with
-
| None -> Printf.printf "YAML: value=None (CORRECT)\n"
-
| Some s -> Printf.printf "YAML: value=Some(%S) (BUG!)\n" s)
-
| Error e -> Printf.printf "YAML ERROR: %s\n" e;
+
match Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml_null) with
+
| Ok data -> (
+
match data.M.value with
+
| None -> Printf.printf "YAML: value=None (CORRECT)\n"
+
| Some s -> Printf.printf "YAML: value=Some(%S) (BUG!)\n" s)
+
| Error e -> (
+
Printf.printf "YAML ERROR: %s\n" e;
-
let json_null = "{\"value\": null}" in
-
match Jsont_bytesrw.decode_string M.data_codec json_null with
-
| Ok data ->
-
(match data.M.value with
-
| None -> Printf.printf "JSON: value=None (CORRECT)\n"
-
| Some s -> Printf.printf "JSON: value=Some(%S) (BUG!)\n" s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e
+
let json_null = "{\"value\": null}" in
+
match Jsont_bytesrw.decode_string M.data_codec json_null with
+
| Ok data -> (
+
match data.M.value with
+
| None -> Printf.printf "JSON: value=None (CORRECT)\n"
+
| Some s -> Printf.printf "JSON: value=Some(%S) (BUG!)\n" s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e)
+68 -73
tests/bin/test_objects.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test object codec functionality with Yamlt *)
+
+
open Bytesrw
(* Helper to read file *)
let read_file path =
···
(* Test: Simple object with required fields *)
let test_simple_object file =
let module M = struct
-
type person = { name: string; age: int }
+
type person = { name : string; age : int }
let person_codec =
Jsont.Object.map ~kind:"Person" (fun name age -> { name; age })
···
let show p = Printf.sprintf "{name=%S; age=%d}" p.name p.age
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.person_codec json in
-
let yaml_result = Yamlt.decode_string M.person_codec yaml in
+
let yaml_result = Yamlt.decode M.person_codec (Bytes.Reader.of_string yaml) in
show_result_both "person"
(Result.map M.show json_result)
···
(* Test: Object with optional fields *)
let test_optional_fields file =
let module M = struct
-
type config = { host: string; port: int option; debug: bool option }
+
type config = { host : string; port : int option; debug : bool option }
let config_codec =
-
Jsont.Object.map ~kind:"Config"
-
(fun host port debug -> { host; port; debug })
+
Jsont.Object.map ~kind:"Config" (fun host port debug ->
+
{ host; port; debug })
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> c.host)
|> Jsont.Object.opt_mem "port" Jsont.int ~enc:(fun c -> c.port)
|> Jsont.Object.opt_mem "debug" Jsont.bool ~enc:(fun c -> c.debug)
|> Jsont.Object.finish
let show c =
-
Printf.sprintf "{host=%S; port=%s; debug=%s}"
-
c.host
-
(match c.port with None -> "None" | Some p -> Printf.sprintf "Some %d" p)
-
(match c.debug with None -> "None" | Some b -> Printf.sprintf "Some %b" b)
+
Printf.sprintf "{host=%S; port=%s; debug=%s}" c.host
+
(match c.port with
+
| None -> "None"
+
| Some p -> Printf.sprintf "Some %d" p)
+
(match c.debug with
+
| None -> "None"
+
| Some b -> Printf.sprintf "Some %b" b)
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.config_codec json in
-
let yaml_result = Yamlt.decode_string M.config_codec yaml in
+
let yaml_result = Yamlt.decode M.config_codec (Bytes.Reader.of_string yaml) in
show_result_both "config"
(Result.map M.show json_result)
···
(* Test: Object with default values *)
let test_default_values file =
let module M = struct
-
type settings = { timeout: int; retries: int; verbose: bool }
+
type settings = { timeout : int; retries : int; verbose : bool }
let settings_codec =
-
Jsont.Object.map ~kind:"Settings"
-
(fun timeout retries verbose -> { timeout; retries; verbose })
-
|> Jsont.Object.mem "timeout" Jsont.int ~enc:(fun s -> s.timeout) ~dec_absent:30
-
|> Jsont.Object.mem "retries" Jsont.int ~enc:(fun s -> s.retries) ~dec_absent:3
-
|> Jsont.Object.mem "verbose" Jsont.bool ~enc:(fun s -> s.verbose) ~dec_absent:false
+
Jsont.Object.map ~kind:"Settings" (fun timeout retries verbose ->
+
{ timeout; retries; verbose })
+
|> Jsont.Object.mem "timeout" Jsont.int
+
~enc:(fun s -> s.timeout)
+
~dec_absent:30
+
|> Jsont.Object.mem "retries" Jsont.int
+
~enc:(fun s -> s.retries)
+
~dec_absent:3
+
|> Jsont.Object.mem "verbose" Jsont.bool
+
~enc:(fun s -> s.verbose)
+
~dec_absent:false
|> Jsont.Object.finish
let show s =
-
Printf.sprintf "{timeout=%d; retries=%d; verbose=%b}"
-
s.timeout s.retries s.verbose
+
Printf.sprintf "{timeout=%d; retries=%d; verbose=%b}" s.timeout s.retries
+
s.verbose
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.settings_codec json in
-
let yaml_result = Yamlt.decode_string M.settings_codec yaml in
+
let yaml_result = Yamlt.decode M.settings_codec (Bytes.Reader.of_string yaml) in
show_result_both "settings"
(Result.map M.show json_result)
···
(* Test: Nested objects *)
let test_nested_objects file =
let module M = struct
-
type address = { street: string; city: string; zip: string }
-
type employee = { name: string; address: address }
+
type address = { street : string; city : string; zip : string }
+
type employee = { name : string; address : address }
let address_codec =
-
Jsont.Object.map ~kind:"Address"
-
(fun street city zip -> { street; city; zip })
+
Jsont.Object.map ~kind:"Address" (fun street city zip ->
+
{ street; city; zip })
|> Jsont.Object.mem "street" Jsont.string ~enc:(fun a -> a.street)
|> Jsont.Object.mem "city" Jsont.string ~enc:(fun a -> a.city)
|> Jsont.Object.mem "zip" Jsont.string ~enc:(fun a -> a.zip)
|> Jsont.Object.finish
let employee_codec =
-
Jsont.Object.map ~kind:"Employee"
-
(fun name address -> { name; address })
+
Jsont.Object.map ~kind:"Employee" (fun name address -> { name; address })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun e -> e.name)
|> Jsont.Object.mem "address" address_codec ~enc:(fun e -> e.address)
|> Jsont.Object.finish
let show e =
-
Printf.sprintf "{name=%S; address={street=%S; city=%S; zip=%S}}"
-
e.name e.address.street e.address.city e.address.zip
+
Printf.sprintf "{name=%S; address={street=%S; city=%S; zip=%S}}" e.name
+
e.address.street e.address.city e.address.zip
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.employee_codec json in
-
let yaml_result = Yamlt.decode_string M.employee_codec yaml in
+
let yaml_result = Yamlt.decode M.employee_codec (Bytes.Reader.of_string yaml) in
show_result_both "employee"
(Result.map M.show json_result)
···
(* Test: Unknown member handling - error *)
let test_unknown_members_error file =
let module M = struct
-
type strict = { name: string }
+
type strict = { name : string }
let strict_codec =
Jsont.Object.map ~kind:"Strict" (fun name -> { name })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun s -> s.name)
|> Jsont.Object.finish
end in
-
let yaml = read_file file in
-
let result = Yamlt.decode_string M.strict_codec yaml in
+
let result = Yamlt.decode M.strict_codec (Bytes.Reader.of_string yaml) in
match result with
| Ok _ -> Printf.printf "Unexpected success\n"
| Error e -> Printf.printf "Expected error: %s\n" e
···
(* Test: Unknown member handling - keep *)
let test_unknown_members_keep file =
let module M = struct
-
type flexible = { name: string; extra: Jsont.json }
+
type flexible = { name : string; extra : Jsont.json }
let flexible_codec =
Jsont.Object.map ~kind:"Flexible" (fun name extra -> { name; extra })
···
|> Jsont.Object.keep_unknown Jsont.json_mems ~enc:(fun f -> f.extra)
|> Jsont.Object.finish
-
let show f =
-
Printf.sprintf "{name=%S; has_extra=true}" f.name
+
let show f = Printf.sprintf "{name=%S; has_extra=true}" f.name
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.flexible_codec json in
-
let yaml_result = Yamlt.decode_string M.flexible_codec yaml in
+
let yaml_result = Yamlt.decode M.flexible_codec (Bytes.Reader.of_string yaml) in
show_result_both "flexible"
(Result.map M.show json_result)
···
(* Test: Object cases (discriminated unions) - simplified version *)
let test_object_cases file =
let module M = struct
-
type circle = { type_: string; radius: float }
+
type circle = { type_ : string; radius : float }
let circle_codec =
Jsont.Object.map ~kind:"Circle" (fun type_ radius -> { type_; radius })
···
|> Jsont.Object.mem "radius" Jsont.number ~enc:(fun c -> c.radius)
|> Jsont.Object.finish
-
let show c =
-
Printf.sprintf "Circle{radius=%.2f}" c.radius
+
let show c = Printf.sprintf "Circle{radius=%.2f}" c.radius
end in
-
let yaml = read_file file in
let json = read_file (file ^ ".json") in
let json_result = Jsont_bytesrw.decode_string M.circle_codec json in
-
let yaml_result = Yamlt.decode_string M.circle_codec yaml in
+
let yaml_result = Yamlt.decode M.circle_codec (Bytes.Reader.of_string yaml) in
show_result_both "shape"
(Result.map M.show json_result)
···
(* Test: Missing required field error *)
let test_missing_required file =
let module M = struct
-
type required = { name: string; age: int }
+
type required = { name : string; age : int }
let required_codec =
Jsont.Object.map ~kind:"Required" (fun name age -> { name; age })
···
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun r -> r.age)
|> Jsont.Object.finish
end in
-
let yaml = read_file file in
-
let result = Yamlt.decode_string M.required_codec yaml in
+
let result = Yamlt.decode M.required_codec (Bytes.Reader.of_string yaml) in
match result with
| Ok _ -> Printf.printf "Unexpected success\n"
| Error e -> Printf.printf "Expected error: %s\n" e
···
(* Test: Encoding objects to different formats *)
let test_encode_object () =
let module M = struct
-
type person = { name: string; age: int; active: bool }
+
type person = { name : string; age : int; active : bool }
let person_codec =
-
Jsont.Object.map ~kind:"Person" (fun name age active -> { name; age; active })
+
Jsont.Object.map ~kind:"Person" (fun name age active ->
+
{ name; age; active })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name)
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
|> Jsont.Object.mem "active" Jsont.bool ~enc:(fun p -> p.active)
|> Jsont.Object.finish
end in
-
let person = M.{ name = "Alice"; age = 30; active = true } in
(* Encode to JSON *)
(match Jsont_bytesrw.encode_string M.person_codec person with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
(* Encode to YAML Block *)
-
(match Yamlt.encode_string ~format:Yamlt.Block M.person_codec person with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block M.person_codec person ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
(* Encode to YAML Flow *)
-
(match Yamlt.encode_string ~format:Yamlt.Flow M.person_codec person with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow M.person_codec person ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e
let () =
let usage = "Usage: test_objects <command> [args...]" in
···
match Sys.argv.(1) with
| "simple" when Stdlib.Array.length Sys.argv = 3 ->
test_simple_object Sys.argv.(2)
-
| "optional" when Stdlib.Array.length Sys.argv = 3 ->
test_optional_fields Sys.argv.(2)
-
| "defaults" when Stdlib.Array.length Sys.argv = 3 ->
test_default_values Sys.argv.(2)
-
| "nested" when Stdlib.Array.length Sys.argv = 3 ->
test_nested_objects Sys.argv.(2)
-
| "unknown-error" when Stdlib.Array.length Sys.argv = 3 ->
test_unknown_members_error Sys.argv.(2)
-
| "unknown-keep" when Stdlib.Array.length Sys.argv = 3 ->
test_unknown_members_keep Sys.argv.(2)
-
| "cases" when Stdlib.Array.length Sys.argv = 3 ->
test_object_cases Sys.argv.(2)
-
| "missing-required" when Stdlib.Array.length Sys.argv = 3 ->
test_missing_required Sys.argv.(2)
-
-
| "encode" when Stdlib.Array.length Sys.argv = 2 ->
-
test_encode_object ()
-
+
| "encode" when Stdlib.Array.length Sys.argv = 2 -> test_encode_object ()
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
···
prerr_endline " unknown-error <file> - Test unknown member error";
prerr_endline " unknown-keep <file> - Test keeping unknown members";
prerr_endline " cases <file> - Test object cases (unions)";
-
prerr_endline " missing-required <file> - Test missing required field error";
+
prerr_endline
+
" missing-required <file> - Test missing required field error";
prerr_endline " encode - Test encoding objects";
exit 1
+9 -6
tests/bin/test_opt_array.ml
···
+
open Bytesrw
+
let () =
let codec =
Jsont.Object.map ~kind:"Test" (fun arr -> arr)
-
|> Jsont.Object.opt_mem "values" (Jsont.array Jsont.string) ~enc:(fun arr -> arr)
+
|> Jsont.Object.opt_mem "values" (Jsont.array Jsont.string) ~enc:(fun arr ->
+
arr)
|> Jsont.Object.finish
in
let yaml = "values: [a, b, c]" in
Printf.printf "Testing optional array field:\n";
-
match Yamlt.decode_string codec yaml with
-
| Ok arr ->
-
(match arr with
-
| None -> Printf.printf "Result: None\n"
-
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
+
match Yamlt.decode codec (Bytes.Reader.of_string yaml) with
+
| Ok arr -> (
+
match arr with
+
| None -> Printf.printf "Result: None\n"
+
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
| Error e -> Printf.printf "Error: %s\n" e
+158 -88
tests/bin/test_roundtrip.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test roundtrip encoding/decoding with Yamlt *)
+
+
open Bytesrw
(* Test: Roundtrip scalars *)
let test_scalar_roundtrip () =
let module M = struct
-
type data = { s: string; n: float; b: bool; nul: unit }
+
type data = { s : string; n : float; b : bool; nul : unit }
let data_codec =
Jsont.Object.map ~kind:"Data" (fun s n b nul -> { s; n; b; nul })
···
let equal d1 d2 =
d1.s = d2.s && d1.n = d2.n && d1.b = d2.b && d1.nul = d2.nul
end in
-
let original = { M.s = "hello"; n = 42.5; b = true; nul = () } in
(* JSON roundtrip *)
let json_encoded = Jsont_bytesrw.encode_string M.data_codec original in
-
let json_decoded = Result.bind json_encoded (Jsont_bytesrw.decode_string M.data_codec) in
+
let json_decoded =
+
Result.bind json_encoded (Jsont_bytesrw.decode_string M.data_codec)
+
in
(match json_decoded with
-
| Ok decoded when M.equal original decoded -> Printf.printf "JSON roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "JSON roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "JSON roundtrip: FAIL (%s)\n" e);
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "JSON roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "JSON roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "JSON roundtrip: FAIL (%s)\n" e);
(* YAML Block roundtrip *)
-
let yaml_block_encoded = Yamlt.encode_string ~format:Yamlt.Block M.data_codec original in
-
let yaml_block_decoded = Result.bind yaml_block_encoded (Yamlt.decode_string M.data_codec) in
+
let yaml_block_encoded =
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block M.data_codec original ~eod:true writer with
+
| Ok () -> Ok (Buffer.contents b)
+
| Error e -> Error e
+
in
+
let yaml_block_decoded =
+
Result.bind yaml_block_encoded (fun yaml ->
+
Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml))
+
in
(match yaml_block_decoded with
-
| Ok decoded when M.equal original decoded -> Printf.printf "YAML Block roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "YAML Block roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "YAML Block roundtrip: FAIL (%s)\n" e);
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "YAML Block roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "YAML Block roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "YAML Block roundtrip: FAIL (%s)\n" e);
(* YAML Flow roundtrip *)
-
let yaml_flow_encoded = Yamlt.encode_string ~format:Yamlt.Flow M.data_codec original in
-
let yaml_flow_decoded = Result.bind yaml_flow_encoded (Yamlt.decode_string M.data_codec) in
-
(match yaml_flow_decoded with
-
| Ok decoded when M.equal original decoded -> Printf.printf "YAML Flow roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "YAML Flow roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "YAML Flow roundtrip: FAIL (%s)\n" e)
+
let yaml_flow_encoded =
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow M.data_codec original ~eod:true writer with
+
| Ok () -> Ok (Buffer.contents b)
+
| Error e -> Error e
+
in
+
let yaml_flow_decoded =
+
Result.bind yaml_flow_encoded (fun yaml ->
+
Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml))
+
in
+
match yaml_flow_decoded with
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "YAML Flow roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "YAML Flow roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "YAML Flow roundtrip: FAIL (%s)\n" e
(* Test: Roundtrip arrays *)
let test_array_roundtrip () =
let module M = struct
-
type data = { items: int array; nested: float array array }
+
type data = { items : int array; nested : float array array }
let data_codec =
Jsont.Object.map ~kind:"Data" (fun items nested -> { items; nested })
-
|> Jsont.Object.mem "items" (Jsont.array Jsont.int) ~enc:(fun d -> d.items)
-
|> Jsont.Object.mem "nested" (Jsont.array (Jsont.array Jsont.number)) ~enc:(fun d -> d.nested)
+
|> Jsont.Object.mem "items" (Jsont.array Jsont.int) ~enc:(fun d ->
+
d.items)
+
|> Jsont.Object.mem "nested"
+
(Jsont.array (Jsont.array Jsont.number))
+
~enc:(fun d -> d.nested)
|> Jsont.Object.finish
-
let equal d1 d2 =
-
d1.items = d2.items && d1.nested = d2.nested
+
let equal d1 d2 = d1.items = d2.items && d1.nested = d2.nested
end in
-
-
let original = { M.items = [|1; 2; 3; 4; 5|]; nested = [|[|1.0; 2.0|]; [|3.0; 4.0|]|] } in
+
let original =
+
{
+
M.items = [| 1; 2; 3; 4; 5 |];
+
nested = [| [| 1.0; 2.0 |]; [| 3.0; 4.0 |] |];
+
}
+
in
(* JSON roundtrip *)
-
let json_result = Result.bind
-
(Jsont_bytesrw.encode_string M.data_codec original)
-
(Jsont_bytesrw.decode_string M.data_codec) in
+
let json_result =
+
Result.bind
+
(Jsont_bytesrw.encode_string M.data_codec original)
+
(Jsont_bytesrw.decode_string M.data_codec)
+
in
(match json_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "JSON array roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "JSON array roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "JSON array roundtrip: FAIL (%s)\n" e);
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "JSON array roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "JSON array roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "JSON array roundtrip: FAIL (%s)\n" e);
(* YAML roundtrip *)
-
let yaml_result = Result.bind
-
(Yamlt.encode_string M.data_codec original)
-
(Yamlt.decode_string M.data_codec) in
-
(match yaml_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "YAML array roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "YAML array roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "YAML array roundtrip: FAIL (%s)\n" e)
+
let yaml_result =
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode M.data_codec original ~eod:true writer with
+
| Ok () ->
+
let yaml = Buffer.contents b in
+
Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml)
+
| Error e -> Error e
+
in
+
match yaml_result with
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "YAML array roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "YAML array roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "YAML array roundtrip: FAIL (%s)\n" e
(* Test: Roundtrip objects *)
let test_object_roundtrip () =
let module M = struct
-
type person = { p_name: string; age: int; active: bool }
-
type company = { c_name: string; employees: person array }
+
type person = { p_name : string; age : int; active : bool }
+
type company = { c_name : string; employees : person array }
let person_codec =
-
Jsont.Object.map ~kind:"Person" (fun p_name age active -> { p_name; age; active })
+
Jsont.Object.map ~kind:"Person" (fun p_name age active ->
+
{ p_name; age; active })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.p_name)
|> Jsont.Object.mem "age" Jsont.int ~enc:(fun p -> p.age)
|> Jsont.Object.mem "active" Jsont.bool ~enc:(fun p -> p.active)
|> Jsont.Object.finish
let company_codec =
-
Jsont.Object.map ~kind:"Company" (fun c_name employees -> { c_name; employees })
+
Jsont.Object.map ~kind:"Company" (fun c_name employees ->
+
{ c_name; employees })
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.c_name)
-
|> Jsont.Object.mem "employees" (Jsont.array person_codec) ~enc:(fun c -> c.employees)
+
|> Jsont.Object.mem "employees" (Jsont.array person_codec) ~enc:(fun c ->
+
c.employees)
|> Jsont.Object.finish
let person_equal p1 p2 =
p1.p_name = p2.p_name && p1.age = p2.age && p1.active = p2.active
let equal c1 c2 =
-
c1.c_name = c2.c_name &&
-
Stdlib.Array.length c1.employees = Stdlib.Array.length c2.employees &&
-
Stdlib.Array.for_all2 person_equal c1.employees c2.employees
+
c1.c_name = c2.c_name
+
&& Stdlib.Array.length c1.employees = Stdlib.Array.length c2.employees
+
&& Stdlib.Array.for_all2 person_equal c1.employees c2.employees
end in
-
-
let original = {
-
M.c_name = "Acme Corp";
-
employees = [|
-
{ p_name = "Alice"; age = 30; active = true };
-
{ p_name = "Bob"; age = 25; active = false };
-
|]
-
} in
+
let original =
+
{
+
M.c_name = "Acme Corp";
+
employees =
+
[|
+
{ p_name = "Alice"; age = 30; active = true };
+
{ p_name = "Bob"; age = 25; active = false };
+
|];
+
}
+
in
(* JSON roundtrip *)
-
let json_result = Result.bind
-
(Jsont_bytesrw.encode_string M.company_codec original)
-
(Jsont_bytesrw.decode_string M.company_codec) in
+
let json_result =
+
Result.bind
+
(Jsont_bytesrw.encode_string M.company_codec original)
+
(Jsont_bytesrw.decode_string M.company_codec)
+
in
(match json_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "JSON object roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "JSON object roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "JSON object roundtrip: FAIL (%s)\n" e);
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "JSON object roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "JSON object roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "JSON object roundtrip: FAIL (%s)\n" e);
(* YAML roundtrip *)
-
let yaml_result = Result.bind
-
(Yamlt.encode_string M.company_codec original)
-
(Yamlt.decode_string M.company_codec) in
-
(match yaml_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "YAML object roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "YAML object roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "YAML object roundtrip: FAIL (%s)\n" e)
+
let yaml_result =
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode M.company_codec original ~eod:true writer with
+
| Ok () ->
+
let yaml = Buffer.contents b in
+
Yamlt.decode M.company_codec (Bytes.Reader.of_string yaml)
+
| Error e -> Error e
+
in
+
match yaml_result with
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "YAML object roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "YAML object roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "YAML object roundtrip: FAIL (%s)\n" e
(* Test: Roundtrip with optionals *)
let test_optional_roundtrip () =
let module M = struct
-
type data = { required: string; optional: int option; nullable: string option }
+
type data = {
+
required : string;
+
optional : int option;
+
nullable : string option;
+
}
let data_codec =
-
Jsont.Object.map ~kind:"Data" (fun required optional nullable -> { required; optional; nullable })
+
Jsont.Object.map ~kind:"Data" (fun required optional nullable ->
+
{ required; optional; nullable })
|> Jsont.Object.mem "required" Jsont.string ~enc:(fun d -> d.required)
|> Jsont.Object.opt_mem "optional" Jsont.int ~enc:(fun d -> d.optional)
-
|> Jsont.Object.mem "nullable" (Jsont.some Jsont.string) ~enc:(fun d -> d.nullable)
+
|> Jsont.Object.mem "nullable" (Jsont.some Jsont.string) ~enc:(fun d ->
+
d.nullable)
|> Jsont.Object.finish
let equal d1 d2 =
-
d1.required = d2.required && d1.optional = d2.optional && d1.nullable = d2.nullable
+
d1.required = d2.required && d1.optional = d2.optional
+
&& d1.nullable = d2.nullable
end in
-
let original = { M.required = "test"; optional = Some 42; nullable = None } in
(* JSON roundtrip *)
-
let json_result = Result.bind
-
(Jsont_bytesrw.encode_string M.data_codec original)
-
(Jsont_bytesrw.decode_string M.data_codec) in
+
let json_result =
+
Result.bind
+
(Jsont_bytesrw.encode_string M.data_codec original)
+
(Jsont_bytesrw.decode_string M.data_codec)
+
in
(match json_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "JSON optional roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "JSON optional roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "JSON optional roundtrip: FAIL (%s)\n" e);
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "JSON optional roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "JSON optional roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "JSON optional roundtrip: FAIL (%s)\n" e);
(* YAML roundtrip *)
-
let yaml_result = Result.bind
-
(Yamlt.encode_string M.data_codec original)
-
(Yamlt.decode_string M.data_codec) in
-
(match yaml_result with
-
| Ok decoded when M.equal original decoded -> Printf.printf "YAML optional roundtrip: PASS\n"
-
| Ok _ -> Printf.printf "YAML optional roundtrip: FAIL (data mismatch)\n"
-
| Error e -> Printf.printf "YAML optional roundtrip: FAIL (%s)\n" e)
+
let yaml_result =
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode M.data_codec original ~eod:true writer with
+
| Ok () ->
+
let yaml = Buffer.contents b in
+
Yamlt.decode M.data_codec (Bytes.Reader.of_string yaml)
+
| Error e -> Error e
+
in
+
match yaml_result with
+
| Ok decoded when M.equal original decoded ->
+
Printf.printf "YAML optional roundtrip: PASS\n"
+
| Ok _ -> Printf.printf "YAML optional roundtrip: FAIL (data mismatch)\n"
+
| Error e -> Printf.printf "YAML optional roundtrip: FAIL (%s)\n" e
let () =
let usage = "Usage: test_roundtrip <command>" in
···
| "array" -> test_array_roundtrip ()
| "object" -> test_object_roundtrip ()
| "optional" -> test_optional_roundtrip ()
-
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
+105 -100
tests/bin/test_scalars.ml
···
(*---------------------------------------------------------------------------
-
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
-
SPDX-License-Identifier: ISC
-
---------------------------------------------------------------------------*)
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
(** Test scalar type resolution with Yamlt codec *)
+
open Bytesrw
+
(* Helper to read file *)
let read_file path =
let ic = open_in path in
···
in
(* Try decoding as null *)
-
let result = Yamlt.decode_string null_codec yaml in
+
let result = Yamlt.decode null_codec (Bytes.Reader.of_string yaml) in
show_result "null_codec" (Result.map (fun () -> "null") result)
(* Test: Boolean type-directed resolution *)
···
Printf.printf "=== Bool Codec ===\n";
let json_result = Jsont_bytesrw.decode_string bool_codec json in
-
let yaml_result = Yamlt.decode_string bool_codec yaml in
+
let yaml_result = Yamlt.decode bool_codec (Bytes.Reader.of_string yaml) in
show_result_json "bool_codec"
(Result.map (Printf.sprintf "%b") json_result)
(Result.map (Printf.sprintf "%b") yaml_result);
Printf.printf "\n=== String Codec ===\n";
let json_result = Jsont_bytesrw.decode_string string_codec json in
-
let yaml_result = Yamlt.decode_string string_codec yaml in
+
let yaml_result = Yamlt.decode string_codec (Bytes.Reader.of_string yaml) in
show_result_json "string_codec"
(Result.map (Printf.sprintf "%S") json_result)
(Result.map (Printf.sprintf "%S") yaml_result)
···
in
let json_result = Jsont_bytesrw.decode_string number_codec json in
-
let yaml_result = Yamlt.decode_string number_codec yaml in
+
let yaml_result = Yamlt.decode number_codec (Bytes.Reader.of_string yaml) in
show_result_json "number_codec"
(Result.map (Printf.sprintf "%.17g") json_result)
···
in
let json_result = Jsont_bytesrw.decode_string string_codec json in
-
let yaml_result = Yamlt.decode_string string_codec yaml in
+
let yaml_result = Yamlt.decode string_codec (Bytes.Reader.of_string yaml) in
show_result_json "string_codec"
(Result.map (Printf.sprintf "%S") json_result)
···
|> Jsont.Object.finish
in
-
let result = Yamlt.decode_string number_codec yaml in
+
let result = Yamlt.decode number_codec (Bytes.Reader.of_string yaml) in
match result with
| Ok f ->
-
if Float.is_nan f then
-
Printf.printf "value: NaN\n"
-
else if f = Float.infinity then
-
Printf.printf "value: +Infinity\n"
-
else if f = Float.neg_infinity then
-
Printf.printf "value: -Infinity\n"
-
else
-
Printf.printf "value: %.17g\n" f
-
| Error e ->
-
Printf.printf "ERROR: %s\n" e
+
if Float.is_nan f then Printf.printf "value: NaN\n"
+
else if f = Float.infinity then Printf.printf "value: +Infinity\n"
+
else if f = Float.neg_infinity then Printf.printf "value: -Infinity\n"
+
else Printf.printf "value: %.17g\n" f
+
| Error e -> Printf.printf "ERROR: %s\n" e
(* Test: Type mismatch errors *)
let test_type_mismatch file expected_type =
let yaml = read_file file in
match expected_type with
-
| "bool" ->
-
let codec =
-
Jsont.Object.map ~kind:"BoolTest" (fun b -> b)
-
|> Jsont.Object.mem "value" Jsont.bool ~enc:(fun b -> b)
-
|> Jsont.Object.finish
-
in
-
let result = Yamlt.decode_string codec yaml in
-
(match result with
-
| Ok _ -> Printf.printf "Unexpected success\n"
-
| Error e -> Printf.printf "Expected error: %s\n" e)
-
| "number" ->
-
let codec =
-
Jsont.Object.map ~kind:"NumberTest" (fun n -> n)
-
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
-
|> Jsont.Object.finish
-
in
-
let result = Yamlt.decode_string codec yaml in
-
(match result with
-
| Ok _ -> Printf.printf "Unexpected success\n"
-
| Error e -> Printf.printf "Expected error: %s\n" e)
-
| "null" ->
-
let codec =
-
Jsont.Object.map ~kind:"NullTest" (fun n -> n)
-
|> Jsont.Object.mem "value" (Jsont.null ()) ~enc:(fun n -> n)
-
|> Jsont.Object.finish
-
in
-
let result = Yamlt.decode_string codec yaml in
-
(match result with
-
| Ok _ -> Printf.printf "Unexpected success\n"
-
| Error e -> Printf.printf "Expected error: %s\n" e)
-
| _ -> failwith "unknown type"
+
| "bool" -> (
+
let codec =
+
Jsont.Object.map ~kind:"BoolTest" (fun b -> b)
+
|> Jsont.Object.mem "value" Jsont.bool ~enc:(fun b -> b)
+
|> Jsont.Object.finish
+
in
+
let result = Yamlt.decode codec (Bytes.Reader.of_string yaml) in
+
match result with
+
| Ok _ -> Printf.printf "Unexpected success\n"
+
| Error e -> Printf.printf "Expected error: %s\n" e)
+
| "number" -> (
+
let codec =
+
Jsont.Object.map ~kind:"NumberTest" (fun n -> n)
+
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
let result = Yamlt.decode codec (Bytes.Reader.of_string yaml) in
+
match result with
+
| Ok _ -> Printf.printf "Unexpected success\n"
+
| Error e -> Printf.printf "Expected error: %s\n" e)
+
| "null" -> (
+
let codec =
+
Jsont.Object.map ~kind:"NullTest" (fun n -> n)
+
|> Jsont.Object.mem "value" (Jsont.null ()) ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
let result = Yamlt.decode codec (Bytes.Reader.of_string yaml) in
+
match result with
+
| Ok _ -> Printf.printf "Unexpected success\n"
+
| Error e -> Printf.printf "Expected error: %s\n" e)
+
| _ -> failwith "unknown type"
(* Test: Decode with Jsont.json to see auto-resolution *)
let test_any_resolution file =
···
in
let json_result = Jsont_bytesrw.decode_string any_codec json in
-
let yaml_result = Yamlt.decode_string any_codec yaml in
+
let yaml_result = Yamlt.decode any_codec (Bytes.Reader.of_string yaml) in
(* Just show that it decoded successfully *)
show_result_json "any_codec"
···
(* Test: Encoding to different formats *)
let test_encode_formats value_type value =
match value_type with
-
| "bool" ->
+
| "bool" -> (
let codec =
Jsont.Object.map ~kind:"BoolTest" (fun b -> b)
|> Jsont.Object.mem "value" Jsont.bool ~enc:(fun b -> b)
···
in
let v = bool_of_string value in
(match Jsont_bytesrw.encode_string codec v with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Block codec v with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Flow codec v with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
-
| "number" ->
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
| "number" -> (
let codec =
Jsont.Object.map ~kind:"NumberTest" (fun n -> n)
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
···
in
let v = float_of_string value in
(match Jsont_bytesrw.encode_string codec v with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Block codec v with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Flow codec v with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
-
| "string" ->
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
| "string" -> (
let codec =
Jsont.Object.map ~kind:"StringTest" (fun s -> s)
|> Jsont.Object.mem "value" Jsont.string ~enc:(fun s -> s)
···
in
let v = value in
(match Jsont_bytesrw.encode_string codec v with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Block codec v with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Flow codec v with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
-
| "null" ->
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
| "null" -> (
let codec =
Jsont.Object.map ~kind:"NullTest" (fun n -> n)
|> Jsont.Object.mem "value" (Jsont.null ()) ~enc:(fun n -> n)
···
in
let v = () in
(match Jsont_bytesrw.encode_string codec v with
-
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
-
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Block codec v with
-
| Ok s -> Printf.printf "YAML Block:\n%s" s
+
| Ok s -> Printf.printf "JSON: %s\n" (String.trim s)
+
| Error e -> Printf.printf "JSON ERROR: %s\n" e);
+
(let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Block codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Block:\n%s" (Buffer.contents b)
| Error e -> Printf.printf "YAML Block ERROR: %s\n" e);
-
(match Yamlt.encode_string ~format:Yamlt.Flow codec v with
-
| Ok s -> Printf.printf "YAML Flow: %s" s
-
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
+
let b = Buffer.create 256 in
+
let writer = Bytes.Writer.of_buffer b in
+
match Yamlt.encode ~format:Yamlt.Flow codec v ~eod:true writer with
+
| Ok () -> Printf.printf "YAML Flow: %s" (Buffer.contents b)
+
| Error e -> Printf.printf "YAML Flow ERROR: %s\n" e)
| _ -> failwith "unknown type"
let () =
···
end;
match Sys.argv.(1) with
-
| "null" when Array.length Sys.argv = 3 ->
-
test_null_resolution Sys.argv.(2)
-
-
| "bool" when Array.length Sys.argv = 3 ->
-
test_bool_resolution Sys.argv.(2)
-
+
| "null" when Array.length Sys.argv = 3 -> test_null_resolution Sys.argv.(2)
+
| "bool" when Array.length Sys.argv = 3 -> test_bool_resolution Sys.argv.(2)
| "number" when Array.length Sys.argv = 3 ->
test_number_resolution Sys.argv.(2)
-
| "string" when Array.length Sys.argv = 3 ->
test_string_resolution Sys.argv.(2)
-
| "special-float" when Array.length Sys.argv = 3 ->
test_special_floats Sys.argv.(2)
-
| "type-mismatch" when Array.length Sys.argv = 4 ->
test_type_mismatch Sys.argv.(2) Sys.argv.(3)
-
-
| "any" when Array.length Sys.argv = 3 ->
-
test_any_resolution Sys.argv.(2)
-
+
| "any" when Array.length Sys.argv = 3 -> test_any_resolution Sys.argv.(2)
| "encode" when Array.length Sys.argv = 4 ->
test_encode_formats Sys.argv.(2) Sys.argv.(3)
-
| _ ->
prerr_endline usage;
prerr_endline "Commands:";
prerr_endline " null <file> - Test null resolution";
-
prerr_endline " bool <file> - Test bool vs string resolution";
+
prerr_endline
+
" bool <file> - Test bool vs string resolution";
prerr_endline " number <file> - Test number resolution";
prerr_endline " string <file> - Test string resolution";
prerr_endline " special-float <file> - Test .inf, .nan, etc.";
-
prerr_endline " type-mismatch <file> <type> - Test error on type mismatch";
-
prerr_endline " any <file> - Test Jsont.any auto-resolution";
+
prerr_endline
+
" type-mismatch <file> <type> - Test error on type mismatch";
+
prerr_endline
+
" any <file> - Test Jsont.any auto-resolution";
prerr_endline " encode <type> <value> - Test encoding to JSON/YAML";
exit 1
+20 -14
tests/bin/test_some_vs_option.ml
···
+
open Bytesrw
+
let () =
(* Using Jsont.some like opt_mem does *)
let codec1 =
Jsont.Object.map ~kind:"Test" (fun arr -> arr)
-
|> Jsont.Object.mem "values" (Jsont.some (Jsont.array Jsont.string)) ~enc:(fun arr -> arr)
+
|> Jsont.Object.mem "values"
+
(Jsont.some (Jsont.array Jsont.string))
+
~enc:(fun arr -> arr)
|> Jsont.Object.finish
in
let yaml = "values: [a, b, c]" in
Printf.printf "Test 1: Jsont.some (Jsont.array) - like opt_mem:\n";
-
(match Yamlt.decode_string codec1 yaml with
-
| Ok arr ->
-
(match arr with
-
| None -> Printf.printf "Result: None\n"
-
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
-
| Error e -> Printf.printf "Error: %s\n" e);
+
(match Yamlt.decode codec1 (Bytes.Reader.of_string yaml) with
+
| Ok arr -> (
+
match arr with
+
| None -> Printf.printf "Result: None\n"
+
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
+
| Error e -> Printf.printf "Error: %s\n" e);
(* Using Jsont.option *)
let codec2 =
Jsont.Object.map ~kind:"Test" (fun arr -> arr)
-
|> Jsont.Object.mem "values" (Jsont.option (Jsont.array Jsont.string)) ~enc:(fun arr -> arr)
+
|> Jsont.Object.mem "values"
+
(Jsont.option (Jsont.array Jsont.string))
+
~enc:(fun arr -> arr)
|> Jsont.Object.finish
in
Printf.printf "\nTest 2: Jsont.option (Jsont.array):\n";
-
(match Yamlt.decode_string codec2 yaml with
-
| Ok arr ->
-
(match arr with
-
| None -> Printf.printf "Result: None\n"
-
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
-
| Error e -> Printf.printf "Error: %s\n" e)
+
match Yamlt.decode codec2 (Bytes.Reader.of_string yaml) with
+
| Ok arr -> (
+
match arr with
+
| None -> Printf.printf "Result: None\n"
+
| Some a -> Printf.printf "Result: Some([%d items])\n" (Array.length a))
+
| Error e -> Printf.printf "Error: %s\n" e
+12 -7
tests/cram/arrays_codec.t
···
File "-", line 1, characters 11-22: array<string>
File "-": in member values of
File "-", line 1, characters 0-22: Nullable object
-
YAML: nullable_array: ["hello"; "null"; "world"; "null"; "test"]
+
YAML: nullable_array: ERROR: Expected string but found null
+
File "-":
+
at index 1 of
+
File "-": array<string>
+
File "-": in member values of
+
File "-": Nullable object
================================================================================
ERROR HANDLING
···
JSON: {"numbers":[1,2,3,4,5],"strings":["hello","world"]}
YAML Block:
numbers:
-
- 1.0
-
- 2.0
-
- 3.0
-
- 4.0
-
- 5.0
+
- 1
+
- 2
+
- 3
+
- 4
+
- 5
strings:
- hello
- world
-
YAML Flow: {numbers: [1.0, 2.0, 3.0, 4.0, 5.0]strings, [hello, world]}
+
YAML Flow: {numbers: [1, 2, 3, 4, 5], strings: [hello, world]}
================================================================================
NEGATIVE TESTS - Wrong File Types
+1 -4
tests/cram/complex_codec.t
···
$ test_complex complex-optional ../data/complex/complex_optional.yml
JSON: complex_optional: host="example.com", port=443, ssl=true, fallbacks=2
-
YAML: complex_optional: ERROR: Expected array<string> but found sequence
-
File "-":
-
File "-": in member fallback_hosts of
-
File "-": Config object
+
YAML: complex_optional: host="example.com", port=443, ssl=true, fallbacks=2
================================================================================
HETEROGENEOUS DATA
+3 -1
tests/cram/dune
···
(glob_files ../data/complex/*.yml)
(glob_files ../data/complex/*.json)
(glob_files ../data/edge/*.yml)
-
(glob_files ../data/edge/*.json)))
+
(glob_files ../data/edge/*.json)
+
(glob_files ../data/multidoc/*.yml)
+
(glob_files ../data/locations/*.yml)))
+10 -10
tests/cram/formats_codec.t
···
$ test_formats number-formats ../data/formats/number_formats.yml
JSON: number_formats: hex=255, octal=63, binary=10
-
YAML: number_formats: ERROR: Expected number but found scalar 0o77
-
File "-":
-
File "-": in member octal of
-
File "-": Numbers object
+
YAML: number_formats: hex=255, octal=63, binary=10
================================================================================
COMMENTS
···
File "-", line 1, characters 10-11:
File "-": in member value of
File "-", line 1, characters 0-11: Wrapper object
-
YAML: empty_document: value=Some("null")
+
YAML: empty_document: ERROR: Expected string but found null
+
File "-":
+
File "-": in member value of
+
File "-": Wrapper object
================================================================================
EXPLICIT TYPE TAGS
···
YAML Block:
name: test
values:
-
- 1.0
-
- 2.0
-
- 3.0
+
- 1
+
- 2
+
- 3
nested:
enabled: true
-
count: 5.0
+
count: 5
YAML Flow:
-
{name: test, values: [1.0, 2.0, 3.0]nested, {enabled: true, count: 5.0}}
+
{name: test, values: [1, 2, 3], nested: {enabled: true, count: 5}}
================================================================================
+217
tests/cram/locations.t
···
+
Location and Layout Preservation Tests with Yamlt
+
==================================================
+
+
This test suite validates the `locs` and `layout` options in the Yamlt decoder,
+
demonstrating how they affect error messages and metadata preservation.
+
+
================================================================================
+
ERROR MESSAGE PRECISION - locs option
+
================================================================================
+
+
The `locs` option controls whether source locations are preserved in error messages.
+
When `locs=false` (default), errors show basic location info.
+
When `locs=true`, errors show precise character positions.
+
+
Basic type error with and without locs
+
+
$ test_locations error-precision ../data/locations/type_error.yml
+
=== Without locs (default) ===
+
Error message:
+
String "not-a-number" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
+
=== With locs=true ===
+
Error message:
+
String "not-a-number" does not parse to OCaml int value
+
File "-", line 2, characters 5-18:
+
File "-", line 2, characters 0-3: in member age of
+
File "-", line 1, characters 0-1: Person object
+
+
================================================================================
+
NESTED ERROR LOCATIONS
+
================================================================================
+
+
The `locs` option is especially useful for nested structures,
+
showing exactly where deep errors occur.
+
+
Error in nested object field
+
+
$ test_locations nested-error ../data/locations/nested_error.yml
+
=== Without locs (default) ===
+
Nested error:
+
String "invalid-zip" does not parse to OCaml int value
+
File "-":
+
File "-": in member zip of
+
File "-": Address object
+
File "-": in member address of
+
File "-": Employee object
+
+
=== With locs=true ===
+
Nested error:
+
String "invalid-zip" does not parse to OCaml int value
+
File "-", line 5, characters 7-19:
+
File "-", line 5, characters 2-5: in member zip of
+
File "-", line 3, characters 2-3: Address object
+
File "-", line 2, characters 0-7: in member address of
+
File "-", line 1, characters 0-1: Employee object
+
+
================================================================================
+
ARRAY ELEMENT ERROR LOCATIONS
+
================================================================================
+
+
The `locs` option pinpoints which array element caused an error.
+
+
Error at specific array index
+
+
$ test_locations array-error ../data/locations/array_error.yml
+
=== Without locs (default) ===
+
Array error:
+
String "not-a-number" does not parse to OCaml int value
+
File "-":
+
at index 2 of
+
File "-": array<OCaml int>
+
File "-": in member values of
+
File "-": Numbers object
+
+
=== With locs=true ===
+
Array error:
+
String "not-a-number" does not parse to OCaml int value
+
File "-", lines 4-5, characters 4-2:
+
at index 2 of
+
File "-", line 2, characters 2-3: array<OCaml int>
+
File "-", line 1, characters 0-6: in member values of
+
File "-", line 1, characters 0-1: Numbers object
+
+
================================================================================
+
FILE PATH IN ERROR MESSAGES
+
================================================================================
+
+
The `file` parameter sets the file path shown in error messages.
+
+
$ test_locations file-path
+
=== Without file path ===
+
Error:
+
String "not-a-number" does not parse to OCaml int value
+
File "-", line 2, characters 5-18:
+
File "-", line 2, characters 0-3: in member age of
+
File "-", line 1, characters 0-1: Person object
+
+
=== With file path ===
+
Error:
+
String "not-a-number" does not parse to OCaml int value
+
File "test.yml", line 2, characters 5-18:
+
File "test.yml", line 2, characters 0-3: in member age of
+
File "test.yml", line 1, characters 0-1: Person object
+
+
================================================================================
+
MISSING FIELD ERROR LOCATIONS
+
================================================================================
+
+
The `locs` option helps identify where fields are missing.
+
+
$ test_locations missing-field ../data/locations/missing_field.yml
+
=== Without locs ===
+
Missing field:
+
Missing member field_c in Complete object
+
File "-":
+
+
=== With locs=true ===
+
Missing field:
+
Missing member field_c in Complete object
+
File "-", line 1, characters 0-1:
+
+
================================================================================
+
LAYOUT PRESERVATION - layout option
+
================================================================================
+
+
The `layout` option controls whether style information (block vs flow)
+
is preserved in metadata for potential round-tripping.
+
+
Basic layout preservation
+
+
$ test_locations layout ../data/locations/simple.yml
+
=== Without layout (default) ===
+
Decoded: host=localhost, port=8080
+
Meta preserved: no
+
+
=== With layout=true ===
+
Decoded: host=localhost, port=8080
+
Meta preserved: yes (style info available for round-tripping)
+
+
================================================================================
+
ROUND-TRIPPING WITH LAYOUT
+
================================================================================
+
+
With `layout=true` during decode and `format:Layout` during encode,
+
the original YAML style can be preserved.
+
+
Flow style preservation
+
+
$ test_locations roundtrip ../data/locations/flow_style.yml
+
=== Original YAML ===
+
items: [apple, banana, cherry]
+
+
=== Decode without layout, re-encode ===
+
items:
+
- apple
+
- banana
+
- cherry
+
+
=== Decode with layout=true, re-encode with Layout format ===
+
items:
+
- apple
+
- banana
+
- cherry
+
+
Block style preservation
+
+
$ test_locations roundtrip ../data/locations/block_style.yml
+
=== Original YAML ===
+
items:
+
- apple
+
- banana
+
- cherry
+
+
=== Decode without layout, re-encode ===
+
items:
+
- apple
+
- banana
+
- cherry
+
+
=== Decode with layout=true, re-encode with Layout format ===
+
items:
+
- apple
+
- banana
+
- cherry
+
+
================================================================================
+
COMBINED OPTIONS - locs and layout together
+
================================================================================
+
+
Both options can be used simultaneously for maximum information.
+
+
$ test_locations combined ../data/locations/valid_settings.yml
+
=== locs=false, layout=false (defaults) ===
+
OK: timeout=30, retries=3
+
+
=== locs=true, layout=false ===
+
OK: timeout=30, retries=3 (with precise locations)
+
+
=== locs=false, layout=true ===
+
OK: timeout=30, retries=3 (with layout metadata)
+
+
=== locs=true, layout=true (both enabled) ===
+
OK: timeout=30, retries=3 (with locations and layout)
+
+
================================================================================
+
SUMMARY OF OPTIONS
+
================================================================================
+
+
locs option:
+
+
layout option:
+
+
Both options add metadata overhead, so only enable when needed.
+
For production parsing where you only need the data, use defaults (both false).
+220
tests/cram/multidoc.t
···
+
Multi-Document YAML Streams with Yamlt
+
========================================
+
+
This test suite validates multi-document YAML stream decoding using decode_all,
+
including error handling, location tracking, and JSON roundtripping.
+
+
================================================================================
+
BASIC MULTIDOC DECODING
+
================================================================================
+
+
Simple multi-document stream with person objects
+
+
$ test_multidoc simple ../data/multidoc/simple.yml
+
Documents:
+
[0] : Alice (age 30)
+
[1] : Bob (age 25)
+
[2] : Charlie (age 35)
+
+
Count documents in a stream
+
+
$ test_multidoc count ../data/multidoc/simple.yml
+
Document count: 3
+
+
================================================================================
+
ERROR HANDLING - MIXED VALID AND INVALID DOCUMENTS
+
================================================================================
+
+
When some documents succeed and others fail, decode_all continues processing
+
and returns results for each document individually.
+
+
Stream with one error in the middle
+
+
$ test_multidoc errors ../data/multidoc/mixed_errors.yml
+
Document results:
+
[0] OK: Alice (age 30)
+
[1] ERROR: String "not-a-number" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
[2] OK: Charlie (age 35)
+
+
Summary statistics for mixed documents
+
+
$ test_multidoc summary ../data/multidoc/mixed_errors.yml
+
Summary: 3 documents (2 ok, 1 error)
+
+
Stream where all documents fail
+
+
$ test_multidoc errors ../data/multidoc/all_errors.yml
+
Document results:
+
[0] ERROR: String "invalid1" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
[1] ERROR: String "invalid2" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
[2] ERROR: String "invalid3" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
+
Summary for all-error stream
+
+
$ test_multidoc summary ../data/multidoc/all_errors.yml
+
Summary: 3 documents (0 ok, 3 error)
+
+
================================================================================
+
LOCATION TRACKING WITH locs=true
+
================================================================================
+
+
Location tracking helps identify exactly where errors occur in each document
+
of a multi-document stream.
+
+
Without locs (default) - basic error information
+
+
$ test_multidoc locations ../data/multidoc/mixed_errors.yml
+
=== Without locs (default) ===
+
[0] OK
+
[1] ERROR:
+
String "not-a-number" does not parse to OCaml int value
+
File "-":
+
File "-": in member age of
+
File "-": Person object
+
[2] OK
+
+
=== With locs=true ===
+
[0] OK
+
[1] ERROR:
+
String "not-a-number" does not parse to OCaml int value
+
File "test.yml", line 6, characters 5-18:
+
File "test.yml", line 6, characters 0-3: in member age of
+
File "test.yml", line 5, characters 0-1: Person object
+
[2] OK
+
+
================================================================================
+
MISSING FIELDS IN MULTIDOC
+
================================================================================
+
+
Documents with missing required fields generate errors but don't stop
+
processing of subsequent documents.
+
+
$ test_multidoc errors ../data/multidoc/missing_fields.yml
+
Document results:
+
[0] OK: Alice (age 30)
+
[1] ERROR: Missing member age in Person object
+
File "-":
+
[2] OK: Charlie (age 35)
+
+
Summary of missing fields test
+
+
$ test_multidoc summary ../data/multidoc/missing_fields.yml
+
Summary: 3 documents (2 ok, 1 error)
+
+
================================================================================
+
JSON ROUNDTRIPPING
+
================================================================================
+
+
Decode YAML multi-document streams and encode each document as JSON.
+
This validates that the data model conversion is correct.
+
+
Simple documents to JSON
+
+
$ test_multidoc json ../data/multidoc/simple.yml
+
JSON outputs:
+
[0] {"name":"Alice","age":30}
+
[1] {"name":"Bob","age":25}
+
[2] {"name":"Charlie","age":35}
+
+
Nested objects to JSON
+
+
$ test_multidoc json ../data/multidoc/nested.yml
+
JSON outputs:
+
[0] {"name":"Alice","age":30,"address":{"street":"123 Main St","city":"Boston"}}
+
[1] {"name":"Bob","age":25,"address":{"street":"456 Oak Ave","city":"Seattle"}}
+
[2] {"name":"Charlie","age":35,"address":{"street":"789 Pine Rd","city":"Portland"}}
+
+
Arrays to JSON
+
+
$ test_multidoc json ../data/multidoc/arrays.yml
+
JSON outputs:
+
[0] [1,2,3]
+
[1] ["apple","banana","cherry"]
+
[2] [true,false,true]
+
+
Scalar values to JSON
+
+
$ test_multidoc json ../data/multidoc/scalars.yml
+
JSON outputs:
+
[0] "hello world"
+
[1] 42
+
[2] true
+
[3] null
+
+
================================================================================
+
NESTED OBJECTS IN MULTIDOC
+
================================================================================
+
+
Test decoding complex nested structures across multiple documents.
+
+
$ test_multidoc nested ../data/multidoc/nested.yml
+
Nested documents:
+
[0] : Alice (age 30) from 123 Main St, Boston
+
[1] : Bob (age 25) from 456 Oak Ave, Seattle
+
[2] : Charlie (age 35) from 789 Pine Rd, Portland
+
+
================================================================================
+
ARRAYS IN MULTIDOC
+
================================================================================
+
+
Test decoding different array types across documents.
+
+
$ test_multidoc arrays ../data/multidoc/arrays.yml
+
Array documents:
+
[0] [1,2,3]
+
[1] ["apple","banana","cherry"]
+
[2] [true,false,true]
+
+
================================================================================
+
SCALARS IN MULTIDOC
+
================================================================================
+
+
Test decoding bare scalar values as documents.
+
+
$ test_multidoc scalars ../data/multidoc/scalars.yml
+
Scalar documents:
+
[0] "hello world"
+
[1] 42
+
[2] true
+
[3] null
+
+
================================================================================
+
EMPTY DOCUMENTS
+
================================================================================
+
+
Empty or null documents in a stream are handled correctly.
+
+
$ test_multidoc json ../data/multidoc/empty_docs.yml
+
JSON outputs:
+
[0] {"name":"Alice","age":30}
+
[1] null
+
[2] {"name":"Charlie","age":35}
+
+
Count including empty documents
+
+
$ test_multidoc count ../data/multidoc/empty_docs.yml
+
Document count: 3
+
+
================================================================================
+
SUMMARY
+
================================================================================
+
+
The decode_all function:
+
- Processes all documents in a stream, not stopping on errors
+
- Returns a sequence of Result values (Ok/Error for each document)
+
- Supports all decode options: locs, layout, file, max_depth, max_nodes
+
- Correctly handles document boundaries even when errors occur
+
- Works with any Jsont codec (objects, arrays, scalars, etc.)
+
- Can be used for JSON roundtripping and format conversion
+37
tests/cram/null_collections.t
···
+
Null to Empty Collection Tests
+
================================
+
+
This test suite validates that yamlt treats null values as empty collections
+
when decoding into Array or Object types, providing a more user-friendly
+
YAML experience.
+
+
================================================================================
+
NULL AS EMPTY COLLECTION
+
================================================================================
+
+
Test various forms of null decoding as empty arrays and objects
+
+
$ test_null_collections
+
=== Test 1: Explicit null as empty array ===
+
Result: []
+
+
=== Test 2: Tilde as empty array ===
+
Result: []
+
+
=== Test 3: Empty array syntax ===
+
Result: []
+
+
=== Test 4: Array with values ===
+
Result: [1; 2; 3]
+
+
=== Test 5: Explicit null as empty object ===
+
Result: {timeout=30; retries=3}
+
+
=== Test 6: Empty object syntax ===
+
Result: {timeout=30; retries=3}
+
+
=== Test 7: Object with values ===
+
Result: {timeout=60; retries=5}
+
+
=== Test 8: Nested null arrays ===
+
Result: {name=test; items_count=0; tags_count=0}
+2 -2
tests/cram/objects_codec.t
···
JSON: {"name":"Alice","age":30,"active":true}
YAML Block:
name: Alice
-
age: 30.0
+
age: 30
active: true
-
YAML Flow: {name: Alice, age: 30.0, active: true}
+
YAML Flow: {name: Alice, age: 30, active: true}
================================================================================
NEGATIVE TESTS - Wrong File Types
+1 -4
tests/cram/scalars_codec.t
···
JSON number_codec
decode: 42
YAML number_codec
-
decode: ERROR: Expected number but found scalar 0o52
-
File "-":
-
File "-": in member value of
-
File "-": NumberTest object
+
decode: 42
Negative numbers
+6
tests/data/locations/array_error.yml
···
+
values:
+
- 1
+
- 2
+
- not-a-number
+
- 4
+
- 5
+4
tests/data/locations/block_style.yml
···
+
items:
+
- apple
+
- banana
+
- cherry
+1
tests/data/locations/flow_style.yml
···
+
items: [apple, banana, cherry]
+2
tests/data/locations/missing_field.yml
···
+
field_a: hello
+
field_b: 42
+5
tests/data/locations/nested_error.yml
···
+
name: Bob
+
address:
+
street: 123 Main St
+
city: Springfield
+
zip: invalid-zip
+2
tests/data/locations/simple.yml
···
+
host: localhost
+
port: 8080
+2
tests/data/locations/type_error.yml
···
+
name: Alice
+
age: not-a-number
+2
tests/data/locations/valid_settings.yml
···
+
timeout: 30
+
retries: 3
+9
tests/data/multidoc/all_errors.yml
···
+
---
+
name: Alice
+
age: invalid1
+
---
+
name: Bob
+
age: invalid2
+
---
+
name: Charlie
+
age: invalid3
+12
tests/data/multidoc/arrays.yml
···
+
---
+
- 1
+
- 2
+
- 3
+
---
+
- apple
+
- banana
+
- cherry
+
---
+
- true
+
- false
+
- true
+7
tests/data/multidoc/empty_docs.yml
···
+
---
+
name: Alice
+
age: 30
+
---
+
---
+
name: Charlie
+
age: 35
+9
tests/data/multidoc/missing_fields.yml
···
+
---
+
name: Alice
+
age: 30
+
---
+
name: Bob
+
---
+
name: Charlie
+
age: 35
+
city: Springfield
+9
tests/data/multidoc/mixed_errors.yml
···
+
---
+
name: Alice
+
age: 30
+
---
+
name: Bob
+
age: not-a-number
+
---
+
name: Charlie
+
age: 35
+18
tests/data/multidoc/nested.yml
···
+
---
+
name: Alice
+
age: 30
+
address:
+
street: 123 Main St
+
city: Boston
+
---
+
name: Bob
+
age: 25
+
address:
+
street: 456 Oak Ave
+
city: Seattle
+
---
+
name: Charlie
+
age: 35
+
address:
+
street: 789 Pine Rd
+
city: Portland
+8
tests/data/multidoc/scalars.yml
···
+
---
+
hello world
+
---
+
42
+
---
+
true
+
---
+
null
+9
tests/data/multidoc/simple.yml
···
+
---
+
name: Alice
+
age: 30
+
---
+
name: Bob
+
age: 25
+
---
+
name: Charlie
+
age: 35
+5
yamlt.opam
···
synopsis: "YAML codec using Jsont type descriptions"
description:
"Allows the same Jsont.t codec definitions to work for both JSON and YAML"
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.org/@anil.recoil.org/ocaml-yamlt"
+
bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-yamlt/issues"
depends: [
"dune" {>= "3.18"}
"ocaml" {>= "4.14.0"}