From ee852228f14047ce239d8ea1f6d07ecccc9369f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Proust?= Date: Tue, 19 Jan 2021 16:44:05 +0100 Subject: [PATCH 1/3] Adapt to newer json-data-encoding --- data-encoding.opam | 4 ++-- src/json.ml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/data-encoding.opam b/data-encoding.opam index 19e00e20..36582f04 100644 --- a/data-encoding.opam +++ b/data-encoding.opam @@ -10,8 +10,8 @@ depends: [ "dune" { >= "1.7" } "ezjsonm" "zarith" - "json-data-encoding" { = "0.8" } - "json-data-encoding-bson" { = "0.8" } + "json-data-encoding" { = "0.9.1" } + "json-data-encoding-bson" { = "0.9.1" } "alcotest" { with-test } "crowbar" { with-test } ] diff --git a/src/json.ml b/src/json.ml index 9017ba4e..361004d7 100644 --- a/src/json.ml +++ b/src/json.ml @@ -91,6 +91,7 @@ let bytes_jsont = kind = String { + str_format = None; pattern = Some "^[a-zA-Z0-9]+$"; min_length = 0; max_length = None; -- GitLab From b35d497803e25a8153c7c848b92e50ec3eb7b7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Proust?= Date: Tue, 10 Nov 2020 16:52:25 +0100 Subject: [PATCH 2/3] Add converter from Json-lexeme-sequence to String-sequence Specifically, add three different variants with different intended uses. --- src/data_encoding.ml | 7 +- src/data_encoding.mli | 62 ++++ src/json.ml | 13 + src/json.mli | 13 + src/json_stream.ml | 505 +++++++++++++++++++++++++++++++++ src/json_stream.mli | 64 +++++ test/dune | 20 +- test/test_generated.ml | 95 ++++++- test/test_json_stream.ml | 249 ++++++++++++++++ test/test_json_stream_sizes.ml | 113 ++++++++ 10 files changed, 1130 insertions(+), 11 deletions(-) create mode 100644 src/json_stream.ml create mode 100644 src/json_stream.mli create mode 100644 test/test_json_stream.ml create mode 100644 test/test_json_stream_sizes.ml diff --git a/src/data_encoding.ml b/src/data_encoding.ml index fa6fb74f..77023bdd 100644 --- a/src/data_encoding.ml +++ b/src/data_encoding.ml @@ -135,7 +135,12 @@ end include Encoding module With_version = With_version module Registration = Registration -module Json = Json + +module Json = struct + include Json + include Json_stream +end + module Bson = Bson module Binary_schema = Binary_schema diff --git a/src/data_encoding.mli b/src/data_encoding.mli index 3398657b..a3936635 100644 --- a/src/data_encoding.mli +++ b/src/data_encoding.mli @@ -804,6 +804,68 @@ module Json : sig (** Construct a JSON object from an encoding. *) val construct : 't Encoding.t -> 't -> json + type jsonm_lexeme = + [ `Null + | `Bool of bool + | `String of string + | `Float of float + | `Name of string + | `As + | `Ae + | `Os + | `Oe ] + + (** [construct_seq enc t] is a representation of [t] as a sequence of json + lexeme ([jsonm_lexeme Seq.t]). This sequence is lazy: lexemes are computed + on-demand. *) + val construct_seq : 't Encoding.t -> 't -> jsonm_lexeme Seq.t + + (** [string_seq_of_jsonm_lexeme_seq ~newline ~chunk_size_hint s] is a sequence + of strings, the concatenation of which is a valid textual representation + of the json value represented by [s]. Each string chunk is roughly + [chunk_size_hint] long (except the last one that may be shorter). + + With the [newline] parameter set to [true], a single newline character is + appended to the textual representation. + + Forcing one element of the resulting string sequence forces multiple + elements of the underlying lexeme sequence. Once enough lexemes have been + forced that roughly [chunk_size_hint] characters are needed to reprensent + them, the representation is returned and the rest of the sequence is held + lazily. + + Note that most chunks split at a lexeme boundary. This may not be true for + string literals or float literals, the representation of which may be + spread across multiple chunks. *) + val string_seq_of_jsonm_lexeme_seq : + newline:bool -> chunk_size_hint:int -> jsonm_lexeme Seq.t -> string Seq.t + + val small_string_seq_of_jsonm_lexeme_seq : + newline:bool -> jsonm_lexeme Seq.t -> string Seq.t + + (** [blit_instructions_seq_of_jsonm_lexeme_seq ~newline ~buffer json] + is a sequence of [(buff, offset, length)] such that the concatenation of the + sub-strings thus designated represents the json value in text form. + + @raise [Invalid_argument _] if [Bytes.length buffer] is less than 32. + + The intended use is to blit each of the substring onto whatever output the + consumer decides. In most cases, the Sequence's [buff] is physically equal + to [buffer]. This is not always true and one cannot rely on that fact. E.g., + when the json includes a long string literal, the function might instruct + the consumer to blit from that literal directly. + + This function performs few allocations, especially of fresh strings. + + Note that once the next element of the sequence is forced, the blit + instructions become invalid: the content of [buff] may have been rewritten + by the side effect of forcing the next element. *) + val blit_instructions_seq_of_jsonm_lexeme_seq : + newline:bool -> + buffer:bytes -> + jsonm_lexeme Seq.t -> + (Bytes.t * int * int) Seq.t + (** Destruct a JSON object into a value. Fail with an exception if the JSON object and encoding do not match.. *) val destruct : 't Encoding.t -> json -> 't diff --git a/src/json.ml b/src/json.ml index 361004d7..859f79de 100644 --- a/src/json.ml +++ b/src/json.ml @@ -394,6 +394,19 @@ include Json_encoding let construct e v = construct (get_json e) v +type jsonm_lexeme = + [ `Null + | `Bool of bool + | `String of string + | `Float of float + | `Name of string + | `As + | `Ae + | `Os + | `Oe ] + +let construct_seq e v = construct_seq (get_json e) v + let destruct e v = destruct (get_json e) v let schema ?definitions_path e = schema ?definitions_path (get_json e) diff --git a/src/json.mli b/src/json.mli index aa7441c0..a0b24701 100644 --- a/src/json.mli +++ b/src/json.mli @@ -83,3 +83,16 @@ val to_string : ?newline:bool -> ?minify:bool -> json -> string val pp : Format.formatter -> json -> unit val bytes_jsont : Bytes.t Json_encoding.encoding + +type jsonm_lexeme = + [ `Null + | `Bool of bool + | `String of string + | `Float of float + | `Name of string + | `As + | `Ae + | `Os + | `Oe ] + +val construct_seq : 't Encoding.t -> 't -> jsonm_lexeme Seq.t diff --git a/src/json_stream.ml b/src/json_stream.ml new file mode 100644 index 00000000..48c39515 --- /dev/null +++ b/src/json_stream.ml @@ -0,0 +1,505 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2020 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type jsonm_lexeme = + [ `Null + | `Bool of bool + | `String of string + | `Float of float + | `Name of string + | `As + | `Ae + | `Os + | `Oe ] + +let string_of_float f = + let (fract, intr) = modf f in + if fract = 0.0 then Format.asprintf "%.0f" intr else Format.asprintf "%g" f + +let string_needs_escaping_at index s = + let exception At of int in + try + for i = index to String.length s - 1 do + match s.[i] with + | '\"' | '\n' | '\r' | '\b' | '\t' | '\\' | '\x00' .. '\x1F' -> + raise (At i) + | _ -> + () + done ; + -1 + with At i -> i + +let do_escape_string s = + let buff = Buffer.create (String.length s) in + for i = 0 to String.length s - 1 do + match s.[i] with + | '\"' -> + Buffer.add_string buff "\\\"" + | '\n' -> + Buffer.add_string buff "\\n" + | '\r' -> + Buffer.add_string buff "\\r" + | '\b' -> + Buffer.add_string buff "\\b" + | '\t' -> + Buffer.add_string buff "\\t" + | '\\' -> + Buffer.add_string buff "\\\\" + | '\x00' .. '\x1F' as c -> + Format.kasprintf (Buffer.add_string buff) "\\u%04x" (Char.code c) + | c -> + Buffer.add_char buff c + done ; + Buffer.contents buff + +let escape_string s = + if string_needs_escaping_at 0 s >= 0 then do_escape_string s else s + +(** small_string_seq_of_jsonm_lexeme_seq: converts a seq of lexeme into a naive + seq of small strings. This may or may not be appripriate depending on the + way the resulting seq is consumed. *) +let small_string_seq_of_jsonm_lexeme_seq ~newline (s : jsonm_lexeme Seq.t) : + string Seq.t = + let rec sseq first depth seq () = + match seq () with + | Seq.Nil -> + assert (depth = 0) ; + if newline then Seq.Cons ("\n", Seq.empty) else Seq.Nil + | Seq.Cons (`Null, seq) -> + let tail = Seq.Cons ("null", sseq false depth seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Bool true, seq) -> + let tail = Seq.Cons ("true", sseq false depth seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Bool false, seq) -> + let tail = Seq.Cons ("false", sseq false depth seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`As, seq) -> + let tail = Seq.Cons ("[", sseq true (depth + 1) seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Ae, seq) -> + Seq.Cons ("]", sseq false (depth - 1) seq) + | Seq.Cons (`Os, seq) -> + let tail = Seq.Cons ("{", sseq true (depth + 1) seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Oe, seq) -> + Seq.Cons ("}", sseq false (depth - 1) seq) + | Seq.Cons (`String s, seq) -> + let tail = + Seq.Cons + ( "\"", + fun () -> + Seq.Cons + ( escape_string s, + fun () -> Seq.Cons ("\"", sseq false depth seq) ) ) + in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Float f, seq) -> + let f = string_of_float f in + let tail = Seq.Cons (f, sseq false depth seq) in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + | Seq.Cons (`Name n, seq) -> + let tail = + Seq.Cons + ( "\"", + fun () -> + Seq.Cons + ( escape_string n, + fun () -> + Seq.Cons + ("\"", fun () -> Seq.Cons (":", sseq true depth seq)) + ) ) + in + if (not first) && depth > 0 then Seq.Cons (",", fun () -> tail) + else tail + in + sseq false 0 s + +let dump_unescaped_string chunk_size_hint buff str index seq = + let rec aux bytes_left_in_buff index = + if bytes_left_in_buff > String.length str - index then ( + Buffer.add_substring buff str index (String.length str - index) ; + seq () ) + else ( + Buffer.add_substring buff str index bytes_left_in_buff ; + let s = Buffer.contents buff in + Buffer.clear buff ; + Seq.Cons (s, fun () -> aux chunk_size_hint (index + bytes_left_in_buff)) + ) + in + aux (chunk_size_hint - Buffer.length buff) index + +(* This is written with the assumption that there aren't many characters that + need escaping: a few here and there, maybe just a couple of newlines in a + block of text. + + Breaking this assumption does not cause errors. However, the performances + might degrade somewhat. *) +let dump_escaped_string chunk_size_hint buff str seq = + let rec aux_outer bytes_left_in_buff index = + let next_escape = string_needs_escaping_at index str in + if bytes_left_in_buff <= 6 then ( + let s = Buffer.contents buff in + Buffer.clear buff ; + Seq.Cons (s, fun () -> aux_outer chunk_size_hint index) ) + else if next_escape < 0 then + (* string does not need escaping: dump the rest of the string as is *) + dump_unescaped_string chunk_size_hint buff str index seq + else if next_escape = 0 then ( + (* index is at character that needs escaping *) + ( match str.[index] with + | '\"' -> + Buffer.add_string buff "\\\"" + | '\n' -> + Buffer.add_string buff "\\n" + | '\r' -> + Buffer.add_string buff "\\r" + | '\b' -> + Buffer.add_string buff "\\b" + | '\t' -> + Buffer.add_string buff "\\t" + | '\\' -> + Buffer.add_string buff "\\\\" + | '\x00' .. '\x1F' as c -> + Format.kasprintf (Buffer.add_string buff) "\\u%04x" (Char.code c) + | c -> + Buffer.add_char buff c ) ; + aux_outer (chunk_size_hint - Buffer.length buff) (index + 1) ) + else + (* string needs escaping but later: write the non-empty non-escaped + prefix and loop back *) + let to_write_unescaped = next_escape - index in + if bytes_left_in_buff > to_write_unescaped then ( + Buffer.add_substring buff str index to_write_unescaped ; + aux_outer + (bytes_left_in_buff - to_write_unescaped) + (index + to_write_unescaped) ) + else + let rec aux_inner bytes_left_in_buff index to_write_unescaped continue + = + if bytes_left_in_buff < to_write_unescaped then ( + Buffer.add_substring buff str index bytes_left_in_buff ; + let s = Buffer.contents buff in + Seq.Cons + ( s, + fun () -> + aux_inner + chunk_size_hint + (index + bytes_left_in_buff) + (to_write_unescaped - bytes_left_in_buff) + continue ) ) + else ( + Buffer.add_substring buff str index to_write_unescaped ; + continue (index + to_write_unescaped) ) + in + aux_inner bytes_left_in_buff index to_write_unescaped (fun index -> + aux_outer (chunk_size_hint - Buffer.length buff) index) + in + aux_outer (chunk_size_hint - Buffer.length buff) 0 + +let dump_string_literal chunk_size_hint buff literal seq = + Buffer.add_char buff '"' ; + dump_escaped_string chunk_size_hint buff literal (fun () -> + Buffer.add_char buff '"' ; seq ()) + +let string_seq_of_jsonm_lexeme_seq ~newline ~chunk_size_hint + (s : jsonm_lexeme Seq.t) : string Seq.t = + (* we need chunk_size_hint to be reasonably high to accommodate all small + literals *) + let chunk_size_hint = min chunk_size_hint 16 in + (* we occasionally print several characters before checking the length of the + buffer (e.g., in the case of a key-value name with non-printable character + at the end: 6 for one hex-encoded non-printable character in a string + 1 + for the string closing double-quote + 1 for the key-value colon separator + = 8 in total) (e.g., [Float.pred 0.] is ["-4.94066e-324"] which is 12 + characters long). + So we allocate just above chunk_size +8 to avoid the need to resize. *) + let buff_size = chunk_size_hint + 16 in + (* single buffer for the whole serialisation *) + let buff = Buffer.create buff_size in + let rec sseq first depth seq () = + if Buffer.length buff >= buff_size then ( + (* emit buffer content if we have reached the chunk size *) + let b = Buffer.contents buff in + Buffer.clear buff ; + Seq.Cons (b, fun () -> (sseq [@ocaml.tailcall]) first depth seq ()) ) + else + match seq () with + (* termination *) + | Seq.Nil -> + assert (depth = 0) ; + if newline then Buffer.add_char buff '\n' ; + (* value terminator: newline *) + if Buffer.length buff = 0 then + (* corner case: we just flushed and haven't added a newline *) + Seq.Nil + else + let b = Buffer.contents buff in + Buffer.clear buff ; + Seq.Cons (b, Seq.empty) + (* fixed length, small lexemes *) + | Seq.Cons (`Null, seq) -> + (* if we are inside an object/array (i.e., depth is > 0) and we are + not the first item (i.e., first is false) then we put a delimiter + character. *) + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + (* then the value *) + Buffer.add_string buff "null" ; + (* and we continue with the rest. Note that depth is unchanged + but first is false whatever it's original value (because whatever + follows _follows_) *) + (sseq [@ocaml.tailcall]) false depth seq () + | Seq.Cons (`Bool true, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + Buffer.add_string buff "true" ; + (sseq [@ocaml.tailcall]) false depth seq () + | Seq.Cons (`Bool false, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + Buffer.add_string buff "false" ; + (sseq [@ocaml.tailcall]) false depth seq () + | Seq.Cons (`As, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + Buffer.add_char buff '[' ; + (* We increase the depth and mark the next value as being the first + value of an array. *) + (sseq [@ocaml.tailcall]) true (depth + 1) seq () + | Seq.Cons (`Ae, seq) -> + assert (depth > 0) ; + Buffer.add_char buff ']' ; + (sseq [@ocaml.tailcall]) false (depth - 1) seq () + | Seq.Cons (`Os, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + Buffer.add_char buff '{' ; + (sseq [@ocaml.tailcall]) true (depth + 1) seq () + | Seq.Cons (`Oe, seq) -> + assert (depth > 0) ; + Buffer.add_char buff '}' ; + (sseq [@ocaml.tailcall]) false (depth - 1) seq () + | Seq.Cons (`String s, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + (* we delegate string literals to [dump_string_literal]. Note that we + pass the rest of the sequence as a kind of continuation. This is + because [dump_string_literal] may fill up the buffer and then some + (depending on the size of the literal) and so it needs to be able + to stick a few things in front. *) + dump_string_literal chunk_size_hint buff s (fun () -> + (sseq [@ocaml.tailcall]) false depth seq ()) + | Seq.Cons (`Float f, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + let f = string_of_float f in + Buffer.add_string buff f ; + (sseq [@ocaml.tailcall]) false depth seq () + | Seq.Cons (`Name n, seq) -> + if (not first) && depth > 0 then Buffer.add_char buff ',' ; + dump_string_literal chunk_size_hint buff n (fun () -> + (* set first to true to avoid printing of separator *) + Buffer.add_char buff ':' ; + (sseq [@ocaml.tailcall]) true depth seq ()) + in + sseq false 0 s + +let biseq_escaped_string_content buffer offset s k = + let can_be_written = Bytes.length buffer - offset in + if String.length s + 1 > can_be_written + Bytes.length buffer then + if + (* large string and.. *) + (* TODO: present the string as a sequence of of blit instructions with + increasing offsets *) + offset < Bytes.length buffer / 2 + then ( + (* ..and the current buffer is almost empty: + dump as much as we can on the current buffer to avoid sending a + small buffer in the seq, and then use the rest of the string as its + own buffer *) + Bytes.blit_string s 0 buffer offset can_be_written ; + let offset = offset + can_be_written in + assert (offset = Bytes.length buffer) ; + Seq.Cons + ( (buffer, 0, offset), + fun () -> + let s = Bytes.unsafe_of_string s in + Seq.Cons + ( (s, can_be_written, Bytes.length s - can_be_written), + fun () -> k 0 ) ) ) + else + (* ..and the buffer is reasonably full: + put the current buffer in the seq and then the string as a single + chunk *) + Seq.Cons + ( (buffer, 0, offset), + fun () -> + let s = Bytes.unsafe_of_string s in + Seq.Cons ((s, 0, Bytes.length s), fun () -> k 0) ) + else if String.length s + 1 <= can_be_written then ( + (* we [+ 1] to account for the closing quote that will be added by [k] *) + (* small string: we dump it on the buffer and continue *) + Bytes.blit_string s 0 buffer offset (String.length s) ; + let offset = offset + String.length s in + k offset ) + else ( + (* medium string: we blit two parts onto the buffer *) + Bytes.blit_string s 0 buffer offset can_be_written ; + let offset = offset + can_be_written in + assert (offset = Bytes.length buffer) ; + Seq.Cons + ( (buffer, 0, offset), + fun () -> + let remain_to_be_written = String.length s - can_be_written in + Bytes.blit_string s can_be_written buffer 0 remain_to_be_written ; + let offset = remain_to_be_written in + k offset ) ) + +let biseq_string_literal buffer offset s k = + Bytes.set buffer offset '"' ; + let offset = offset + 1 in + let first_escape = string_needs_escaping_at 0 s in + if first_escape < 0 then + biseq_escaped_string_content buffer offset s (fun offset -> + Bytes.set buffer offset '"' ; + k (offset + 1)) + else + (* NOTE: offset can't be 0 because we just wrote '"', also the string cannot + be empty because this is matched-for earlier *) + (* TODO: optimise by escaping using the available buffer *) + let s = do_escape_string s in + biseq_escaped_string_content buffer offset s (fun offset -> + Bytes.set buffer offset '"' ; + k (offset + 1)) + +let blit_instructions_seq_of_jsonm_lexeme_seq ~newline ~buffer lexeme_seq = + let buffer_size = Bytes.length buffer in + if buffer_size < 32 then + raise + (Invalid_argument + "Data_encoding.blit_instructions_seq_of_jsonm_lexeme_seq") ; + let flush_at = buffer_size - 16 in + let[@ocaml.inline] sep first depth offset = + (* if we are inside an object/array (i.e., depth is > 0) and we are + not the first item (i.e., first is false) then we put a delimiter + character. *) + if (not first) && depth > 0 then ( + Bytes.set buffer offset ',' ; + offset + 1 ) + else offset + in + let rec biseq first depth offset seq () = + if offset >= flush_at then + (* emit buffer content if we have reached the chunk size *) + Seq.Cons + ( (buffer, 0, offset), + fun () -> (biseq [@ocaml.tailcall]) first depth 0 seq () ) + else + match seq () with + (* termination *) + | Seq.Nil -> + assert (depth = 0) ; + let offset = + if newline then ( + Bytes.set buffer offset '\n' ; + offset + 1 ) + else offset + in + if offset = 0 then + (* corner case: we just flushed (and haven't added a newline) *) + Seq.Nil + else Seq.Cons ((buffer, 0, offset), fun () -> Seq.Nil) + (* fixed length, small lexemes *) + | Seq.Cons (`Null, seq) -> + let offset = sep first depth offset in + Bytes.blit_string "null" 0 buffer offset 4 ; + let offset = offset + 4 in + (biseq [@ocaml.tailcall]) false depth offset seq () + | Seq.Cons (`Bool true, seq) -> + let offset = sep first depth offset in + Bytes.blit_string "true" 0 buffer offset 4 ; + let offset = offset + 4 in + (biseq [@ocaml.tailcall]) false depth offset seq () + | Seq.Cons (`Bool false, seq) -> + let offset = sep first depth offset in + Bytes.blit_string "false" 0 buffer offset 5 ; + let offset = offset + 5 in + (biseq [@ocaml.tailcall]) false depth offset seq () + | Seq.Cons (`As, seq) -> + let offset = sep first depth offset in + Bytes.set buffer offset '[' ; + let offset = offset + 1 in + (biseq [@ocaml.tailcall]) true (depth + 1) offset seq () + | Seq.Cons (`Ae, seq) -> + Bytes.set buffer offset ']' ; + let offset = offset + 1 in + (biseq [@ocaml.tailcall]) false (depth - 1) offset seq () + | Seq.Cons (`Os, seq) -> + let offset = sep first depth offset in + Bytes.set buffer offset '{' ; + let offset = offset + 1 in + (biseq [@ocaml.tailcall]) true (depth + 1) offset seq () + | Seq.Cons (`Oe, seq) -> + Bytes.set buffer offset '}' ; + let offset = offset + 1 in + (biseq [@ocaml.tailcall]) false (depth - 1) offset seq () + | Seq.Cons (`String "", seq) -> + let offset = sep first depth offset in + Bytes.blit_string "\"\"" 0 buffer offset 2 ; + let offset = offset + 2 in + (biseq [@ocaml.tailcall]) false depth offset seq () + | Seq.Cons (`String s, seq) -> + let offset = sep first depth offset in + (* we delegate string literals to [dump_string_literal]. Note that we + pass the rest of the sequence as a kind of continuation. This is + because [dump_string_literal] may fill up the buffer and then some + (depending on the size of the literal) and so it needs to be able + to stick a few things in front. *) + biseq_string_literal buffer offset s (fun offset -> + (biseq [@ocaml.tailcall]) false depth offset seq ()) + | Seq.Cons (`Float f, seq) -> + let offset = sep first depth offset in + let f = string_of_float f in + biseq_escaped_string_content buffer offset f (fun offset -> + (biseq [@ocaml.tailcall]) false depth offset seq ()) + | Seq.Cons (`Name n, seq) -> + let offset = sep first depth offset in + biseq_string_literal buffer offset n (fun offset -> + if offset = buffer_size then + Seq.Cons + ( (buffer, 0, offset), + fun () -> + Bytes.set buffer 0 ':' ; + let offset = 1 in + (* set first to true to avoid printing of separator *) + (biseq [@ocaml.tailcall]) true depth offset seq () ) + else ( + (* set first to true to avoid printing of separator *) + Bytes.set buffer offset ':' ; + let offset = offset + 1 in + (biseq [@ocaml.tailcall]) true depth offset seq () )) + in + biseq false 0 0 lexeme_seq diff --git a/src/json_stream.mli b/src/json_stream.mli new file mode 100644 index 00000000..9bf82f26 --- /dev/null +++ b/src/json_stream.mli @@ -0,0 +1,64 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2020 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type jsonm_lexeme = + [ `Null + | `Bool of bool + | `String of string + | `Float of float + | `Name of string + | `As + | `Ae + | `Os + | `Oe ] + +val small_string_seq_of_jsonm_lexeme_seq : + newline:bool -> jsonm_lexeme Seq.t -> string Seq.t + +val string_seq_of_jsonm_lexeme_seq : + newline:bool -> chunk_size_hint:int -> jsonm_lexeme Seq.t -> string Seq.t + +(** [blit_instructions_seq_of_jsonm_lexeme_seq ~newline ~buffer json] + is a sequence of [(buff, offset, length)] such that the concatenation of the + sub-strings thus designated represents the json value in text form. + + @raise [Invalid_argument _] if [Bytes.length buffer] is less than 32. + + The intended use is to blit each of the substring onto whatever output the + consumer decides. In most cases, the Sequence's [buff] is physically equal + to [buffer]. This is not always true and one cannot rely on that fact. E.g., + when the json includes a long string literal, the function might instruct + the consumer to blit from that literal directly. + + This function performs few allocations, especially of fresh strings. + + Note that once the next element of the sequence is forced, the blit + instructions become invalid: the content of [buff] may have been rewritten + by the side effect of forcing the next element. *) +val blit_instructions_seq_of_jsonm_lexeme_seq : + newline:bool -> + buffer:bytes -> + jsonm_lexeme Seq.t -> + (Bytes.t * int * int) Seq.t diff --git a/test/dune b/test/dune index 3da71628..b1b79b31 100644 --- a/test/dune +++ b/test/dune @@ -1,11 +1,13 @@ (executables - (names test test_generated bench_data_encoding) + (names test test_generated test_json_stream test_json_stream_sizes + bench_data_encoding) (libraries data_encoding alcotest crowbar) (flags :standard)) (alias (name buildtest) - (deps test.exe test_generated.exe bench_data_encoding.exe)) + (deps test.exe test_generated.exe test_json_stream.exe + test_json_stream_sizes.exe bench_data_encoding.exe)) (alias (name runtest_test) @@ -17,11 +19,23 @@ (action (run %{exe:test_generated.exe}))) +(alias + (name runtest_test_json_stream) + (action + (run %{exe:test_json_stream.exe}))) + +(alias + (name runtest_test_json_stream_sizes) + (action + (run %{exe:test_json_stream_sizes.exe}))) + (alias (name runtest) (deps (alias runtest_test) - (alias runtest_test_generated))) + (alias runtest_test_generated) + (alias runtest_test_json_stream) + (alias runtest_test_json_stream_sizes))) (alias (name run_bench) diff --git a/test/test_generated.ml b/test/test_generated.ml index da1c6d17..422b75da 100644 --- a/test/test_generated.ml +++ b/test/test_generated.ml @@ -33,6 +33,10 @@ let char = Crowbar.map [Crowbar.uint8] Char.chr let string = Crowbar.bytes +let ascii_letter = + let open Crowbar in + map [choose [range ~min:65 26; range ~min:97 26]] Char.chr + (* The v0.1 of Crowbar doesn't have fixed-size string generation. When we * update Crowbar, we can improve this generator. *) let short_string = @@ -40,8 +44,10 @@ let short_string = choose [ const ""; - map [char] (fun c -> String.make 1 c); - map [char; char; char; char] (fun c1 c2 c3 c4 -> + map [ascii_letter] (fun c -> String.make 1 c); + map + [ascii_letter; ascii_letter; ascii_letter; ascii_letter] + (fun c1 c2 c3 c4 -> let s = Bytes.make 4 c1 in Bytes.set s 1 c2 ; Bytes.set s 2 c3 ; @@ -53,8 +59,10 @@ let short_string1 = let open Crowbar in choose [ - map [char] (fun c -> String.make 1 c); - map [char; char; char; char] (fun c1 c2 c3 c4 -> + map [ascii_letter] (fun c -> String.make 1 c); + map + [ascii_letter; ascii_letter; ascii_letter; ascii_letter] + (fun c1 c2 c3 c4 -> let s = Bytes.make 4 c1 in Bytes.set s 1 c2 ; Bytes.set s 2 c3 ; @@ -1224,17 +1232,19 @@ let gen = (* TODO: use newer version of crowbar to get these generators map [int16] map_int16; map [uint16] map_uint16; - *) + *) map [int32] map_int32; map [int64] map_int64; (* NOTE: the int encoding require ranges to be 30-bit compatible *) map [int8; int8; int8] map_range_int; + (* FLOATS don't roundtrip because of pretty printing map [float; float; float] map_range_float; - map [bool] map_bool; + map [float] map_float; + *) + map [bool] map_bool; map [short_string] map_string; map [short_string; uint8] map_string_with_check_size; map [short_mbytes] map_bytes; - map [float] map_float; map [short_string1] map_fixed_string; map [short_mbytes1] map_fixed_bytes; map [short_string] map_variable_string; @@ -1327,6 +1337,69 @@ let roundtrip_json pp ding v = in Crowbar.check_eq ~pp v vv +let pp_jsonm_lexeme fmt = function + | `Null -> + Format.pp_print_string fmt "(null)" + | `Bool true -> + Format.pp_print_string fmt "(true)" + | `Bool false -> + Format.pp_print_string fmt "(false)" + | `String _ -> + Format.pp_print_string fmt "(string)" + | `Float f -> + Format.fprintf fmt "(float:%f)" f + | `Name _ -> + Format.pp_print_string fmt "(name)" + | `As -> + Format.pp_print_char fmt '[' + | `Ae -> + Format.pp_print_char fmt ']' + | `Os -> + Format.pp_print_char fmt '{' + | `Oe -> + Format.pp_print_char fmt '}' + +let pp_jsonm_lexeme_seq fmt s = Seq.iter (pp_jsonm_lexeme fmt) s + +let roundtrip_json_stream pp ding v = + let json = + try Data_encoding.Json.construct_seq ding v + with Invalid_argument m -> + Crowbar.fail (Format.asprintf "Cannot construct: %a (%s)" pp v m) + in + let str = + Seq.fold_left ( ^ ) "" + @@ Data_encoding.Json.string_seq_of_jsonm_lexeme_seq + ~newline:false + ~chunk_size_hint:128 + json + in + let ezjsonm = + match Data_encoding.Json.from_string str with + | Error msg -> + Crowbar.failf "%s (%a) (%s)" msg pp_jsonm_lexeme_seq json str + | Ok json -> + json + in + let vv = + try Data_encoding.Json.destruct ding ezjsonm + with Data_encoding.Json.Cannot_destruct (_, _) -> + Crowbar.fail "Cannot destruct" + in + if v = vv then Crowbar.check true + else + Crowbar.failf + "value: %a@\njsonm_lexeme: %a@\nstring: %s@\nezjsonm: %a;@\nvalue: %a" + pp + v + pp_jsonm_lexeme_seq + json + str + Data_encoding.Json.pp + ezjsonm + pp + vv + let roundtrip_binary_to_bytes pp ding v = let bin = try Data_encoding.Binary.to_bytes_exn ding v @@ -1418,6 +1491,10 @@ let test_testable_json (testable : testable) = let module T = (val testable) in roundtrip_json T.pp T.ding T.v +let test_testable_json_stream (testable : testable) = + let module T = (val testable) in + roundtrip_json_stream T.pp T.ding T.v + let test_testable_binary_to_bytes (testable : testable) = let module T = (val testable) in roundtrip_binary_to_bytes T.pp T.ding T.v @@ -1447,4 +1524,8 @@ let () = ~name:"json (construct/destruct) roundtrips" [gen] test_testable_json ; + Crowbar.add_test + ~name:"json-stream roundtrips" + [gen] + test_testable_json_stream ; () diff --git a/test/test_json_stream.ml b/test/test_json_stream.ml new file mode 100644 index 00000000..6fd76d30 --- /dev/null +++ b/test/test_json_stream.ml @@ -0,0 +1,249 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2020 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +type json = + [ `O of (string * json) list + | `Bool of bool + | `Float of float + | `A of json list + | `Null + | `String of string ] + +let pp_json : json Crowbar.printer = + fun fmt json -> Format.fprintf fmt "%s" (Data_encoding.Json.to_string json) + +let ascii_letter = + let open Crowbar in + map [choose [range ~min:65 26; range ~min:97 26]] Char.chr + +let names = + let open Crowbar in + map [list1 ascii_letter] (fun ls -> String.of_seq @@ List.to_seq ls) + +let string = + let open Crowbar in + map [uint8; ascii_letter] String.make + +let longstring = + let open Crowbar in + with_printer (fun fmt s -> + if String.length s < 8 then Format.fprintf fmt "\"%s\"" s + else Format.fprintf fmt "%c(%04d)" s.[0] (String.length s)) + @@ map [range 4096; ascii_letter] String.make + +let ezjson : json Crowbar.gen = + (* We don't generate random floats/strings because it doesn't roundtrip on + even the standard printer. *) + (* We generate long strings to test chunking. *) + let open Crowbar in + fix (fun json -> + let field = map [names; json] (fun k v -> (k, v)) in + choose + [ + map [list field] (fun kvs -> + let (has_dup, _) = + List.fold_left + (fun (has, seen) (k, _) -> + let has = has || List.exists (( = ) k) seen in + if has then (has, []) else (has, k :: seen)) + (false, []) + kvs + in + if has_dup then bad_test () else `O kvs); + map [bool] (fun b -> `Bool b); + map [list json] (fun vs -> `A vs); + const `Null; + map [string] (fun s -> `String s); + map [longstring] (fun s -> `String s); + map [int32] (fun i -> `Float (Int32.to_float i)); + (* map [float] (fun f -> `Float f); *) + + ]) + +let ezjson = Crowbar.with_printer pp_json ezjson + +let large_ezjson = + (* special generator for testing large values *) + let open Crowbar in + with_printer pp_json + @@ map [ezjson; ezjson; ezjson] (fun j1 j2 j3 -> + `A + (List.init 16 (fun _ -> + `A + (List.init 16 (fun _ -> + `O + [ + ("j1", j1); + ("j2", `A [j2; `Bool true]); + ("this", `Null); + ("j3", j3); + ]))))) + +let jsonm_lexeme_seq = + Crowbar.map [ezjson] Json_encoding.jsonm_lexeme_seq_of_ezjson + +let () = + let open Crowbar in + add_test ~name:"gen" [jsonm_lexeme_seq] (fun j -> ignore j ; check true) + +let () = + let open Crowbar in + add_test ~name:"small_string_serialisation" [jsonm_lexeme_seq] (fun j -> + let s = + Data_encoding.Json.small_string_seq_of_jsonm_lexeme_seq + ~newline:false + j + in + Seq.iter ignore s) + +let () = + let open Crowbar in + add_test + ~name:"small_string_serialisation-deserialisation" + [ezjson] + (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let s = + Data_encoding.Json.small_string_seq_of_jsonm_lexeme_seq + ~newline:false + j + in + let s = Seq.fold_left ( ^ ) "" s in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) + +let () = + let open Crowbar in + add_test ~name:"serialisation" [jsonm_lexeme_seq] (fun j -> + let s = + Data_encoding.Json.string_seq_of_jsonm_lexeme_seq + ~newline:false + ~chunk_size_hint:512 + j + in + Seq.iter ignore s) + +let () = + let open Crowbar in + add_test ~name:"serialisation(16)-deserialisation" [ezjson] (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let s = + Data_encoding.Json.string_seq_of_jsonm_lexeme_seq + ~newline:false + ~chunk_size_hint:16 + j + in + let s = Seq.fold_left ( ^ ) "" s in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) + +let () = + let open Crowbar in + add_test ~name:"serialisation(1024)-deserialisation" [ezjson] (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let s = + Data_encoding.Json.string_seq_of_jsonm_lexeme_seq + ~newline:false + ~chunk_size_hint:1024 + j + in + let s = Seq.fold_left ( ^ ) "" s in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) + +let () = + let open Crowbar in + add_test ~name:"blit-instructions(32)" [ezjson] (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let buffer = Bytes.create 32 in + let s = + Data_encoding.Json.blit_instructions_seq_of_jsonm_lexeme_seq + ~newline:false + ~buffer + j + in + let s = + let b = Buffer.create 8 in + Seq.iter (fun (s, o, l) -> Buffer.add_subbytes b s o l) s ; + Buffer.contents b + in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) + +let () = + let open Crowbar in + add_test ~name:"blit-instructions(1024)" [ezjson] (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let buffer = Bytes.create 1024 in + let s = + Data_encoding.Json.blit_instructions_seq_of_jsonm_lexeme_seq + ~newline:false + ~buffer + j + in + let s = + let b = Buffer.create 8 in + Seq.iter (fun (s, o, l) -> Buffer.add_subbytes b s o l) s ; + Buffer.contents b + in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) + +let () = + let open Crowbar in + add_test ~name:"large value, blit instructions" [large_ezjson] (fun ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let buffer = Bytes.create 128 in + let s = + Data_encoding.Json.blit_instructions_seq_of_jsonm_lexeme_seq + ~newline:false + ~buffer + j + in + let s = + let b = Buffer.create 128 in + Seq.iter (fun (s, o, l) -> Buffer.add_subbytes b s o l) s ; + Buffer.contents b + in + match Data_encoding.Json.from_string s with + | Error e -> + fail e + | Ok j -> + check_eq ~pp:pp_json ezj j) diff --git a/test/test_json_stream_sizes.ml b/test/test_json_stream_sizes.ml new file mode 100644 index 00000000..fc3f9093 --- /dev/null +++ b/test/test_json_stream_sizes.ml @@ -0,0 +1,113 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2020 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(* This test is for stress-testing the cases of strings-heavy objects with + various sizes of blitting. It only searches for exceptions in the serialising + process, not for round-trip errors. *) + +type json = + [ `O of (string * json) list + | `Bool of bool + | `Float of float + | `A of json list + | `Null + | `String of string ] + +let pp_json : json Crowbar.printer = + fun fmt json -> Format.fprintf fmt "%s" (Data_encoding.Json.to_string json) + +let ascii_letter = + let open Crowbar in + map [choose [range ~min:65 26; range ~min:97 26]] Char.chr + +let names = + let open Crowbar in + map [list1 ascii_letter] (fun ls -> String.of_seq @@ List.to_seq ls) + +let strings = + (* statically allocated collection *) + let char n = + [|'0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9'|].(n mod 10) + in + Array.init 146 (fun i -> String.init i char) + +let string = + let open Crowbar in + map [range 146] (Array.get strings) + +let ezjson : json Crowbar.gen = + let open Crowbar in + fix (fun json -> + let field = map [string; json] (fun n j -> (n, j)) in + choose + [ + map [list json] (fun jsons -> `A jsons); + map [string] (fun s -> `String s); + map + [string; string; string; string; string; string] + (fun s1 s2 s3 s4 s5 s6 -> + `A + [ + `String s1; + `String s2; + `String s3; + `String s4; + `String s5; + `String s6; + ]); + const `Null; + map [list field] (fun kvs -> `O kvs); + ]) + +let () = + let open Crowbar in + add_test + ~name:"blitting with a lot of string boundaries" + [range ~min:32 16; ezjson] + (fun buff_size ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let buffer = Bytes.create buff_size in + let s = + Data_encoding.Json.blit_instructions_seq_of_jsonm_lexeme_seq + ~newline:false + ~buffer + j + in + Seq.iter ignore s) + +let () = + let open Crowbar in + add_test + ~name:"string-seq with a lot of string boundaries" + [range ~min:32 16; ezjson] + (fun chunk_size_hint ezj -> + let j = Json_encoding.jsonm_lexeme_seq_of_ezjson ezj in + let s = + Data_encoding.Json.string_seq_of_jsonm_lexeme_seq + ~newline:false + ~chunk_size_hint + j + in + Seq.iter ignore s) -- GitLab From 564a1f6bd7af9595bc2d1bef82c4664dc12d784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Proust?= Date: Tue, 29 Dec 2020 11:08:45 +0100 Subject: [PATCH 3/3] Bump dune-lang version --- dune-project | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dune-project b/dune-project index f3aaead3..a189cea8 100644 --- a/dune-project +++ b/dune-project @@ -1,3 +1,3 @@ -(lang dune 1.7) +(lang dune 1.11) (name data-encoding) (using fmt 1.1) -- GitLab