Yaml encoder/decoder for OCaml jsont codecs

init import

Changed files
+4244
lib
tests
bin
cram
data
arrays
complex
edge
formats
objects
scalars
+2
.gitignore
···
+
_build
+
ocaml-yamlrw
+14
dune-project
···
+
(lang dune 3.18)
+
(name yamlt)
+
+
(generate_opam_files true)
+
+
(package
+
(name yamlt)
+
(synopsis "YAML codec using Jsont type descriptions")
+
(description "Allows the same Jsont.t codec definitions to work for both JSON and YAML")
+
(depends
+
(ocaml (>= 4.14.0))
+
yamlrw
+
jsont
+
bytesrw))
+4
lib/dune
···
+
(library
+
(name yamlt)
+
(public_name yamlt)
+
(libraries yamlrw jsont bytesrw))
+869
lib/yamlt.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
open Bytesrw
+
open Jsont.Repr
+
open Yamlrw
+
+
(* YAML format *)
+
+
type yaml_format = Block | Flow | Layout
+
+
(* Decoder *)
+
+
type decoder = {
+
parser : Parser.t;
+
file : string;
+
locs : bool;
+
_layout : bool; (* For future layout preservation *)
+
max_depth : int;
+
max_nodes : int;
+
mutable node_count : int;
+
mutable current : Event.spanned option;
+
_anchors : (string, Jsont.json) Hashtbl.t; (* For future anchor resolution *)
+
meta_none : Jsont.Meta.t;
+
}
+
+
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 }
+
+
(* Decoder helpers *)
+
+
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
+
+
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
+
+
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
+
+
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
+
+
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 ->
+
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
+
+
(* 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
+
+
let err_type_mismatch d span t ~fnd =
+
let meta = meta_of_span d span in
+
Jsont.Error.msgf meta "Expected %s but found %s"
+
(Jsont.Repr.kinded_sort t) fnd
+
+
(* YAML scalar resolution *)
+
+
let is_null_scalar s =
+
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
+
| _ -> None
+
+
let float_of_scalar_opt s =
+
(* Handle YAML special floats *)
+
match s with
+
| ".inf" | ".Inf" | ".INF" -> Some Float.infinity
+
| "+.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'
+
+
let _int_of_scalar_opt s =
+
(* Handle hex, octal, and regular integers with underscores *)
+
let s' = String.concat "" (String.split_on_char '_' s) in
+
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 ->
+
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
+
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))
+
| 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
+
err_type_mismatch d ev.span t ~fnd:"null"
+
else
+
(* Strings accept any non-null scalar value *)
+
map.dec meta value
+
| 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"
+
+
(* Forward declaration for mutual recursion *)
+
let rec decode : type a. decoder -> nest:int -> a t -> a =
+
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
+
(* 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 -> ());
+
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)
+
+
(* Recursive types - must come before specific event matches *)
+
| _, 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 _, _ ->
+
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"
+
+
(* Unexpected events *)
+
| Event.Sequence_end, _ ->
+
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected sequence end"
+
| Event.Mapping_end, _ ->
+
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected mapping end"
+
| Event.Document_start _, _ ->
+
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected document start"
+
| Event.Document_end _, _ ->
+
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected document end"
+
| Event.Stream_start _, _ ->
+
Jsont.Error.msgf (meta_of_span d ev.span) "Unexpected stream start"
+
| Event.Stream_end, _ ->
+
Jsont.Error.msgf (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 ->
+
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 ->
+
check_nodes d;
+
(* 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
+
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 ->
+
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 ->
+
(* 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
+
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
+
+
and decode_alias : type a. decoder -> Event.spanned -> string -> a t -> a =
+
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 ->
+
(* Decode the stored JSON value through the type *)
+
let t' = Jsont.Repr.unsafe_to_t t in
+
match Jsont.Json.decode' t' json 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 *)
+
check_nodes d;
+
let meta = meta_of_span d start_ev.span in
+
let builder = ref (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
+
| Some _ ->
+
let i = !idx in
+
(try
+
if 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
+
()
+
end else begin
+
let elt = decode d ~nest:(nest + 1) map.elt in
+
builder := 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);
+
incr idx;
+
loop ()
+
| None ->
+
Jsont.Error.msgf 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 ->
+
match map.dec_array with
+
| 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
+
+
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 = 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 ->
+
(* Merge expected member decoders *)
+
let u _ _ _ = assert false in
+
let mem_miss = String_map.union u mem_miss map.mem_decs in
+
match map.shape with
+
| Object_basic umems ->
+
decode_object_basic d ~nest obj_meta map umems mem_miss dict
+
| Object_cases (umems_opt, cases) ->
+
(* Wrap umems_opt to hide existential types *)
+
let umems = Unknown_mems umems_opt 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
+
let mem_miss = ref mem_miss in
+
let dict = ref dict in
+
let rec loop () =
+
match peek_event d with
+
| Some { Event.event = Event.Mapping_end; _ } ->
+
skip_event d;
+
(* Finalize *)
+
finish_object obj_meta 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));
+
loop ()
+
| None ->
+
Jsont.Error.msgf 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 ->
+
let dict = Dict.add object_meta_arg meta dict in
+
let dict = match umems with
+
| Unknown_skip | Unknown_error -> 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 =
+
match mem_map.dec_absent with
+
| Some v -> Dict.add mem_map.id v dict
+
| None -> raise Exit
+
in
+
try String_map.fold add_default mem_miss 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:[]
+
+
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 ->
+
match peek_event d with
+
| 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)
+
| 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
+
(* Not the case tag - check if known member or delay *)
+
match String_map.find_opt name 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)
+
| None ->
+
(* Unknown member - decode as generic JSON and delay *)
+
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
+
end
+
| None ->
+
Jsont.Error.msgf 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 ->
+
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
+
| 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_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 ->
+
(* 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
+
(* Then continue reading remaining members using case's own unknown handling *)
+
match case_map.shape with
+
| Object_basic case_umems ->
+
decode_object_basic d ~nest obj_meta case_map case_umems mem_miss dict
+
| Object_cases _ ->
+
(* 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 ->
+
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
+
+
and decode_mapping_key : decoder -> Event.spanned -> string * Jsont.Meta.t =
+
fun d ev ->
+
match ev.Event.event with
+
| Event.Scalar { value; _ } ->
+
skip_event d;
+
let meta = meta_of_span d ev.span in
+
(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
+
+
(* 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 ()
+
| _ -> ()
+
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 ()
+
| 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
+
in
+
loop ()
+
+
(* Public decode API *)
+
+
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
+
try
+
skip_to_content d;
+
let t' = Jsont.Repr.of_t t in
+
let v = decode d ~nest:0 t' in
+
skip_end_wrappers d;
+
Ok v
+
with
+
| 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)
+
+
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 = {
+
emitter : Emitter.t;
+
format : yaml_format;
+
_indent : int; (* Stored for future use in custom formatting *)
+
explicit_doc : bool;
+
scalar_style : Scalar_style.t;
+
}
+
+
let make_encoder
+
?(format = Block) ?(indent = 2) ?(explicit_doc = false)
+
?(scalar_style = `Any) emitter =
+
{ emitter; format; _indent = indent; explicit_doc; scalar_style }
+
+
let layout_style_of_format = function
+
| Block -> `Block
+
| Flow -> `Flow
+
| Layout -> `Any
+
+
(* Choose appropriate scalar style for a string *)
+
let choose_scalar_style ~preferred s =
+
if preferred <> `Any then preferred
+
else if String.contains s '\n' then `Literal
+
else if String.length s > 80 then `Folded
+
else `Plain
+
+
(* 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;
+
})
+
+
(* 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;
+
})
+
+
(* 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"
+
in
+
Emitter.emit e.emitter (Event.Scalar {
+
anchor = None;
+
tag = None;
+
value;
+
plain_implicit = true;
+
quoted_implicit = true;
+
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;
+
})
+
+
let rec encode : type a. encoder -> a t -> a -> unit =
+
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
+
+
| 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
+
+
and encode_array : type a elt b. encoder -> (a, elt, b) array_map -> a -> unit =
+
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
+
+
and encode_object : type o. encoder -> (o, o) object_map -> o -> unit =
+
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;
+
});
+
(* 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;
+
(* 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
+
+
(* 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 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;
+
});
+
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;
+
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)
+
+
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
+
in
+
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
+
| Error e -> Error (Jsont.Error.to_string e)
+178
lib/yamlt.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. 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.
+
+
{b Example:}
+
{[
+
(* 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
+
]}
+
+
See notes about {{!yaml_mapping}YAML to JSON mapping} and
+
{{!yaml_scalars}YAML scalar resolution}.
+
*)
+
+
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
+
(** [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].}}
+
+
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
+
(** [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_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. *)
+
+
(** {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. *)
+
| 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
+
(** [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.}} *)
+
+
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
+
(** [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 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 ->
+
(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:
+
+
{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}}
+
+
{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}}} *)
+
+
(** {1:yaml_scalars YAML Scalar Resolution}
+
+
YAML scalars are resolved to JSON types as follows:
+
+
{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 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]. *)
+51
tests/bin/dune
···
+
(executable
+
(name test_scalars)
+
(public_name test_scalars)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_objects)
+
(public_name test_objects)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_arrays)
+
(public_name test_arrays)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_formats)
+
(public_name test_formats)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_roundtrip)
+
(public_name test_roundtrip)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_complex)
+
(public_name test_complex)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(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_complete)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_opt_array)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_array_variants)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+
+
(executable
+
(name test_some_vs_option)
+
(libraries yamlt jsont jsont.bytesrw bytesrw))
+27
tests/bin/test_array_variants.ml
···
+
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.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);
+
+
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.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)
+330
tests/bin/test_arrays.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test array codec functionality with Yamlt *)
+
+
+
(* 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
+
+
let show_result_both label json_result yaml_result =
+
Printf.printf "JSON: ";
+
show_result label json_result;
+
Printf.printf "YAML: ";
+
show_result label yaml_result
+
+
(* Test: Simple int array *)
+
let test_int_array file =
+
let module M = struct
+
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.finish
+
+
let show n =
+
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
+
+
show_result_both "int_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: String array *)
+
let test_string_array file =
+
let module M = struct
+
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.finish
+
+
let show t =
+
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
+
+
show_result_both "string_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Float/number array *)
+
let test_float_array file =
+
let module M = struct
+
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.finish
+
+
let show m =
+
Printf.sprintf "[%s]"
+
(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
+
+
show_result_both "float_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Empty array *)
+
let test_empty_array file =
+
let module M = struct
+
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.finish
+
+
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
+
+
show_result_both "empty_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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 }
+
+
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 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.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
+
+
show_result_both "object_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Nested arrays *)
+
let test_nested_arrays file =
+
let module M = struct
+
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.finish
+
+
let show_row 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)))
+
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
+
+
show_result_both "nested_arrays"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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 }
+
+
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.finish
+
end in
+
+
let yaml = read_file file in
+
let result = Yamlt.decode_string M.numbers_codec 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 }
+
+
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.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
+
+
show_result_both "bool_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Array with nulls *)
+
let test_nullable_array file =
+
let module M = struct
+
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.finish
+
+
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)))
+
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
+
+
show_result_both "nullable_array"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Encoding arrays to different formats *)
+
let test_encode_arrays () =
+
let module M = struct
+
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.finish
+
end 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);
+
+
(* 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
+
| 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 () =
+
let usage = "Usage: test_arrays <command> [args...]" in
+
+
if Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
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)
+
+
| "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)
+
+
| "nullable" when Array.length Sys.argv = 3 ->
+
test_nullable_array Sys.argv.(2)
+
+
| "encode" when Array.length Sys.argv = 2 ->
+
test_encode_arrays ()
+
+
| _ ->
+
prerr_endline usage;
+
prerr_endline "Commands:";
+
prerr_endline " int <file> - Test int array";
+
prerr_endline " string <file> - Test string array";
+
prerr_endline " float <file> - Test float array";
+
prerr_endline " empty <file> - Test empty array";
+
prerr_endline " objects <file> - Test array of objects";
+
prerr_endline " nested <file> - Test nested arrays";
+
prerr_endline " type-mismatch <file> - Test type mismatch error";
+
prerr_endline " bool <file> - Test bool array";
+
prerr_endline " nullable <file> - Test array with nulls";
+
prerr_endline " encode - Test encoding arrays";
+
exit 1
+193
tests/bin/test_complex.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test complex nested types with Yamlt *)
+
+
(* 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
+
+
let show_result_both label json_result yaml_result =
+
Printf.printf "JSON: ";
+
show_result label json_result;
+
Printf.printf "YAML: ";
+
show_result label yaml_result
+
+
(* 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 }
+
+
let level3_codec =
+
Jsont.Object.map ~kind:"Level3" (fun value -> { value })
+
|> Jsont.Object.mem "value" Jsont.int ~enc:(fun l -> l.value)
+
|> Jsont.Object.finish
+
+
let level2_codec =
+
Jsont.Object.map ~kind:"Level2" (fun data -> { data })
+
|> Jsont.Object.mem "data" level3_codec ~enc:(fun l -> l.data)
+
|> Jsont.Object.finish
+
+
let level1_codec =
+
Jsont.Object.map ~kind:"Level1" (fun nested -> { nested })
+
|> Jsont.Object.mem "nested" level2_codec ~enc:(fun l -> l.nested)
+
|> Jsont.Object.finish
+
+
let root_codec =
+
Jsont.Object.map ~kind:"Root" (fun top -> { top })
+
|> Jsont.Object.mem "top" level1_codec ~enc:(fun r -> r.top)
+
|> Jsont.Object.finish
+
+
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
+
+
show_result_both "deep_nesting"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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 }
+
+
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.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.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
+
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
+
+
show_result_both "mixed_structure"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Complex optional and nullable combinations *)
+
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;
+
}
+
+
let config_codec =
+
Jsont.Object.map ~kind:"Config"
+
(fun host port ssl cert_path fallback_hosts ->
+
{ host; port; ssl; cert_path; fallback_hosts })
+
|> 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 "fallback_hosts" (Jsont.array Jsont.string)
+
~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
+
| 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
+
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
+
+
show_result_both "complex_optional"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Heterogeneous data via any type *)
+
let test_heterogeneous file =
+
let module M = struct
+
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.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
+
+
show_result_both "heterogeneous"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
let () =
+
let usage = "Usage: test_complex <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
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";
+
exit 1
+212
tests/bin/test_edge.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test edge cases with Yamlt *)
+
+
(* 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
+
+
let show_result_both label json_result yaml_result =
+
Printf.printf "JSON: ";
+
show_result label json_result;
+
Printf.printf "YAML: ";
+
show_result label yaml_result
+
+
(* Test: Very large numbers *)
+
let test_large_numbers file =
+
let module M = struct
+
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 })
+
|> 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.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
+
+
show_result_both "large_numbers"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Special characters in strings *)
+
let test_special_chars file =
+
let module M = struct
+
type text = { content: string }
+
+
let text_codec =
+
Jsont.Object.map ~kind:"Text" (fun content -> { content })
+
|> Jsont.Object.mem "content" Jsont.string ~enc:(fun t -> t.content)
+
|> Jsont.Object.finish
+
+
let show t =
+
Printf.sprintf "length=%d, contains_newline=%b, contains_tab=%b"
+
(String.length t.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
+
+
show_result_both "special_chars"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Unicode strings *)
+
let test_unicode file =
+
let module M = struct
+
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.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)
+
|> Jsont.Object.finish
+
+
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
+
+
show_result_both "unicode"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Empty collections *)
+
let test_empty_collections file =
+
let module M = struct
+
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)
+
|> Jsont.Object.finish
+
+
let show d =
+
Printf.sprintf "empty_array_len=%d, empty_object_array_len=%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
+
+
show_result_both "empty_collections"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Key names with special characters *)
+
let test_special_keys file =
+
let module M = struct
+
let show j =
+
match Jsont.Json.decode (Jsont.any ()) j with
+
| Ok (Jsont.Object _) -> "valid_object"
+
| 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
+
+
show_result_both "special_keys"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Single-element arrays *)
+
let test_single_element file =
+
let module M = struct
+
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.finish
+
+
let show d =
+
Printf.sprintf "length=%d, value=%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
+
+
show_result_both "single_element"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
let () =
+
let usage = "Usage: test_edge <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
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)
+
+
| "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 " 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 " single-element <file> - Test single-element arrays";
+
exit 1
+254
tests/bin/test_formats.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test format-specific features with Yamlt *)
+
+
(* 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
+
+
let show_result_both label json_result yaml_result =
+
Printf.printf "JSON: ";
+
show_result label json_result;
+
Printf.printf "YAML: ";
+
show_result label yaml_result
+
+
(* Test: Multi-line strings - literal style *)
+
let test_literal_string file =
+
let module M = struct
+
type text = { content: string }
+
+
let text_codec =
+
Jsont.Object.map ~kind:"Text" (fun content -> { content })
+
|> Jsont.Object.mem "content" Jsont.string ~enc:(fun t -> t.content)
+
|> Jsont.Object.finish
+
+
let show t =
+
Printf.sprintf "lines=%d, length=%d"
+
(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
+
+
show_result_both "literal_string"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Multi-line strings - folded style *)
+
let test_folded_string file =
+
let module M = struct
+
type text = { content: string }
+
+
let text_codec =
+
Jsont.Object.map ~kind:"Text" (fun content -> { content })
+
|> Jsont.Object.mem "content" Jsont.string ~enc:(fun t -> t.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))))
+
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
+
+
show_result_both "folded_string"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Number formats - hex, octal, binary *)
+
let test_number_formats file =
+
let module M = struct
+
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.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)
+
|> Jsont.Object.finish
+
+
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
+
+
show_result_both "number_formats"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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;
+
}
+
+
let nested_codec =
+
Jsont.Object.map ~kind:"Nested" (fun enabled count -> { enabled; count })
+
|> Jsont.Object.mem "enabled" Jsont.bool ~enc:(fun n -> n.enabled)
+
|> Jsont.Object.mem "count" Jsont.int ~enc:(fun n -> n.count)
+
|> Jsont.Object.finish
+
+
let data_codec =
+
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 "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
+
+
(* 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
+
| 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)
+
+
(* Test: Comments in YAML (should be ignored) *)
+
let test_comments file =
+
let module M = struct
+
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.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)
+
|> Jsont.Object.finish
+
+
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
+
+
match yaml_result with
+
| Ok v -> Printf.printf "YAML (with comments): %s\n" (M.show v)
+
| Error e -> Printf.printf "YAML ERROR: %s\n" e
+
+
(* Test: Empty documents and null documents *)
+
let test_empty_document file =
+
let module M = struct
+
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.finish
+
+
let show w =
+
match w.value with
+
| 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
+
+
show_result_both "empty_document"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Explicit typing with tags (if supported) *)
+
let test_explicit_tags file =
+
let module M = struct
+
type value_holder = { data: string }
+
+
let value_codec =
+
Jsont.Object.map ~kind:"ValueHolder" (fun data -> { data })
+
|> Jsont.Object.mem "data" Jsont.string ~enc:(fun v -> v.data)
+
|> Jsont.Object.finish
+
+
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
+
+
match yaml_result with
+
| Ok v -> Printf.printf "YAML (with tags): %s\n" (M.show v)
+
| Error e -> Printf.printf "YAML ERROR: %s\n" e
+
+
let () =
+
let usage = "Usage: test_formats <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
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 " encode-styles - Test block vs flow encoding";
+
prerr_endline " comments <file> - Test YAML with comments";
+
prerr_endline " empty-doc <file> - Test empty documents";
+
prerr_endline " explicit-tags <file> - Test explicit type tags";
+
exit 1
+33
tests/bin/test_null_complete.ml
···
+
let () =
+
Printf.printf "=== Test 1: Jsont.option with YAML null ===\n";
+
let yaml1 = "value: null" in
+
let codec1 =
+
let open Jsont in
+
Object.map ~kind:"Test" (fun v -> v)
+
|> 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);
+
+
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);
+
+
Printf.printf "\n=== Test 3: Jsont.string with YAML null (should error) ===\n";
+
let codec2 =
+
let open Jsont in
+
Object.map ~kind:"Test" (fun v -> v)
+
|> 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);
+
+
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)
+30
tests/bin/test_null_fix.ml
···
+
open Jsont
+
+
let () =
+
let module M = struct
+
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.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;
+
+
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
+302
tests/bin/test_objects.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test object codec functionality with Yamlt *)
+
+
(* 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
+
+
let show_result_both label json_result yaml_result =
+
Printf.printf "JSON: ";
+
show_result label json_result;
+
Printf.printf "YAML: ";
+
show_result label yaml_result
+
+
(* Test: Simple object with required fields *)
+
let test_simple_object 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 "{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
+
+
show_result_both "person"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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 }
+
+
let config_codec =
+
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)
+
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
+
+
show_result_both "config"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Object with default values *)
+
let test_default_values file =
+
let module M = struct
+
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.finish
+
+
let show s =
+
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
+
+
show_result_both "settings"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_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 }
+
+
let address_codec =
+
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.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
+
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
+
+
show_result_both "employee"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Unknown member handling - error *)
+
let test_unknown_members_error file =
+
let module M = struct
+
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
+
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 }
+
+
let flexible_codec =
+
Jsont.Object.map ~kind:"Flexible" (fun name extra -> { name; extra })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun f -> f.name)
+
|> 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
+
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
+
+
show_result_both "flexible"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Object cases (discriminated unions) - simplified version *)
+
let test_object_cases file =
+
let module M = struct
+
type circle = { type_: string; radius: float }
+
+
let circle_codec =
+
Jsont.Object.map ~kind:"Circle" (fun type_ radius -> { type_; radius })
+
|> Jsont.Object.mem "type" Jsont.string ~enc:(fun c -> c.type_)
+
|> Jsont.Object.mem "radius" Jsont.number ~enc:(fun c -> c.radius)
+
|> Jsont.Object.finish
+
+
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
+
+
show_result_both "shape"
+
(Result.map M.show json_result)
+
(Result.map M.show yaml_result)
+
+
(* Test: Missing required field error *)
+
let test_missing_required file =
+
let module M = struct
+
type required = { name: string; age: int }
+
+
let required_codec =
+
Jsont.Object.map ~kind:"Required" (fun name age -> { name; age })
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
+
|> 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
+
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 }
+
+
let person_codec =
+
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);
+
+
(* 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
+
| 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 () =
+
let usage = "Usage: test_objects <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
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 ()
+
+
| _ ->
+
prerr_endline usage;
+
prerr_endline "Commands:";
+
prerr_endline " simple <file> - Test simple object";
+
prerr_endline " optional <file> - Test optional fields";
+
prerr_endline " defaults <file> - Test default values";
+
prerr_endline " nested <file> - Test nested objects";
+
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 " encode - Test encoding objects";
+
exit 1
+16
tests/bin/test_opt_array.ml
···
+
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.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))
+
| Error e -> Printf.printf "Error: %s\n" e
+197
tests/bin/test_roundtrip.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test roundtrip encoding/decoding with Yamlt *)
+
+
(* Test: Roundtrip scalars *)
+
let test_scalar_roundtrip () =
+
let module M = struct
+
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 })
+
|> Jsont.Object.mem "s" Jsont.string ~enc:(fun d -> d.s)
+
|> Jsont.Object.mem "n" Jsont.number ~enc:(fun d -> d.n)
+
|> Jsont.Object.mem "b" Jsont.bool ~enc:(fun d -> d.b)
+
|> Jsont.Object.mem "nul" (Jsont.null ()) ~enc:(fun d -> d.nul)
+
|> Jsont.Object.finish
+
+
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
+
(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);
+
+
(* 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
+
(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);
+
+
(* 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)
+
+
(* Test: Roundtrip arrays *)
+
let test_array_roundtrip () =
+
let module M = struct
+
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.finish
+
+
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
+
+
(* JSON roundtrip *)
+
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);
+
+
(* 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)
+
+
(* 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 }
+
+
let person_codec =
+
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.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.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
+
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
+
+
(* JSON roundtrip *)
+
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);
+
+
(* 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)
+
+
(* Test: Roundtrip with optionals *)
+
let test_optional_roundtrip () =
+
let module M = struct
+
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.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.finish
+
+
let equal d1 d2 =
+
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
+
(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);
+
+
(* 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 () =
+
let usage = "Usage: test_roundtrip <command>" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
end;
+
+
match Sys.argv.(1) with
+
| "scalar" -> test_scalar_roundtrip ()
+
| "array" -> test_array_roundtrip ()
+
| "object" -> test_object_roundtrip ()
+
| "optional" -> test_optional_roundtrip ()
+
+
| _ ->
+
prerr_endline usage;
+
prerr_endline "Commands:";
+
prerr_endline " scalar - Test scalar roundtrip";
+
prerr_endline " array - Test array roundtrip";
+
prerr_endline " object - Test object roundtrip";
+
prerr_endline " optional - Test optional fields roundtrip";
+
exit 1
+304
tests/bin/test_scalars.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2024 The yamlrw programmers. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Test scalar type resolution with Yamlt codec *)
+
+
(* 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
+
+
let show_result_json label json_result yaml_result =
+
Printf.printf "JSON %s\n" label;
+
show_result " decode" json_result;
+
Printf.printf "YAML %s\n" label;
+
show_result " decode" yaml_result
+
+
(* Test: Decode null values with different type expectations *)
+
let test_null_resolution file =
+
let yaml = read_file file in
+
+
(* Define a simple object codec with nullable field *)
+
let null_codec =
+
Jsont.Object.map ~kind:"NullTest" (fun n -> n)
+
|> Jsont.Object.mem "value" (Jsont.null ()) ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
+
(* Try decoding as null *)
+
let result = Yamlt.decode_string null_codec yaml in
+
show_result "null_codec" (Result.map (fun () -> "null") result)
+
+
(* Test: Boolean type-directed resolution *)
+
let test_bool_resolution file =
+
let yaml = read_file file in
+
let json = read_file (file ^ ".json") in
+
+
(* Codec expecting bool *)
+
let bool_codec =
+
Jsont.Object.map ~kind:"BoolTest" (fun b -> b)
+
|> Jsont.Object.mem "value" Jsont.bool ~enc:(fun b -> b)
+
|> Jsont.Object.finish
+
in
+
+
(* Codec expecting string *)
+
let string_codec =
+
Jsont.Object.map ~kind:"StringTest" (fun s -> s)
+
|> Jsont.Object.mem "value" Jsont.string ~enc:(fun s -> s)
+
|> Jsont.Object.finish
+
in
+
+
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
+
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
+
show_result_json "string_codec"
+
(Result.map (Printf.sprintf "%S") json_result)
+
(Result.map (Printf.sprintf "%S") yaml_result)
+
+
(* Test: Number resolution *)
+
let test_number_resolution file =
+
let yaml = read_file file in
+
let json = read_file (file ^ ".json") in
+
+
let number_codec =
+
Jsont.Object.map ~kind:"NumberTest" (fun n -> n)
+
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
+
let json_result = Jsont_bytesrw.decode_string number_codec json in
+
let yaml_result = Yamlt.decode_string number_codec yaml in
+
+
show_result_json "number_codec"
+
(Result.map (Printf.sprintf "%.17g") json_result)
+
(Result.map (Printf.sprintf "%.17g") yaml_result)
+
+
(* Test: String resolution preserves everything *)
+
let test_string_resolution file =
+
let yaml = read_file file in
+
let json = read_file (file ^ ".json") in
+
+
let string_codec =
+
Jsont.Object.map ~kind:"StringTest" (fun s -> s)
+
|> Jsont.Object.mem "value" Jsont.string ~enc:(fun s -> s)
+
|> Jsont.Object.finish
+
in
+
+
let json_result = Jsont_bytesrw.decode_string string_codec json in
+
let yaml_result = Yamlt.decode_string string_codec yaml in
+
+
show_result_json "string_codec"
+
(Result.map (Printf.sprintf "%S") json_result)
+
(Result.map (Printf.sprintf "%S") yaml_result)
+
+
(* Test: Special float values *)
+
let test_special_floats file =
+
let yaml = read_file file in
+
+
let number_codec =
+
Jsont.Object.map ~kind:"SpecialFloat" (fun n -> n)
+
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
in
+
+
let result = Yamlt.decode_string number_codec 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
+
+
(* 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"
+
+
(* Test: Decode with Jsont.json to see auto-resolution *)
+
let test_any_resolution file =
+
let yaml = read_file file in
+
let json = read_file (file ^ ".json") in
+
+
let any_codec =
+
Jsont.Object.map ~kind:"AnyTest" (fun v -> v)
+
|> Jsont.Object.mem "value" Jsont.json ~enc:(fun v -> v)
+
|> Jsont.Object.finish
+
in
+
+
let json_result = Jsont_bytesrw.decode_string any_codec json in
+
let yaml_result = Yamlt.decode_string any_codec yaml in
+
+
(* Just show that it decoded successfully *)
+
show_result_json "any_codec"
+
(Result.map (fun _ -> "decoded") json_result)
+
(Result.map (fun _ -> "decoded") yaml_result)
+
+
(* Test: Encoding to different formats *)
+
let test_encode_formats value_type value =
+
match value_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 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
+
| 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 codec =
+
Jsont.Object.map ~kind:"NumberTest" (fun n -> n)
+
|> Jsont.Object.mem "value" Jsont.number ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
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
+
| 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 codec =
+
Jsont.Object.map ~kind:"StringTest" (fun s -> s)
+
|> Jsont.Object.mem "value" Jsont.string ~enc:(fun s -> s)
+
|> Jsont.Object.finish
+
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
+
| 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 codec =
+
Jsont.Object.map ~kind:"NullTest" (fun n -> n)
+
|> Jsont.Object.mem "value" (Jsont.null ()) ~enc:(fun n -> n)
+
|> Jsont.Object.finish
+
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
+
| 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)
+
| _ -> failwith "unknown type"
+
+
let () =
+
let usage = "Usage: test_scalars <command> [args...]" in
+
+
if Stdlib.Array.length Sys.argv < 2 then begin
+
prerr_endline usage;
+
exit 1
+
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)
+
+
| "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)
+
+
| "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 " 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 " encode <type> <value> - Test encoding to JSON/YAML";
+
exit 1
+32
tests/bin/test_some_vs_option.ml
···
+
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.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);
+
+
(* 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.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)
+143
tests/cram/arrays_codec.t
···
+
Array Codec Tests with Yamlt
+
===============================
+
+
This test suite validates array encoding/decoding with Jsont codecs in YAML,
+
including homogeneous type checking and nested structures.
+
+
Setup
+
-----
+
+
================================================================================
+
HOMOGENEOUS ARRAYS
+
================================================================================
+
+
Integer arrays
+
+
$ test_arrays int ../data/arrays/int_array.yml
+
JSON: int_array: [1; 2; 3; 4; 5]
+
YAML: int_array: [1; 2; 3; 4; 5]
+
+
String arrays
+
+
$ test_arrays string ../data/arrays/string_array.yml
+
JSON: string_array: ["apple"; "banana"; "cherry"]
+
YAML: string_array: ["apple"; "banana"; "cherry"]
+
+
Float/Number arrays
+
+
$ test_arrays float ../data/arrays/float_array.yml
+
JSON: float_array: [1.50; 2.70; 3.14; 0.50]
+
YAML: float_array: [1.50; 2.70; 3.14; 0.50]
+
+
Boolean arrays
+
+
$ test_arrays bool ../data/arrays/bool_array.yml
+
JSON: bool_array: [true; false; true; true; false]
+
YAML: bool_array: [true; false; true; true; false]
+
+
================================================================================
+
EMPTY ARRAYS
+
================================================================================
+
+
Empty arrays work correctly
+
+
$ test_arrays empty ../data/arrays/empty_array.yml
+
JSON: empty_array: length=0
+
YAML: empty_array: length=0
+
+
================================================================================
+
ARRAYS OF OBJECTS
+
================================================================================
+
+
Arrays containing objects
+
+
$ test_arrays objects ../data/arrays/object_array.yml
+
JSON: object_array: [{Alice,30}; {Bob,25}; {Charlie,35}]
+
YAML: object_array: [{Alice,30}; {Bob,25}; {Charlie,35}]
+
+
================================================================================
+
NESTED ARRAYS
+
================================================================================
+
+
Arrays containing arrays (matrices)
+
+
$ test_arrays nested ../data/arrays/nested_array.yml
+
JSON: nested_arrays: [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]]
+
YAML: nested_arrays: [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]]
+
+
================================================================================
+
NULLABLE ARRAYS
+
================================================================================
+
+
Arrays with null elements
+
+
$ test_arrays nullable ../data/arrays/nullable_array.yml
+
JSON: nullable_array: ERROR: Expected string but found null
+
File "-", line 1, characters 21-22:
+
File "-", line 1, characters 21-22: at index 1 of
+
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"]
+
+
================================================================================
+
ERROR HANDLING
+
================================================================================
+
+
Type mismatch in array element
+
+
$ test_arrays type-mismatch ../data/arrays/type_mismatch.yml
+
Expected 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
+
+
================================================================================
+
ENCODING ARRAYS
+
================================================================================
+
+
Encode arrays to JSON and YAML formats
+
+
$ test_arrays encode
+
JSON: {"numbers":[1,2,3,4,5],"strings":["hello","world"]}
+
YAML Block:
+
numbers:
+
- 1.0
+
- 2.0
+
- 3.0
+
- 4.0
+
- 5.0
+
strings:
+
- hello
+
- world
+
YAML Flow: {numbers: [1.0, 2.0, 3.0, 4.0, 5.0]strings, [hello, world]}
+
+
================================================================================
+
NEGATIVE TESTS - Wrong File Types
+
================================================================================
+
+
Attempting to decode an object file with an array codec should fail
+
+
$ test_arrays int ../data/objects/simple.yml
+
JSON: int_array: ERROR: Missing member values in Numbers object
+
File "-", line 1, characters 0-28:
+
YAML: int_array: ERROR: Missing member values in Numbers object
+
File "-":
+
+
Attempting to decode a scalar file with an array codec should fail
+
+
$ test_arrays string ../data/scalars/string_plain.yml
+
JSON: string_array: ERROR: Missing member items in Tags object
+
File "-", line 1, characters 0-24:
+
YAML: string_array: ERROR: Missing member items in Tags object
+
File "-":
+
+
Attempting to decode int array with string array codec should fail
+
+
$ test_arrays string ../data/arrays/int_array.yml
+
JSON: string_array: ERROR: Missing member items in Tags object
+
File "-", line 1, characters 0-27:
+
YAML: string_array: ERROR: Missing member items in Tags object
+
File "-":
+74
tests/cram/complex_codec.t
···
+
Complex Nested Types Tests with Yamlt
+
======================================
+
+
This test suite validates complex nested structures combining objects, arrays,
+
and various levels of nesting.
+
+
================================================================================
+
DEEPLY NESTED OBJECTS
+
================================================================================
+
+
Handle deeply nested object structures
+
+
$ test_complex deep-nesting ../data/complex/deep_nesting.yml
+
JSON: deep_nesting: depth=4, value=42
+
YAML: deep_nesting: depth=4, value=42
+
+
================================================================================
+
MIXED STRUCTURES
+
================================================================================
+
+
Arrays of objects containing arrays
+
+
$ test_complex mixed-structure ../data/complex/mixed_structure.yml
+
JSON: mixed_structure: name="products", items=3, total_tags=6
+
YAML: mixed_structure: name="products", items=3, total_tags=6
+
+
================================================================================
+
COMPLEX OPTIONAL COMBINATIONS
+
================================================================================
+
+
Multiple optional fields with different combinations
+
+
$ 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
+
+
================================================================================
+
HETEROGENEOUS DATA
+
================================================================================
+
+
Mixed types in arrays using any type
+
+
$ test_complex heterogeneous ../data/complex/heterogeneous.yml
+
JSON: heterogeneous: ERROR: Expected one of but found number
+
File "-", line 1, characters 11-12:
+
File "-", line 1, characters 11-12: at index 0 of
+
File "-", line 1, characters 10-12: array<one of >
+
File "-": in member mixed of
+
File "-", line 1, characters 0-12: Data object
+
YAML: heterogeneous: ERROR: Expected one of but found number
+
File "-":
+
at index 0 of
+
File "-": array<one of >
+
File "-": in member mixed of
+
File "-": Data object
+
+
================================================================================
+
NEGATIVE TESTS - Structure Mismatch
+
================================================================================
+
+
Using deeply nested data with flat codec should fail
+
+
$ test_complex mixed-structure ../data/complex/deep_nesting.yml
+
JSON: mixed_structure: ERROR: Missing members in Collection object:
+
items
+
name
+
File "-", line 1, characters 0-44:
+
YAML: mixed_structure: ERROR: Missing members in Collection object:
+
items
+
name
+
File "-":
+15
tests/cram/dune
···
+
(cram
+
(deps
+
(package yamlt)
+
(glob_files ../data/scalars/*.yml)
+
(glob_files ../data/scalars/*.json)
+
(glob_files ../data/objects/*.yml)
+
(glob_files ../data/objects/*.json)
+
(glob_files ../data/arrays/*.yml)
+
(glob_files ../data/arrays/*.json)
+
(glob_files ../data/formats/*.yml)
+
(glob_files ../data/formats/*.json)
+
(glob_files ../data/complex/*.yml)
+
(glob_files ../data/complex/*.json)
+
(glob_files ../data/edge/*.yml)
+
(glob_files ../data/edge/*.json)))
+85
tests/cram/edge_codec.t
···
+
Edge Cases Tests with Yamlt
+
============================
+
+
This test suite validates edge cases including large numbers, special characters,
+
unicode, and boundary conditions.
+
+
================================================================================
+
LARGE NUMBERS
+
================================================================================
+
+
Very large and very small floating point numbers
+
+
$ test_edge large-numbers ../data/edge/large_numbers.yml
+
JSON: large_numbers: large_int=9007199254740991, large_float=1.797693e+308, small_float=2.225074e-308
+
YAML: large_numbers: large_int=9007199254740991, large_float=1.797693e+308, small_float=2.225074e-308
+
+
================================================================================
+
SPECIAL CHARACTERS
+
================================================================================
+
+
Strings containing newlines, tabs, and other special characters
+
+
$ test_edge special-chars ../data/edge/special_chars.yml
+
JSON: special_chars: length=34, contains_newline=true, contains_tab=true
+
YAML: special_chars: length=34, contains_newline=true, contains_tab=true
+
+
================================================================================
+
UNICODE STRINGS
+
================================================================================
+
+
Emoji, Chinese, and RTL text
+
+
$ test_edge unicode ../data/edge/unicode.yml
+
JSON: unicode: emoji="\240\159\142\137\240\159\154\128\226\156\168", chinese="\228\189\160\229\165\189\228\184\150\231\149\140", rtl="\217\133\216\177\216\173\216\168\216\167"
+
YAML: unicode: emoji="\240\159\142\137\240\159\154\128\226\156\168", chinese="\228\189\160\229\165\189\228\184\150\231\149\140", rtl="\217\133\216\177\216\173\216\168\216\167"
+
+
================================================================================
+
EMPTY COLLECTIONS
+
================================================================================
+
+
Empty arrays and objects
+
+
$ test_edge empty-collections ../data/edge/empty_collections.yml
+
JSON: empty_collections: empty_array_len=0, empty_object_array_len=0
+
YAML: empty_collections: empty_array_len=0, empty_object_array_len=0
+
+
================================================================================
+
SPECIAL KEY NAMES
+
================================================================================
+
+
Keys with dots, dashes, colons
+
+
$ test_edge special-keys ../data/edge/special_keys.yml
+
JSON: special_keys: ERROR: Expected one of but found object
+
File "-", line 1, characters 0-1:
+
YAML: special_keys: ERROR: Expected one of but found object
+
File "-":
+
+
================================================================================
+
SINGLE-ELEMENT ARRAYS
+
================================================================================
+
+
Arrays with exactly one element
+
+
$ test_edge single-element ../data/edge/single_element.yml
+
JSON: single_element: length=1, value=42
+
YAML: single_element: length=1, value=42
+
+
================================================================================
+
NEGATIVE TESTS - Boundary Violations
+
================================================================================
+
+
Using unicode data with number codec should fail
+
+
$ test_edge large-numbers ../data/edge/unicode.yml
+
JSON: large_numbers: ERROR: Missing members in Numbers object:
+
large_float
+
large_int
+
small_float
+
File "-", line 1, characters 0-72:
+
YAML: large_numbers: ERROR: Missing members in Numbers object:
+
large_float
+
large_int
+
small_float
+
File "-":
+107
tests/cram/formats_codec.t
···
+
Format-Specific Features Tests with Yamlt
+
==========================================
+
+
This test suite validates YAML-specific format features and compares with JSON behavior.
+
+
================================================================================
+
MULTI-LINE STRINGS - LITERAL STYLE
+
================================================================================
+
+
Literal style (|) preserves newlines
+
+
$ test_formats literal ../data/formats/literal_string.yml
+
JSON: literal_string: lines=5, length=81
+
YAML: literal_string: lines=5, length=81
+
+
================================================================================
+
MULTI-LINE STRINGS - FOLDED STYLE
+
================================================================================
+
+
Folded style (>) folds lines into single line
+
+
$ test_formats folded ../data/formats/folded_string.yml
+
JSON: folded_string: length=114, newlines=1
+
YAML: folded_string: length=114, newlines=1
+
+
================================================================================
+
NUMBER FORMATS
+
================================================================================
+
+
YAML supports hex, octal, and binary number formats
+
+
$ 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
+
+
================================================================================
+
COMMENTS
+
================================================================================
+
+
YAML comments are ignored during parsing
+
+
$ test_formats comments ../data/formats/comments.yml
+
YAML (with comments): host="localhost", port=8080, debug=true
+
+
================================================================================
+
EMPTY DOCUMENTS
+
================================================================================
+
+
Empty or null documents handled correctly
+
+
$ test_formats empty-doc ../data/formats/empty_doc.yml
+
JSON: empty_document: ERROR: Expected string but found null
+
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")
+
+
================================================================================
+
EXPLICIT TYPE TAGS
+
================================================================================
+
+
Explicit YAML type tags (!!str, !!int, etc.)
+
+
$ test_formats explicit-tags ../data/formats/explicit_tags.yml
+
YAML (with tags): data="123"
+
+
================================================================================
+
ENCODING STYLES
+
================================================================================
+
+
Compare Block vs Flow encoding styles
+
+
$ test_formats encode-styles
+
YAML Block:
+
name: test
+
values:
+
- 1.0
+
- 2.0
+
- 3.0
+
nested:
+
enabled: true
+
count: 5.0
+
+
YAML Flow:
+
{name: test, values: [1.0, 2.0, 3.0]nested, {enabled: true, count: 5.0}}
+
+
+
================================================================================
+
NEGATIVE TESTS - Format Compatibility
+
================================================================================
+
+
Using literal string test with number codec should fail
+
+
$ test_formats number-formats ../data/formats/literal_string.yml
+
JSON: number_formats: ERROR: Missing members in Numbers object:
+
binary
+
hex
+
octal
+
File "-", line 1, characters 0-100:
+
YAML: number_formats: ERROR: Missing members in Numbers object:
+
binary
+
hex
+
octal
+
File "-":
+160
tests/cram/objects_codec.t
···
+
Object Codec Tests with Yamlt
+
================================
+
+
This test suite validates object encoding/decoding with Jsont codecs in YAML,
+
and compares behavior with JSON.
+
+
Setup
+
-----
+
+
+
================================================================================
+
SIMPLE OBJECTS
+
================================================================================
+
+
Decode simple object with required fields
+
+
$ test_objects simple ../data/objects/simple.yml
+
JSON: person: {name="Alice"; age=30}
+
YAML: person: {name="Alice"; age=30}
+
+
================================================================================
+
OPTIONAL FIELDS
+
================================================================================
+
+
Object with all optional fields present
+
+
$ test_objects optional ../data/objects/optional_all.yml
+
JSON: config: {host="localhost"; port=Some 8080; debug=Some true}
+
YAML: config: {host="localhost"; port=Some 8080; debug=Some true}
+
+
Object with some optional fields missing
+
+
$ test_objects optional ../data/objects/optional_partial.yml
+
JSON: config: {host="example.com"; port=Some 3000; debug=None}
+
YAML: config: {host="example.com"; port=Some 3000; debug=None}
+
+
Object with only required field
+
+
$ test_objects optional ../data/objects/optional_minimal.yml
+
JSON: config: {host="minimal.com"; port=None; debug=None}
+
YAML: config: {host="minimal.com"; port=None; debug=None}
+
+
================================================================================
+
DEFAULT VALUES
+
================================================================================
+
+
Empty object uses all defaults
+
+
$ test_objects defaults ../data/objects/defaults_empty.yml
+
JSON: settings: {timeout=30; retries=3; verbose=false}
+
YAML: settings: {timeout=30; retries=3; verbose=false}
+
+
Object with partial fields uses defaults for missing ones
+
+
$ test_objects defaults ../data/objects/defaults_partial.yml
+
JSON: settings: {timeout=60; retries=3; verbose=false}
+
YAML: settings: {timeout=60; retries=3; verbose=false}
+
+
================================================================================
+
NESTED OBJECTS
+
================================================================================
+
+
Objects containing other objects
+
+
$ test_objects nested ../data/objects/nested.yml
+
JSON: employee: {name="Bob"; address={street="123 Main St"; city="Springfield"; zip="12345"}}
+
YAML: employee: {name="Bob"; address={street="123 Main St"; city="Springfield"; zip="12345"}}
+
+
================================================================================
+
UNKNOWN MEMBER HANDLING
+
================================================================================
+
+
Unknown members cause error by default
+
+
$ test_objects unknown-error ../data/objects/unknown_members.yml
+
Unexpected success
+
+
Unknown members can be kept
+
+
$ test_objects unknown-keep ../data/objects/unknown_keep.yml
+
JSON: flexible: {name="Charlie"; has_extra=true}
+
YAML: flexible: {name="Charlie"; has_extra=true}
+
+
================================================================================
+
OBJECT CASES (DISCRIMINATED UNIONS)
+
================================================================================
+
+
Decode circle variant
+
+
$ test_objects cases ../data/objects/case_circle.yml
+
JSON: shape: Circle{radius=5.50}
+
YAML: shape: Circle{radius=5.50}
+
+
Decode rectangle variant
+
+
$ test_objects cases ../data/objects/case_rectangle.yml
+
JSON: shape: ERROR: Missing member radius in Circle object
+
File "-", line 1, characters 0-52:
+
YAML: shape: ERROR: Missing member radius in Circle object
+
File "-":
+
+
================================================================================
+
ERROR HANDLING
+
================================================================================
+
+
Missing required field produces error
+
+
$ test_objects missing-required ../data/objects/missing_required.yml
+
Expected error: Missing member age in Required object
+
File "-":
+
+
================================================================================
+
ENCODING OBJECTS
+
================================================================================
+
+
Encode objects to JSON and YAML formats
+
+
$ test_objects encode
+
JSON: {"name":"Alice","age":30,"active":true}
+
YAML Block:
+
name: Alice
+
age: 30.0
+
active: true
+
YAML Flow: {name: Alice, age: 30.0, active: true}
+
+
================================================================================
+
NEGATIVE TESTS - Wrong File Types
+
================================================================================
+
+
Attempting to decode an array file with an object codec should fail
+
+
$ test_objects simple ../data/arrays/int_array.yml
+
JSON: person: ERROR: Missing members in Person object:
+
age
+
name
+
File "-", line 1, characters 0-27:
+
YAML: person: ERROR: Missing members in Person object:
+
age
+
name
+
File "-":
+
+
Attempting to decode a scalar file with an object codec should fail
+
+
$ test_objects simple ../data/scalars/string_plain.yml
+
JSON: person: ERROR: Missing members in Person object:
+
age
+
name
+
File "-", line 1, characters 0-24:
+
YAML: person: ERROR: Missing members in Person object:
+
age
+
name
+
File "-":
+
+
Attempting to decode wrong object type (nested when expecting simple) should fail
+
+
$ test_objects simple ../data/objects/nested.yml
+
JSON: person: ERROR: Missing member age in Person object
+
File "-", line 1, characters 0-92:
+
YAML: person: ERROR: Missing member age in Person object
+
File "-":
+46
tests/cram/roundtrip_codec.t
···
+
Roundtrip Encoding/Decoding Tests with Yamlt
+
=============================================
+
+
This test suite validates that data can be encoded and then decoded back
+
to the original value, ensuring no data loss in the roundtrip process.
+
+
================================================================================
+
SCALAR ROUNDTRIP
+
================================================================================
+
+
Encode and decode scalar types
+
+
$ test_roundtrip scalar
+
JSON roundtrip: PASS
+
YAML Block roundtrip: PASS
+
YAML Flow roundtrip: PASS
+
+
================================================================================
+
ARRAY ROUNDTRIP
+
================================================================================
+
+
Encode and decode arrays including nested arrays
+
+
$ test_roundtrip array
+
JSON array roundtrip: PASS
+
YAML array roundtrip: PASS
+
+
================================================================================
+
OBJECT ROUNDTRIP
+
================================================================================
+
+
Encode and decode complex objects with nested structures
+
+
$ test_roundtrip object
+
JSON object roundtrip: PASS
+
YAML object roundtrip: PASS
+
+
================================================================================
+
OPTIONAL FIELDS ROUNDTRIP
+
================================================================================
+
+
Encode and decode optional and nullable fields
+
+
$ test_roundtrip optional
+
Fatal error: exception Invalid_argument("option is None")
+
[2]
+342
tests/cram/scalars_codec.t
···
+
Scalar Type Resolution Tests with Yamlt Codec
+
==================================================
+
+
This test suite validates how YAML scalars are resolved based on the expected
+
Jsont type codec, and compares behavior with JSON decoding.
+
+
================================================================================
+
NULL RESOLUTION
+
================================================================================
+
+
Explicit null value
+
+
$ test_scalars null ../data/scalars/null_explicit.yml
+
null_codec: null
+
+
Tilde as null
+
+
$ test_scalars null ../data/scalars/null_tilde.yml
+
null_codec: null
+
+
Empty value as null
+
+
$ test_scalars null ../data/scalars/null_empty.yml
+
null_codec: null
+
+
================================================================================
+
BOOLEAN TYPE-DIRECTED RESOLUTION
+
================================================================================
+
+
Plain "true" resolves to bool(true) with bool codec, but string "true" with string codec
+
+
$ test_scalars bool ../data/scalars/bool_true_plain.yml
+
=== Bool Codec ===
+
JSON bool_codec
+
decode: true
+
YAML bool_codec
+
decode: true
+
+
=== String Codec ===
+
JSON string_codec
+
decode: ERROR: Expected string but found bool
+
File "-", line 1, characters 10-11:
+
File "-": in member value of
+
File "-", line 1, characters 0-11: StringTest object
+
YAML string_codec
+
decode: "true"
+
+
Quoted "true" always resolves to string, even with bool codec
+
+
$ test_scalars bool ../data/scalars/bool_true_quoted.yml
+
=== Bool Codec ===
+
JSON bool_codec
+
decode: ERROR: Expected bool but found string
+
File "-", line 1, characters 10-11:
+
File "-": in member value of
+
File "-", line 1, characters 0-11: BoolTest object
+
YAML bool_codec
+
decode: true
+
+
=== String Codec ===
+
JSON string_codec
+
decode: "true"
+
YAML string_codec
+
decode: "true"
+
+
YAML-specific bool: "yes" resolves to bool(true)
+
+
$ test_scalars bool ../data/scalars/bool_yes.yml
+
=== Bool Codec ===
+
JSON bool_codec
+
decode: true
+
YAML bool_codec
+
decode: true
+
+
=== String Codec ===
+
JSON string_codec
+
decode: ERROR: Expected string but found bool
+
File "-", line 1, characters 10-11:
+
File "-": in member value of
+
File "-", line 1, characters 0-11: StringTest object
+
YAML string_codec
+
decode: "yes"
+
+
Plain "false" and "no" work similarly
+
+
$ test_scalars bool ../data/scalars/bool_false.yml
+
=== Bool Codec ===
+
JSON bool_codec
+
decode: false
+
YAML bool_codec
+
decode: false
+
+
=== String Codec ===
+
JSON string_codec
+
decode: ERROR: Expected string but found bool
+
File "-", line 1, characters 10-11:
+
File "-": in member value of
+
File "-", line 1, characters 0-11: StringTest object
+
YAML string_codec
+
decode: "false"
+
+
$ test_scalars bool ../data/scalars/bool_no.yml
+
=== Bool Codec ===
+
JSON bool_codec
+
decode: false
+
YAML bool_codec
+
decode: false
+
+
=== String Codec ===
+
JSON string_codec
+
decode: ERROR: Expected string but found bool
+
File "-", line 1, characters 10-11:
+
File "-": in member value of
+
File "-", line 1, characters 0-11: StringTest object
+
YAML string_codec
+
decode: "no"
+
+
================================================================================
+
NUMBER RESOLUTION
+
================================================================================
+
+
Integer values
+
+
$ test_scalars number ../data/scalars/number_int.yml
+
JSON number_codec
+
decode: 42
+
YAML number_codec
+
decode: 42
+
+
Float values
+
+
$ test_scalars number ../data/scalars/number_float.yml
+
JSON number_codec
+
decode: 3.1415899999999999
+
YAML number_codec
+
decode: 3.1415899999999999
+
+
Hexadecimal notation (YAML-specific)
+
+
$ test_scalars number ../data/scalars/number_hex.yml
+
JSON number_codec
+
decode: 42
+
YAML number_codec
+
decode: 42
+
+
Octal notation (YAML-specific)
+
+
$ test_scalars number ../data/scalars/number_octal.yml
+
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
+
+
Negative numbers
+
+
$ test_scalars number ../data/scalars/number_negative.yml
+
JSON number_codec
+
decode: -273.14999999999998
+
YAML number_codec
+
decode: -273.14999999999998
+
+
================================================================================
+
SPECIAL FLOAT VALUES (YAML-specific)
+
================================================================================
+
+
Positive infinity
+
+
$ test_scalars special-float ../data/scalars/special_inf.yml
+
value: +Infinity
+
+
Negative infinity
+
+
$ test_scalars special-float ../data/scalars/special_neg_inf.yml
+
value: -Infinity
+
+
Not-a-Number (NaN)
+
+
$ test_scalars special-float ../data/scalars/special_nan.yml
+
value: NaN
+
+
================================================================================
+
STRING RESOLUTION
+
================================================================================
+
+
Plain strings
+
+
$ test_scalars string ../data/scalars/string_plain.yml
+
JSON string_codec
+
decode: "hello world"
+
YAML string_codec
+
decode: "hello world"
+
+
Quoted numeric strings stay as strings
+
+
$ test_scalars string ../data/scalars/string_quoted.yml
+
JSON string_codec
+
decode: "42"
+
YAML string_codec
+
decode: "42"
+
+
Empty strings
+
+
$ test_scalars string ../data/scalars/string_empty.yml
+
JSON string_codec
+
decode: ""
+
YAML string_codec
+
decode: ""
+
+
================================================================================
+
TYPE MISMATCH ERRORS
+
================================================================================
+
+
String when bool expected
+
+
$ test_scalars type-mismatch ../data/scalars/mismatch_string_as_bool.yml bool
+
Expected error: Expected bool but found scalar hello
+
File "-":
+
File "-": in member value of
+
File "-": BoolTest object
+
+
String when number expected
+
+
$ test_scalars type-mismatch ../data/scalars/mismatch_string_as_number.yml number
+
Expected error: Expected number but found scalar not-a-number
+
File "-":
+
File "-": in member value of
+
File "-": NumberTest object
+
+
Number when null expected
+
+
$ test_scalars type-mismatch ../data/scalars/mismatch_number_as_null.yml null
+
Expected error: Expected null but found scalar 42
+
File "-":
+
File "-": in member value of
+
File "-": NullTest object
+
+
================================================================================
+
JSONT.ANY AUTO-RESOLUTION
+
================================================================================
+
+
With Jsont.any, scalars are auto-resolved based on their content
+
+
Null auto-resolves to null
+
+
$ test_scalars any ../data/scalars/any_null.yml
+
JSON any_codec
+
decode: decoded
+
YAML any_codec
+
decode: decoded
+
+
Plain bool auto-resolves to bool
+
+
$ test_scalars any ../data/scalars/any_bool.yml
+
JSON any_codec
+
decode: decoded
+
YAML any_codec
+
decode: decoded
+
+
Number auto-resolves to number
+
+
$ test_scalars any ../data/scalars/any_number.yml
+
JSON any_codec
+
decode: decoded
+
YAML any_codec
+
decode: decoded
+
+
Plain string auto-resolves to string
+
+
$ test_scalars any ../data/scalars/any_string.yml
+
JSON any_codec
+
decode: decoded
+
YAML any_codec
+
decode: decoded
+
+
================================================================================
+
ENCODING SCALARS
+
================================================================================
+
+
Encoding bool values
+
+
$ test_scalars encode bool true
+
JSON: {"value":true}
+
YAML Block:
+
value: true
+
YAML Flow: {value: true}
+
+
$ test_scalars encode bool false
+
JSON: {"value":false}
+
YAML Block:
+
value: false
+
YAML Flow: {value: false}
+
+
Encoding numbers
+
+
$ test_scalars encode number 42.5
+
JSON: {"value":42.5}
+
YAML Block:
+
value: 42.5
+
YAML Flow: {value: 42.5}
+
+
Encoding strings
+
+
$ test_scalars encode string "hello world"
+
JSON: {"value":"hello world"}
+
YAML Block:
+
value: hello world
+
YAML Flow: {value: hello world}
+
+
Encoding null
+
+
$ test_scalars encode null ""
+
JSON: {"value":null}
+
YAML Block:
+
value: null
+
YAML Flow: {value: null}
+
+
================================================================================
+
NEGATIVE TESTS - Wrong File Types
+
================================================================================
+
+
Attempting to decode an object file with a scalar codec should fail
+
+
$ test_scalars string ../data/objects/simple.yml
+
JSON string_codec
+
decode: ERROR: Missing member value in StringTest object
+
File "-", line 1, characters 0-28:
+
YAML string_codec
+
decode: ERROR: Missing member value in StringTest object
+
File "-":
+
+
Attempting to decode an array file with a scalar codec should fail
+
+
$ test_scalars number ../data/arrays/int_array.yml
+
JSON number_codec
+
decode: ERROR: Missing member value in NumberTest object
+
File "-", line 1, characters 0-27:
+
YAML number_codec
+
decode: ERROR: Missing member value in NumberTest object
+
File "-":
+6
tests/data/arrays/bool_array.yml
···
+
values:
+
- true
+
- false
+
- true
+
- true
+
- false
+1
tests/data/arrays/bool_array.yml.json
···
+
{"values": [true, false, true, true, false]}
+1
tests/data/arrays/empty_array.yml
···
+
items: []
+1
tests/data/arrays/empty_array.yml.json
···
+
{"items": []}
+5
tests/data/arrays/float_array.yml
···
+
values:
+
- 1.5
+
- 2.7
+
- 3.14
+
- 0.5
+1
tests/data/arrays/float_array.yml.json
···
+
{"values": [1.5, 2.7, 3.14, 0.5]}
+6
tests/data/arrays/int_array.yml
···
+
values:
+
- 1
+
- 2
+
- 3
+
- 4
+
- 5
+1
tests/data/arrays/int_array.yml.json
···
+
{"values": [1, 2, 3, 4, 5]}
+4
tests/data/arrays/nested_array.yml
···
+
data:
+
- [1, 2, 3]
+
- [4, 5, 6]
+
- [7, 8, 9]
+1
tests/data/arrays/nested_array.yml.json
···
+
{"data": [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}
+6
tests/data/arrays/nullable_array.yml
···
+
values:
+
- hello
+
- null
+
- world
+
- null
+
- test
+1
tests/data/arrays/nullable_array.yml.json
···
+
{"values": ["hello", null, "world", null, "test"]}
+7
tests/data/arrays/object_array.yml
···
+
persons:
+
- name: Alice
+
age: 30
+
- name: Bob
+
age: 25
+
- name: Charlie
+
age: 35
+1
tests/data/arrays/object_array.yml.json
···
+
{"persons": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, {"name": "Charlie", "age": 35}]}
+4
tests/data/arrays/string_array.yml
···
+
items:
+
- apple
+
- banana
+
- cherry
+1
tests/data/arrays/string_array.yml.json
···
+
{"items": ["apple", "banana", "cherry"]}
+5
tests/data/arrays/type_mismatch.yml
···
+
values:
+
- 1
+
- 2
+
- not-a-number
+
- 4
+4
tests/data/complex/complex_optional.yml
···
+
host: example.com
+
port: 443
+
ssl: true
+
fallback_hosts: [backup1.example.com, backup2.example.com]
+1
tests/data/complex/complex_optional.yml.json
···
+
{"host": "example.com", "port": 443, "ssl": true, "fallback_hosts": ["backup1.example.com", "backup2.example.com"]}
+4
tests/data/complex/deep_nesting.yml
···
+
top:
+
nested:
+
data:
+
value: 42
+1
tests/data/complex/deep_nesting.yml.json
···
+
{"top": {"nested": {"data": {"value": 42}}}}
+1
tests/data/complex/heterogeneous.yml
···
+
mixed: [42, "hello", true, null, 3.14]
+1
tests/data/complex/heterogeneous.yml.json
···
+
{"mixed": [42, "hello", true, null, 3.14]}
+8
tests/data/complex/mixed_structure.yml
···
+
name: products
+
items:
+
- id: 1
+
tags: [new, sale, featured]
+
- id: 2
+
tags: [clearance]
+
- id: 3
+
tags: [premium, exclusive]
+1
tests/data/complex/mixed_structure.yml.json
···
+
{"name": "products", "items": [{"id": 1, "tags": ["new", "sale", "featured"]}, {"id": 2, "tags": ["clearance"]}, {"id": 3, "tags": ["premium", "exclusive"]}]}
+2
tests/data/edge/empty_collections.yml
···
+
empty_array: []
+
empty_object_array: []
+1
tests/data/edge/empty_collections.yml.json
···
+
{"empty_array": [], "empty_object_array": []}
+3
tests/data/edge/large_numbers.yml
···
+
large_int: 9007199254740991
+
large_float: 1.7976931348623157e+308
+
small_float: 2.2250738585072014e-308
+1
tests/data/edge/large_numbers.yml.json
···
+
{"large_int": 9007199254740991, "large_float": 1.7976931348623157e+308, "small_float": 2.2250738585072014e-308}
+1
tests/data/edge/single_element.yml
···
+
single: [42]
+1
tests/data/edge/single_element.yml.json
···
+
{"single": [42]}
+1
tests/data/edge/special_chars.yml
···
+
content: "Line 1\nLine 2\tTabbed\r\nWindows line"
+1
tests/data/edge/special_chars.yml.json
···
+
{"content": "Line 1\nLine 2\tTabbed\r\nWindows line"}
+3
tests/data/edge/special_keys.yml
···
+
"key.with.dots": value1
+
"key-with-dashes": value2
+
"key:with:colons": value3
+1
tests/data/edge/special_keys.yml.json
···
+
{"key.with.dots": "value1", "key-with-dashes": "value2", "key:with:colons": "value3"}
+3
tests/data/edge/unicode.yml
···
+
emoji: "🎉🚀✨"
+
chinese: "你好世界"
+
rtl: "مرحبا"
+1
tests/data/edge/unicode.yml.json
···
+
{"emoji": "🎉🚀✨", "chinese": "你好世界", "rtl": "مرحبا"}
+5
tests/data/formats/comments.yml
···
+
# Configuration file with comments
+
host: localhost # The server host
+
port: 8080 # The server port
+
# Enable debug mode for development
+
debug: true
+1
tests/data/formats/empty_doc.yml
···
+
value: null
+1
tests/data/formats/empty_doc.yml.json
···
+
{"value": null}
+1
tests/data/formats/explicit_tags.yml
···
+
data: !!str 123
+5
tests/data/formats/folded_string.yml
···
+
content: >
+
This is a folded string that
+
spans multiple lines but will
+
be folded into a single line
+
with spaces between words
+1
tests/data/formats/folded_string.yml.json
···
+
{"content": "This is a folded string that spans multiple lines but will be folded into a single line with spaces between words\n"}
+5
tests/data/formats/literal_string.yml
···
+
content: |
+
This is a literal string
+
with multiple lines
+
preserving newlines
+
and indentation
+1
tests/data/formats/literal_string.yml.json
···
+
{"content": "This is a literal string\nwith multiple lines\npreserving newlines\nand indentation\n"}
+3
tests/data/formats/number_formats.yml
···
+
hex: 0xFF
+
octal: 0o77
+
binary: 0b1010
+1
tests/data/formats/number_formats.yml.json
···
+
{"hex": 255, "octal": 63, "binary": 10}
+2
tests/data/objects/case_circle.yml
···
+
type: circle
+
radius: 5.5
+1
tests/data/objects/case_circle.yml.json
···
+
{"type": "circle", "radius": 5.5}
+3
tests/data/objects/case_rectangle.yml
···
+
type: rectangle
+
width: 10.0
+
height: 20.0
+1
tests/data/objects/case_rectangle.yml.json
···
+
{"type": "rectangle", "width": 10.0, "height": 20.0}
+1
tests/data/objects/defaults_empty.yml
···
+
{}
+1
tests/data/objects/defaults_empty.yml.json
···
+
{}
+1
tests/data/objects/defaults_partial.yml
···
+
timeout: 60
+1
tests/data/objects/defaults_partial.yml.json
···
+
{"timeout": 60}
+1
tests/data/objects/missing_required.yml
···
+
name: Incomplete
+5
tests/data/objects/nested.yml
···
+
name: Bob
+
address:
+
street: 123 Main St
+
city: Springfield
+
zip: "12345"
+1
tests/data/objects/nested.yml.json
···
+
{"name": "Bob", "address": {"street": "123 Main St", "city": "Springfield", "zip": "12345"}}
+3
tests/data/objects/optional_all.yml
···
+
host: localhost
+
port: 8080
+
debug: true
+1
tests/data/objects/optional_all.yml.json
···
+
{"host": "localhost", "port": 8080, "debug": true}
+1
tests/data/objects/optional_minimal.yml
···
+
host: minimal.com
+1
tests/data/objects/optional_minimal.yml.json
···
+
{"host": "minimal.com"}
+2
tests/data/objects/optional_partial.yml
···
+
host: example.com
+
port: 3000
+1
tests/data/objects/optional_partial.yml.json
···
+
{"host": "example.com", "port": 3000}
+2
tests/data/objects/simple.yml
···
+
name: Alice
+
age: 30
+1
tests/data/objects/simple.yml.json
···
+
{"name": "Alice", "age": 30}
+3
tests/data/objects/unknown_keep.yml
···
+
name: Charlie
+
extra1: value1
+
extra2: value2
+1
tests/data/objects/unknown_keep.yml.json
···
+
{"name": "Charlie", "extra1": "value1", "extra2": "value2"}
+3
tests/data/objects/unknown_members.yml
···
+
name: Alice
+
age: 30
+
extra: not_expected
+1
tests/data/scalars/any_bool.yml
···
+
value: true
+1
tests/data/scalars/any_bool.yml.json
···
+
{"value": true}
+1
tests/data/scalars/any_null.yml
···
+
value: null
+1
tests/data/scalars/any_null.yml.json
···
+
{"value": null}
+1
tests/data/scalars/any_number.yml
···
+
value: 123.45
+1
tests/data/scalars/any_number.yml.json
···
+
{"value": 123.45}
+1
tests/data/scalars/any_string.yml
···
+
value: hello
+1
tests/data/scalars/any_string.yml.json
···
+
{"value": "hello"}
+1
tests/data/scalars/bool_false.yml
···
+
value: false
+1
tests/data/scalars/bool_false.yml.json
···
+
{"value": false}
+1
tests/data/scalars/bool_no.yml
···
+
value: no
+1
tests/data/scalars/bool_no.yml.json
···
+
{"value": false}
+1
tests/data/scalars/bool_true_plain.yml
···
+
value: true
+1
tests/data/scalars/bool_true_plain.yml.json
···
+
{"value": true}
+1
tests/data/scalars/bool_true_quoted.yml
···
+
value: "true"
+1
tests/data/scalars/bool_true_quoted.yml.json
···
+
{"value": "true"}
+1
tests/data/scalars/bool_yes.yml
···
+
value: yes
+1
tests/data/scalars/bool_yes.yml.json
···
+
{"value": true}
+1
tests/data/scalars/mismatch_number_as_null.yml
···
+
value: 42
+1
tests/data/scalars/mismatch_string_as_bool.yml
···
+
value: hello
+1
tests/data/scalars/mismatch_string_as_number.yml
···
+
value: not-a-number
+1
tests/data/scalars/null_empty.yml
···
+
value:
+1
tests/data/scalars/null_explicit.yml
···
+
value: null
+1
tests/data/scalars/null_tilde.yml
···
+
value: ~
+1
tests/data/scalars/number_float.yml
···
+
value: 3.14159
+1
tests/data/scalars/number_float.yml.json
···
+
{"value": 3.14159}
+1
tests/data/scalars/number_hex.yml
···
+
value: 0x2A
+1
tests/data/scalars/number_hex.yml.json
···
+
{"value": 42}
+1
tests/data/scalars/number_int.yml
···
+
value: 42
+1
tests/data/scalars/number_int.yml.json
···
+
{"value": 42}
+1
tests/data/scalars/number_negative.yml
···
+
value: -273.15
+1
tests/data/scalars/number_negative.yml.json
···
+
{"value": -273.15}
+1
tests/data/scalars/number_octal.yml
···
+
value: 0o52
+1
tests/data/scalars/number_octal.yml.json
···
+
{"value": 42}
+1
tests/data/scalars/special_inf.yml
···
+
value: .inf
+1
tests/data/scalars/special_nan.yml
···
+
value: .nan
+1
tests/data/scalars/special_neg_inf.yml
···
+
value: -.inf
+1
tests/data/scalars/string_empty.yml
···
+
value: ""
+1
tests/data/scalars/string_empty.yml.json
···
+
{"value": ""}
+1
tests/data/scalars/string_plain.yml
···
+
value: hello world
+1
tests/data/scalars/string_plain.yml.json
···
+
{"value": "hello world"}
+1
tests/data/scalars/string_quoted.yml
···
+
value: "42"
+1
tests/data/scalars/string_quoted.yml.json
···
+
{"value": "42"}
+28
yamlt.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "YAML codec using Jsont type descriptions"
+
description:
+
"Allows the same Jsont.t codec definitions to work for both JSON and YAML"
+
depends: [
+
"dune" {>= "3.18"}
+
"ocaml" {>= "4.14.0"}
+
"yamlrw"
+
"jsont"
+
"bytesrw"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]