Skip to content

Commit

Permalink
Add suf separator to configure what terminates JSON values
Browse files Browse the repository at this point in the history
This is a generalization of `newline`, but it works for all writing
functions and can be overridden.
  • Loading branch information
Leonidas-from-XIV committed Mar 3, 2022
1 parent 700a222 commit 0330104
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 69 deletions.
7 changes: 5 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
(@tmcgilchrist, #117)
- Add a benchmark to judge the respective performance of providing a buffer vs
letting Yojson create an internal (#134, @Leonidas-from-XIV)
- Add an optional `suf` keyword argument was added to functions that write
serialized JSON, thus allowing NDJSON output. Most functions default to not
adding any suffix except for `to_file` (#124, @panglesd) and functions
writing sequences of values where the default is `\n` (#135,
@Leonidas-from-XIV)

### Change

- The function `to_file` now adds a newline at the end of the generated file. An
optional argument allows to return to the original behaviour (#124, @panglesd)
- The `stream_from_*` and `stream_to_*` functions now use a `Seq.t` instead of a
`Stream.t`, and they are renamed into `seq_from_*` and `seq_to_*` (@gasche, #131).

Expand Down
40 changes: 19 additions & 21 deletions lib/write.ml
Original file line number Diff line number Diff line change
Expand Up @@ -417,89 +417,87 @@ and write_std_variant ob s o =
#endif


let to_buffer ?(std = false) ob x =
let to_buffer ?(suf = "") ?(std = false) ob x =
if std then
write_std_json ob x
else
write_json ob x

write_json ob x;
Buffer.add_string ob suf

let to_string ?buf ?(len = 256) ?std x =
let to_string ?buf ?(len = 256) ?(suf = "") ?std x =
let ob =
match buf with
None -> Buffer.create len
| Some ob ->
Buffer.clear ob;
ob
in
to_buffer ?std ob x;
to_buffer ~suf ?std ob x;
let s = Buffer.contents ob in
Buffer.clear ob;
s

let to_channel ?buf ?(len=4096) ?std oc x =
let to_channel ?buf ?(len=4096) ?(suf = "") ?std oc x =
let ob =
match buf with
None -> Buffer.create len
| Some ob -> Buffer.clear ob; ob
in
to_buffer ?std ob x;
to_buffer ~suf ?std ob x;
Buffer.output_buffer oc ob;
Buffer.clear ob

let to_output ?buf ?(len=4096) ?std out x =
let to_output ?buf ?(len=4096) ?(suf = "") ?std out x =
let ob =
match buf with
None -> Buffer.create len
| Some ob -> Buffer.clear ob; ob
in
to_buffer ?std ob x;
to_buffer ~suf ?std ob x;
out#output (Buffer.contents ob) 0 (Buffer.length ob);
Buffer.clear ob

let to_file ?len ?std ?(newline = true) file x =
let to_file ?len ?std ?(suf = "\n") file x =
let oc = open_out file in
try
to_channel ?len ?std oc x;
if newline then
output_string oc "\n";
to_channel ?len ~suf ?std oc x;
close_out oc
with e ->
close_out_noerr oc;
raise e

let seq_to_buffer ?std ob st =
Seq.iter (to_buffer ?std ob) st
let seq_to_buffer ?(suf = "\n") ?std ob st =
Seq.iter (to_buffer ~suf ?std ob) st

let seq_to_string ?buf ?(len = 256) ?std st =
let seq_to_string ?buf ?(len = 256) ?(suf = "\n") ?std st =
let ob =
match buf with
None -> Buffer.create len
| Some ob ->
Buffer.clear ob;
ob
in
seq_to_buffer ?std ob st;
seq_to_buffer ~suf ?std ob st;
let s = Buffer.contents ob in
Buffer.clear ob;
s

let seq_to_channel ?buf ?(len=2096) ?std oc seq =
let seq_to_channel ?buf ?(len=2096) ?(suf = "\n") ?std oc seq =
let ob =
match buf with
None -> Buffer.create len
| Some ob -> Buffer.clear ob; ob
in
Seq.iter (fun json ->
to_buffer ?std ob json;
to_buffer ~suf ?std ob json;
Buffer.output_buffer oc ob;
Buffer.clear ob;
) seq

let seq_to_file ?len ?std file st =
let seq_to_file ?len ?(suf = "\n") ?std file st =
let oc = open_out file in
try
seq_to_channel ?len ?std oc st;
seq_to_channel ?len ~suf ?std oc st;
close_out oc
with e ->
close_out_noerr oc;
Expand Down
28 changes: 21 additions & 7 deletions lib/write.mli
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
val to_string :
?buf:Buffer.t ->
?len:int ->
?suf:string ->
?std:bool ->
t -> string
(** Write a compact JSON value to a string.
@param buf allows to reuse an existing buffer created with
[Buffer.create]. The buffer is cleared of all contents
before starting and right before returning.
@param len initial length of the output buffer.
@param suf appended to the output as a suffix,
defaults to empty string.
@param std use only standard JSON syntax,
i.e. convert tuples and variants into standard JSON (if applicable),
refuse to print NaN and infinities,
Expand All @@ -20,6 +23,7 @@ val to_string :
val to_channel :
?buf:Buffer.t ->
?len:int ->
?suf:string ->
?std:bool ->
out_channel -> t -> unit
(** Write a compact JSON value to a channel.
Expand All @@ -30,6 +34,7 @@ val to_channel :
val to_output :
?buf:Buffer.t ->
?len:int ->
?suf:string ->
?std:bool ->
< output : string -> int -> int -> int; .. > -> t -> unit
(** Write a compact JSON value to an OO channel.
Expand All @@ -39,14 +44,15 @@ val to_output :
val to_file :
?len:int ->
?std:bool ->
?newline:bool ->
?suf:string ->
string -> t -> unit
(** Write a compact JSON value to a file.
See [to_string] for the role of the optional arguments.
@param newline whether to end the content of the file with a new line.
Optional parameter with value [true] by default. *)
@param suf is a suffix appended to the output Newline by default
for POSIX compliance. *)

val to_buffer :
?suf:string ->
?std:bool ->
Buffer.t -> t -> unit
(** Write a compact JSON value to an existing buffer.
Expand All @@ -55,35 +61,43 @@ val to_buffer :
val seq_to_string :
?buf:Buffer.t ->
?len:int ->
?suf:string ->
?std:bool ->
t Seq.t -> string
(** Write a newline-separated sequence of compact one-line JSON values to
(** Write a sequence of [suf]-suffixed compact one-line JSON values to
a string.
@param suf is the suffix ouf each value written. Newline by default.
See [to_string] for the role of the optional arguments. *)

val seq_to_channel :
?buf:Buffer.t ->
?len:int ->
?suf:string ->
?std:bool ->
out_channel -> t Seq.t -> unit
(** Write a newline-separated sequence of compact one-line JSON values to
(** Write a sequence of [suf]-suffixed compact one-line JSON values to
a channel.
@param suf is the suffix of each value written. Newline by default.
See [to_channel] for the role of the optional arguments. *)

val seq_to_file :
?len:int ->
?suf:string ->
?std:bool ->
string -> t Seq.t -> unit
(** Write a newline-separated sequence of compact one-line JSON values to
(** Write a sequence of [suf]-suffixed compact one-line JSON values to
a file.
@param suf is the suffix of each value written. Newline by default.
See [to_string] for the role of the optional arguments. *)

val seq_to_buffer :
?suf:string ->
?std:bool ->
Buffer.t ->
t Seq.t -> unit
(** Write a newline-separated sequence of compact one-line JSON values to
(** Write a sequence of [suf]-suffixed compact one-line JSON values to
an existing buffer.
@param suf is the suffix of each value written. Newline by default.
See [to_string] for the role of the optional arguments. *)

val write_t : Buffer.t -> t -> unit
Expand Down
82 changes: 43 additions & 39 deletions test/test_write.ml
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
let to_string () =
Alcotest.(check string) __LOC__ Fixtures.json_string (Yojson.Safe.to_string Fixtures.json_value)
let to_string_tests =
let test ?suf expected =
Alcotest.(check string) __LOC__ expected (Yojson.Safe.to_string ?suf Fixtures.json_value)
in
[
"to_string with default settings", `Quick, (fun () -> test Fixtures.json_string);
"to_string with newline", `Quick, (fun () -> test ~suf:"\n" Fixtures.json_string_newline);
"to_string without newline", `Quick, (fun () -> test ~suf:"" Fixtures.json_string);
]

let to_file () =
let test ?newline () =
let to_file_tests =
let test ?suf expected =
let output_file = Filename.temp_file "test_yojson_to_file" ".json" in
let correction = match newline with
| None ->
Yojson.Safe.to_file output_file Fixtures.json_value;
Fixtures.json_string_newline
| Some newline ->
Yojson.Safe.to_file ~newline output_file Fixtures.json_value;
if newline then
Fixtures.json_string_newline
else
Fixtures.json_string
in
Yojson.Safe.to_file ?suf output_file Fixtures.json_value;
let file_content =
let ic = open_in output_file in
let length = in_channel_length ic in
let s = really_input_string ic length in
close_in ic;
s
in
Alcotest.(check string) __LOC__ correction file_content;
Sys.remove output_file
Sys.remove output_file;
Alcotest.(check string) __LOC__ expected file_content
in
test ();
test ~newline:true ();
test ~newline:false ()
[
"to_file with default settings", `Quick, (fun () -> test Fixtures.json_string_newline);
"to_file with newline", `Quick, (fun () -> test ~suf:"\n" Fixtures.json_string_newline);
"to_file without newline", `Quick, (fun () -> test ~suf:"" Fixtures.json_string);
]

(* List.to_seq is not available on old OCaml versions. *)
let rec list_to_seq = function
| [] -> (fun () -> Seq.Nil)
| x :: xs -> (fun () -> Seq.Cons (x, list_to_seq xs))

let seq_to_file () =
let output_file = Filename.temp_file "test_yojson_seq_to_file" ".json" in
let data = [`String "foo"; `String "bar"] in
Yojson.Safe.seq_to_file output_file (list_to_seq data);
let read_data =
let seq = Yojson.Safe.seq_from_file output_file in
let acc = ref [] in
Seq.iter (fun v -> acc := v :: !acc) seq;
List.rev !acc
let seq_to_file_tests =
let test ?suf () =
let output_file = Filename.temp_file "test_yojson_seq_to_file" ".json" in
let data = [`String "foo"; `String "bar"] in
Yojson.Safe.seq_to_file ?suf output_file (list_to_seq data);
let read_data =
let seq = Yojson.Safe.seq_from_file output_file in
let acc = ref [] in
Seq.iter (fun v -> acc := v :: !acc) seq;
List.rev !acc
in
Sys.remove output_file;
Alcotest.(check (list Testable.yojson)) "seq_{to,from}_file roundtrip" data read_data
in
Sys.remove output_file;
if data <> read_data then
(* TODO: it would be nice to use Alcotest.check,
but we don't have a 'testable' instance for JSON values. *)
Alcotest.fail "seq_{to,from}_file roundtrip failure"
[
"seq_to_file with default settings", `Quick, (fun () -> test ());
"seq_to_file with newline", `Quick, (fun () -> test ~suf:"\n" ());
"seq_to_file without newline", `Quick, (fun () -> test ~suf:"" ());
]

let single_json = [
"to_string", `Quick, to_string;
"to_file", `Quick, to_file;
"seq_to_file", `Quick, seq_to_file;
]
let single_json =
List.flatten [
to_file_tests;
to_string_tests;
seq_to_file_tests;
]

0 comments on commit 0330104

Please sign in to comment.