Skip to content

Commit d6e2381

Browse files
committed
ppx: add to_json_string,of_json_string,json_string derivers
1 parent f83aec6 commit d6e2381

10 files changed

+229
-60
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
- PPX: change JSON representation of polyvariants, make it compatible with
1212
ppx_deriving_yojson and ppx_yojson_conv
1313
([#27](https://github.com/melange-community/melange-json/pull/27))
14+
- PPX: add `[@@json_string]` for deriving converters to/from JSON strings
15+
directly
16+
([#30](https://github.com/melange-community/melange-json/pull/30))
1417

1518
## 1.3.0 (2024-08-28)
1619

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,21 @@ let json = to_json B
279279
(* "bbb" *)
280280
```
281281

282+
#### `[@@deriving json_string]`: a shortcut for JSON string conversion
283+
284+
For convenience, one can use `[@@deriving json_string]` to generate converters
285+
directly to and from JSON strings:
286+
287+
```ocaml
288+
type t = A [@@deriving json, json_string]
289+
290+
let "\"A\"" = to_json_string A
291+
let A = of_json_string "\"A\""
292+
```
293+
294+
Similarly, there's `[@@deriving to_json_string]` and `[@@deriving
295+
of_json_string]` to generate the converters separately.
296+
282297
## PPX for OCaml native
283298

284299
A similar PPX is exposed in the `melange-json-native` package, which works with

ppx/browser/ppx_deriving_json_js.ml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,13 @@ module To_json = struct
211211
end
212212

213213
let () =
214-
let _ = Ppx_deriving_tools.register Of_json.deriving in
215-
let _ = Ppx_deriving_tools.register To_json.deriving in
216-
let _ =
214+
let of_json = Ppx_deriving_tools.register Of_json.deriving in
215+
let to_json = Ppx_deriving_tools.register To_json.deriving in
216+
let json =
217217
Ppx_deriving_tools.register_combined "json"
218218
[ To_json.deriving; Of_json.deriving ]
219219
in
220+
let (_ : Deriving.t) = Of_json_string.register ~of_json () in
221+
let (_ : Deriving.t) = To_json_string.register ~to_json () in
222+
let (_ : Deriving.t) = Json_string.register ~json () in
220223
()

ppx/native/ppx_deriving_json_common.ml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
open StdLabels
12
open Ppxlib
3+
open Ast_builder.Default
24
open Ppx_deriving_tools.Conv
35

46
let get_of_variant_case ?mark_as_seen ~variant ~polyvariant = function
@@ -78,3 +80,78 @@ let ld_drop_default ld =
7880
"found [@drop_default] attribute without [@option]"
7981
| Some (), Some () -> `Drop_option
8082
| None, _ -> `No
83+
84+
let expand_via ~what ~through make ~ctxt (rec_flag, tds) =
85+
let loc = Expansion_context.Deriver.derived_item_loc ctxt in
86+
let expand_one (td : type_declaration) =
87+
let loc = td.ptype_loc in
88+
let pat =
89+
let { txt; loc } = td.ptype_name in
90+
let txt = Expansion_helpers.mangle what txt in
91+
ppat_var ~loc { Location.txt; loc }
92+
in
93+
let name_of_td_param idx (ty, _) =
94+
match ty.ptyp_desc with
95+
| Ptyp_any -> Printf.sprintf "_%d" idx
96+
| Ptyp_var name -> name
97+
| _ ->
98+
Location.raise_errorf ~loc:ty.ptyp_loc
99+
"unsupported type parameter"
100+
in
101+
let names = List.mapi td.ptype_params ~f:name_of_td_param in
102+
let expr =
103+
let of_json =
104+
let { txt; loc = _ } = td.ptype_name in
105+
let txt = Expansion_helpers.mangle through txt in
106+
let of_json = pexp_ident ~loc { loc; txt = lident txt } in
107+
pexp_apply ~loc of_json
108+
(List.map names ~f:(fun name -> Nolabel, evar ~loc name))
109+
in
110+
let body = make ~loc of_json in
111+
List.fold_left (List.rev names) ~init:body ~f:(fun e name ->
112+
[%expr fun [%p pvar ~loc name] -> [%e e]])
113+
in
114+
value_binding ~loc ~pat ~expr
115+
in
116+
pstr_value_list ~loc rec_flag (List.map tds ~f:expand_one)
117+
118+
module Of_json_string = struct
119+
let expand =
120+
expand_via ~what:(Expansion_helpers.Suffix "of_json_string")
121+
~through:(Expansion_helpers.Suffix "of_json") (fun ~loc of_json ->
122+
[%expr
123+
fun _json ->
124+
[%e of_json] (Ppx_deriving_json_runtime.of_string _json)])
125+
126+
let register ~of_json () =
127+
Deriving.add "of_json_string"
128+
~str_type_decl:
129+
(Deriving.Generator.V2.make ~deps:[ of_json ] Deriving.Args.empty
130+
expand)
131+
end
132+
133+
module To_json_string = struct
134+
let expand =
135+
expand_via ~what:(Expansion_helpers.Suffix "to_json_string")
136+
~through:(Expansion_helpers.Suffix "to_json") (fun ~loc to_json ->
137+
[%expr
138+
fun _data ->
139+
Ppx_deriving_json_runtime.to_string ([%e to_json] _data)])
140+
141+
let register ~to_json () =
142+
Deriving.add "to_json_string"
143+
~str_type_decl:
144+
(Deriving.Generator.V2.make ~deps:[ to_json ] Deriving.Args.empty
145+
expand)
146+
end
147+
148+
module Json_string = struct
149+
let expand ~ctxt tds =
150+
Of_json_string.expand ~ctxt tds @ To_json_string.expand ~ctxt tds
151+
152+
let register ~json () =
153+
Deriving.add "json_string"
154+
~str_type_decl:
155+
(Deriving.Generator.V2.make ~deps:[ json ] Deriving.Args.empty
156+
expand)
157+
end

ppx/native/ppx_deriving_json_native.ml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,13 @@ module To_json = struct
230230
end
231231

232232
let () =
233-
let _ = Ppx_deriving_tools.register Of_json.deriving in
234-
let _ = Ppx_deriving_tools.register To_json.deriving in
235-
let _ =
233+
let of_json = Ppx_deriving_tools.register Of_json.deriving in
234+
let to_json = Ppx_deriving_tools.register To_json.deriving in
235+
let (json : Deriving.t) =
236236
Ppx_deriving_tools.(
237237
register_combined "json" [ To_json.deriving; Of_json.deriving ])
238238
in
239+
let (_ : Deriving.t) = Of_json_string.register ~of_json () in
240+
let (_ : Deriving.t) = To_json_string.register ~to_json () in
241+
let (_ : Deriving.t) = Json_string.register ~json () in
239242
()

ppx/test/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
(deps
33
(package melange-json)
44
./example.ml
5+
./example_json_string.ml
56
../../.ocamlformat
67
../native/ppx_deriving_json_native_test.exe
78
../browser/ppx_deriving_json_js_test.exe))

ppx/test/example.ml

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,48 +21,46 @@ type allow_extra_fields = {a: int} [@@deriving json] [@@json.allow_extra_fields]
2121
type allow_extra_fields2 = A of {a: int} [@json.allow_extra_fields] [@@deriving json]
2222
type drop_default_option = { a: int; b_opt: int option; [@option] [@json.drop_default] } [@@deriving json]
2323

24-
module Cases = struct
25-
type json = Ppx_deriving_json_runtime.t
26-
type of_json = C : string * (json -> 'a) * ('a -> json) * 'a -> of_json
27-
let of_json_cases = [
28-
C ({|1|}, user_of_json, user_to_json, 1);
29-
C ({|1.1|}, floaty_of_json, floaty_to_json, 1.1);
30-
C ({|1.0|}, floaty_of_json, floaty_to_json, 1.0);
31-
C ({|42|}, floaty_of_json, floaty_to_json, 42.0);
32-
C ({|"OK"|}, (param_of_json string_of_json), (param_to_json string_to_json), "OK");
33-
C ({|"some"|}, opt_of_json, opt_to_json, (Some "some"));
34-
C ({|["Ok", 1]|}, res_of_json, res_to_json, Ok 1);
35-
C ({|["Error", "oops"]|}, res_of_json, res_to_json, Error "oops");
36-
C ({|[42, "works"]|}, tuple_of_json, tuple_to_json, (42, "works"));
37-
C ({|{"name":"N","age":1}|}, record_of_json, record_to_json, {name="N"; age=1});
38-
C ({|["A"]|}, sum_of_json, sum_to_json, (A : sum));
39-
C ({|["B", 42]|}, sum_of_json, sum_to_json, (B 42 : sum));
40-
C ({|["C", {"name": "cname"}]|}, sum_of_json, sum_to_json, (C {name="cname"} : sum));
41-
C ({|["A"]|}, sum_of_json, sum_to_json, (A : sum));
42-
C ({|["S2", 42, "hello"]|}, sum2_of_json, sum2_to_json, (S2 (42, "hello")));
43-
C ({|["B", 42]|}, poly_of_json, poly_to_json, (`B 42 : poly));
44-
C ({|["P2", 42, "hello"]|}, poly2_of_json, poly2_to_json, (`P2 (42, "hello") : poly2));
45-
C ({|["Fix",["Fix",["Fix",["A"]]]]|}, recur_of_json, recur_to_json, (Fix (Fix (Fix A))));
46-
C ({|["Fix",["Fix",["Fix",["A"]]]]|}, polyrecur_of_json, polyrecur_to_json, (`Fix (`Fix (`Fix `A))));
47-
C ({|{"my_name":"N","my_age":1}|}, record_aliased_of_json, record_aliased_to_json, {name="N"; age=1});
48-
C ({|{"my_name":"N"}|}, record_aliased_of_json, record_aliased_to_json, {name="N"; age=100});
49-
C ({|{}|}, record_opt_of_json, record_opt_to_json, {k=None});
50-
C ({|{"k":42}|}, record_opt_of_json, record_opt_to_json, {k=Some 42});
51-
C ({|["A",1]|}, p2_of_json int_of_json string_of_json, p2_to_json int_to_json string_to_json, A 1);
52-
C ({|["B","ok"]|}, p2_of_json int_of_json string_of_json, p2_to_json int_to_json string_to_json, B "ok");
53-
C ({|{"a":1,"b":2}|}, allow_extra_fields_of_json, allow_extra_fields_to_json, {a=1});
54-
C ({|["A",{"a":1,"b":2}]|}, allow_extra_fields2_of_json, allow_extra_fields2_to_json, A {a=1});
55-
C ({|{"a":1}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=None});
56-
C ({|{"a":1,"b_opt":2}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=Some 2});
57-
]
58-
let run' ~json_of_string ~json_to_string (C (data, of_json, to_json, v)) =
59-
print_endline (Printf.sprintf "JSON DATA: %s" data);
60-
let json = json_of_string data in
61-
let v' = of_json json in
62-
assert (v' = v);
63-
let json' = to_json v' in
64-
let data' = json_to_string json' in
65-
print_endline (Printf.sprintf "JSON REPRINT: %s" data')
66-
let run ~json_of_string ~json_to_string () =
67-
List.iter (run' ~json_of_string ~json_to_string) of_json_cases
68-
end
24+
type json = Ppx_deriving_json_runtime.t
25+
type of_json = C : string * (json -> 'a) * ('a -> json) * 'a -> of_json
26+
let of_json_cases = [
27+
C ({|1|}, user_of_json, user_to_json, 1);
28+
C ({|1.1|}, floaty_of_json, floaty_to_json, 1.1);
29+
C ({|1.0|}, floaty_of_json, floaty_to_json, 1.0);
30+
C ({|42|}, floaty_of_json, floaty_to_json, 42.0);
31+
C ({|"OK"|}, (param_of_json string_of_json), (param_to_json string_to_json), "OK");
32+
C ({|"some"|}, opt_of_json, opt_to_json, (Some "some"));
33+
C ({|["Ok", 1]|}, res_of_json, res_to_json, Ok 1);
34+
C ({|["Error", "oops"]|}, res_of_json, res_to_json, Error "oops");
35+
C ({|[42, "works"]|}, tuple_of_json, tuple_to_json, (42, "works"));
36+
C ({|{"name":"N","age":1}|}, record_of_json, record_to_json, {name="N"; age=1});
37+
C ({|["A"]|}, sum_of_json, sum_to_json, (A : sum));
38+
C ({|["B", 42]|}, sum_of_json, sum_to_json, (B 42 : sum));
39+
C ({|["C", {"name": "cname"}]|}, sum_of_json, sum_to_json, (C {name="cname"} : sum));
40+
C ({|["A"]|}, sum_of_json, sum_to_json, (A : sum));
41+
C ({|["S2", 42, "hello"]|}, sum2_of_json, sum2_to_json, (S2 (42, "hello")));
42+
C ({|["B", 42]|}, poly_of_json, poly_to_json, (`B 42 : poly));
43+
C ({|["P2", 42, "hello"]|}, poly2_of_json, poly2_to_json, (`P2 (42, "hello") : poly2));
44+
C ({|["Fix",["Fix",["Fix",["A"]]]]|}, recur_of_json, recur_to_json, (Fix (Fix (Fix A))));
45+
C ({|["Fix",["Fix",["Fix",["A"]]]]|}, polyrecur_of_json, polyrecur_to_json, (`Fix (`Fix (`Fix `A))));
46+
C ({|{"my_name":"N","my_age":1}|}, record_aliased_of_json, record_aliased_to_json, {name="N"; age=1});
47+
C ({|{"my_name":"N"}|}, record_aliased_of_json, record_aliased_to_json, {name="N"; age=100});
48+
C ({|{}|}, record_opt_of_json, record_opt_to_json, {k=None});
49+
C ({|{"k":42}|}, record_opt_of_json, record_opt_to_json, {k=Some 42});
50+
C ({|["A",1]|}, p2_of_json int_of_json string_of_json, p2_to_json int_to_json string_to_json, A 1);
51+
C ({|["B","ok"]|}, p2_of_json int_of_json string_of_json, p2_to_json int_to_json string_to_json, B "ok");
52+
C ({|{"a":1,"b":2}|}, allow_extra_fields_of_json, allow_extra_fields_to_json, {a=1});
53+
C ({|["A",{"a":1,"b":2}]|}, allow_extra_fields2_of_json, allow_extra_fields2_to_json, A {a=1});
54+
C ({|{"a":1}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=None});
55+
C ({|{"a":1,"b_opt":2}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=Some 2});
56+
]
57+
let run' (C (data, of_json, to_json, v)) =
58+
print_endline (Printf.sprintf "JSON DATA: %s" data);
59+
let json = Ppx_deriving_json_runtime.of_string data in
60+
let v' = of_json json in
61+
assert (v' = v);
62+
let json' = to_json v' in
63+
let data' = Ppx_deriving_json_runtime.to_string json' in
64+
print_endline (Printf.sprintf "JSON REPRINT: %s" data')
65+
let test () =
66+
List.iter run' of_json_cases

ppx/test/example_json_string.ml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
open Ppx_deriving_json_runtime.Primitives
2+
3+
let print fmt = Printf.ksprintf print_endline fmt
4+
5+
module To_json_string = struct
6+
type ('a, 'b) t = A of 'a | B of 'b
7+
[@@deriving to_json, to_json_string]
8+
9+
let test () =
10+
let to_json_string = to_json_string int_to_json bool_to_json in
11+
print "** To_json_string **";
12+
print "A 42 -> %s" (to_json_string (A 42));
13+
print "B false -> %s" (to_json_string (B false))
14+
end
15+
16+
module Of_json_string = struct
17+
type ('a, 'b) t = A of 'a | B of 'b
18+
[@@deriving of_json, of_json_string]
19+
20+
let test () =
21+
let of_json_string = of_json_string int_of_json bool_of_json in
22+
print "** Of_json_string **";
23+
print {|["A", 42] = A 42 -> %b|} (of_json_string {|["A", 42]|} = A 42);
24+
print {|["B", false] = B false -> %b|}
25+
(of_json_string {|["B", false]|} = B false)
26+
end
27+
28+
module Json_string = struct
29+
type ('a, 'b) t = A of 'a | B of 'b [@@deriving json, json_string]
30+
31+
let test () =
32+
print "** Json_string **";
33+
let to_json_string = to_json_string int_to_json bool_to_json in
34+
print "A 42 -> %s" (to_json_string (A 42));
35+
print "B false -> %s" (to_json_string (B false));
36+
let of_json_string = of_json_string int_of_json bool_of_json in
37+
print {|["A", 42] = A 42 -> %b|} (of_json_string {|["A", 42]|} = A 42);
38+
print {|["B", false] = B false -> %b|}
39+
(of_json_string {|["B", false]|} = B false)
40+
end
41+
42+
let test () =
43+
To_json_string.test ();
44+
Of_json_string.test ();
45+
Json_string.test ()

ppx/test/ppx_deriving_json_js.e2e.t

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
> (library
77
> (name lib)
88
> (modes melange)
9-
> (modules example main)
9+
> (modules example example_json_string main)
1010
> (flags :standard -w -37-69 -open Ppx_deriving_json_runtime.Primitives)
1111
> (preprocess (pps melange.ppx melange-json.ppx)))
1212
> (melange.emit
@@ -17,15 +17,16 @@
1717
> (module_systems commonjs))' > dune
1818

1919
$ echo '
20-
> open Example
21-
> let () = Cases.run ()
22-
> ~json_to_string:Js.Json.stringify
23-
> ~json_of_string:Js.Json.parseExn
20+
> let () = print_endline "*** json deriver tests ***"
21+
> let () = Example.test ()
22+
> let () = print_endline "*** json_string deriver tests ***"
23+
> let () = Example_json_string.test ()
2424
> ' >> main.ml
2525

2626
$ dune build @js
2727

2828
$ node ./_build/default/output/main.js
29+
*** json deriver tests ***
2930
JSON DATA: 1
3031
JSON REPRINT: 1
3132
JSON DATA: 1.1
@@ -84,3 +85,15 @@
8485
JSON REPRINT: {"a":1}
8586
JSON DATA: {"a":1,"b_opt":2}
8687
JSON REPRINT: {"a":1,"b_opt":2}
88+
*** json_string deriver tests ***
89+
** To_json_string **
90+
A 42 -> ["A",42]
91+
B false -> ["B",false]
92+
** Of_json_string **
93+
["A", 42] = A 42 -> true
94+
["B", false] = B false -> true
95+
** Json_string **
96+
A 42 -> ["A",42]
97+
B false -> ["B",false]
98+
["A", 42] = A 42 -> true
99+
["B", false] = B false -> true

ppx/test/ppx_deriving_json_native.e2e.t

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
> (preprocess (pps melange-json-native.ppx)))' > dune
1010

1111
$ echo '
12-
> open Example
13-
> let () = Cases.run ()
14-
> ~json_to_string:Yojson.Basic.to_string
15-
> ~json_of_string:Yojson.Basic.from_string
12+
> let () = Example.test ()
13+
> let () = print_endline "*** json_string deriver tests ***"
14+
> let () = Example_json_string.test ()
1615
> ' >> main.ml
1716

1817
$ dune build ./main.exe
@@ -76,3 +75,15 @@
7675
JSON REPRINT: {"a":1}
7776
JSON DATA: {"a":1,"b_opt":2}
7877
JSON REPRINT: {"a":1,"b_opt":2}
78+
*** json_string deriver tests ***
79+
** To_json_string **
80+
A 42 -> ["A",42]
81+
B false -> ["B",false]
82+
** Of_json_string **
83+
["A", 42] = A 42 -> true
84+
["B", false] = B false -> true
85+
** Json_string **
86+
A 42 -> ["A",42]
87+
B false -> ["B",false]
88+
["A", 42] = A 42 -> true
89+
["B", false] = B false -> true

0 commit comments

Comments
 (0)