Skip to content

Commit

Permalink
ppx: add [@drop_default] for record fields
Browse files Browse the repository at this point in the history
For now it only works for record fields annotated with `[@option]`, by
dropping the field from JSON repr when the record value is `None`.

What's missing is to also support `[@drop_default]` for fields annotated
by `[@default X]` but we need to decide how to check for equality
between the default value and the field value.

One nice idea I had is to generate code like this:

```ocaml
type t = { a : int [@default 0] [@drop_default] }
...
let bnds =
  match [%equal int] a 0 with
  | true -> bnds
  | false -> ("a", a)::bnds
in
...
```

but this means this ppx will depends on another ppx which provides
`[%equal t]` deriver but sadly `ppx_compare` doesn't work with melange
now.
  • Loading branch information
andreypopp committed Sep 9, 2024
1 parent 3dd1c72 commit 97685ae
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 26 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
([#11](https://github.com/melange-community/melange-json/pull/11))
- Add `melange-json-native` package
([#12](https://github.com/melange-community/melange-json/pull/12))
- Add `[@drop_default]` attribute to drop `None` values from JSON
representation
([#17](https://github.com/melange-community/melange-json/pull/17))

## 1.2.0 (2024-08-16)

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,22 @@ let t = of_json (Json.parseOrRaise {|{"a": 42}|})
(* t = { a = 42; b = None; } *)
```

#### `[@json.drop_default]`: drop default values from JSON

When a field has `[@option]` attribute one can use `[@json.drop_default]`
attribute to make the generated `to_json` function to drop the field if it's
value is `None`:

```ocaml
type t = {
a: int;
b: string option [@json.option] [@json.drop_default];
} [@@deriving to_json]
let t = to_json { a = 1; b = None; }
(* {"a": 1} *)
```

#### `[@json.key "S"]`: customizing keys for record fields

You can specify custom keys for record fields using the `[@json.key E]`
Expand Down
6 changes: 1 addition & 5 deletions ppx/browser/dune
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
(library
(public_name melange-json.ppx)
(name ppx_deriving_json_js)
(modules
:standard
\
ppx_deriving_json_runtime
ppx_deriving_json_js_test)
(modules :standard \ ppx_deriving_json_runtime ppx_deriving_json_js_test)
(libraries ppxlib)
(ppx_runtime_libraries melange-json.ppx-runtime)
(preprocess
Expand Down
19 changes: 15 additions & 4 deletions ppx/browser/ppx_deriving_json_js.ml
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,21 @@ module To_json = struct
let loc = t.rcd_loc in
let fs =
List.map2 t.rcd_fields es ~f:(fun ld x ->
let n = ld.pld_name in
let n = Option.value ~default:n (ld_attr_json_key ld) in
let this = derive ld.pld_type x in
map_loc lident n, this)
let k =
let k = ld.pld_name in
Option.value ~default:k (ld_attr_json_key ld)
in
let v =
let v = derive ld.pld_type x in
match ld_drop_default ld with
| `No -> v
| `Drop_option ->
[%expr
match [%e x] with
| Stdlib.Option.None -> Js.Undefined.empty
| Stdlib.Option.Some _ -> Js.Undefined.return [%e v]]
in
map_loc lident k, v)
in
let record = pexp_record ~loc fs None in
as_json ~loc [%expr [%mel.obj [%e record]]]
Expand Down
16 changes: 16 additions & 0 deletions ppx/native/ppx_deriving_json_common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ let ld_attr_json_default =
Ast_pattern.(single_expr_payload __)
(fun x -> x))

let ld_attr_json_drop_default =
Attribute.get
(Attribute.declare "json.drop_default"
Attribute.Context.label_declaration
Ast_pattern.(pstr nil)
())

let ld_attr_default ld =
match ld_attr_json_default ld with
| Some e -> Some e
Expand All @@ -60,3 +67,12 @@ let ld_attr_default ld =
let loc = ld.pld_loc in
Some [%expr Stdlib.Option.None]
| None -> None)

let ld_drop_default ld =
let loc = ld.pld_loc in
match ld_attr_json_drop_default ld, ld_attr_json_option ld with
| Some (), None ->
Ppx_deriving_tools.error ~loc
"found [@drop_default] attribute without [@option]"
| Some (), Some () -> `Drop_option
| None, _ -> `No
38 changes: 30 additions & 8 deletions ppx/native/ppx_deriving_json_native.ml
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,44 @@ module Of_json = struct
end

module To_json = struct
let gen_exp_pat ~loc prefix =
let n = gen_symbol ~prefix () in
evar ~loc n, pvar ~loc n

let derive_of_tuple derive t es =
let loc = t.tpl_loc in
let es = List.map2 t.tpl_types es ~f:derive in
[%expr `List [%e elist ~loc es]]

let derive_of_record derive t es =
let loc = t.rcd_loc in
let es =
List.map2 t.rcd_fields es ~f:(fun ld x ->
let key =
Option.value ~default:ld.pld_name (ld_attr_json_key ld)
in
[%expr
[%e estring ~loc:key.loc key.txt], [%e derive ld.pld_type x]])
let ebnds, pbnds = gen_exp_pat ~loc "bnds" in
let e =
List.combine t.rcd_fields es
|> List.fold_left ~init:ebnds ~f:(fun acc (ld, x) ->
let key =
Option.value ~default:ld.pld_name (ld_attr_json_key ld)
in
let k = estring ~loc:key.loc key.txt in
let v = derive ld.pld_type x in
let ebnds =
match ld_drop_default ld with
| `No -> [%expr ([%e k], [%e v]) :: [%e ebnds]]
| `Drop_option ->
[%expr
match [%e x] with
| Stdlib.Option.None -> [%e ebnds]
| Stdlib.Option.Some _ ->
([%e k], [%e v]) :: [%e ebnds]]
in
[%expr
let [%p pbnds] = [%e ebnds] in
[%e acc]])
in
[%expr `Assoc [%e elist ~loc es]]
[%expr
`Assoc
(let [%p pbnds] = [] in
[%e e])]

let derive_of_variant_case derive vcs es =
match vcs with
Expand Down
3 changes: 3 additions & 0 deletions ppx/test/example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type epoly = [ `a [@json.as "A_aliased"] | `b ] [@@deriving json]
type ('a, 'b) p2 = A of 'a | B of 'b [@@deriving json]
type allow_extra_fields = {a: int} [@@deriving json] [@@json.allow_extra_fields]
type allow_extra_fields2 = A of {a: int} [@json.allow_extra_fields] [@@deriving json]
type drop_default_option = { a: int; b_opt: int option; [@option] [@json.drop_default] } [@@deriving json]

module Cases = struct
type json = Ppx_deriving_json_runtime.t
Expand Down Expand Up @@ -47,6 +48,8 @@ module Cases = struct
C ({|["B","ok"]|}, p2_of_json int_of_json string_of_json, p2_to_json int_to_json string_to_json, B "ok");
C ({|{"a":1,"b":2}|}, allow_extra_fields_of_json, allow_extra_fields_to_json, {a=1});
C ({|["A",{"a":1,"b":2}]|}, allow_extra_fields2_of_json, allow_extra_fields2_to_json, A {a=1});
C ({|{"a":1}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=None});
C ({|{"a":1,"b_opt":2}|}, drop_default_option_of_json, drop_default_option_to_json, {a=1; b_opt=Some 2});
]
let run' ~json_of_string ~json_to_string (C (data, of_json, to_json, v)) =
print_endline (Printf.sprintf "JSON DATA: %s" data);
Expand Down
4 changes: 4 additions & 0 deletions ppx/test/ppx_deriving_json_js.e2e.t
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@
JSON REPRINT: {"a":1}
JSON DATA: ["A",{"a":1,"b":2}]
JSON REPRINT: ["A",{"a":1}]
JSON DATA: {"a":1}
JSON REPRINT: {"a":1}
JSON DATA: {"a":1,"b_opt":2}
JSON REPRINT: {"a":1,"b_opt":2}
71 changes: 71 additions & 0 deletions ppx/test/ppx_deriving_json_js.t
Original file line number Diff line number Diff line change
Expand Up @@ -915,3 +915,74 @@
let _ = allow_extra_fields2_to_json
end [@@ocaml.doc "@inline"] [@@merlin.hide]
$ cat <<"EOF" | run
> type drop_default_option = { a: int; b_opt: int option; [@option] [@json.drop_default] } [@@deriving json]
> EOF
type drop_default_option = {
a : int;
b_opt : int option; [@option] [@json.drop_default]
}
[@@deriving json]
include struct
let _ = fun (_ : drop_default_option) -> ()
[@@@ocaml.warning "-39-11-27"]
let rec drop_default_option_of_json =
(fun x ->
if
Stdlib.not
(Stdlib.( && )
(Stdlib.( = ) (Js.typeof x) "object")
(Stdlib.( && )
(Stdlib.not (Js.Array.isArray x))
(Stdlib.not
(Stdlib.( == ) (Obj.magic x : 'a Js.null) Js.null))))
then
Ppx_deriving_json_runtime.of_json_error "expected a JSON object";
let fs =
(Obj.magic x
: < a : Js.Json.t Js.undefined
; b_opt : Js.Json.t Js.undefined >
Js.t)
in
{
a =
(match Js.Undefined.toOption fs##a with
| Stdlib.Option.Some v -> int_of_json v
| Stdlib.Option.None ->
Ppx_deriving_json_runtime.of_json_error
"missing field \"a\"");
b_opt =
(match Js.Undefined.toOption fs##b_opt with
| Stdlib.Option.Some v -> (option_of_json int_of_json) v
| Stdlib.Option.None -> Stdlib.Option.None);
}
: Js.Json.t -> drop_default_option)

let _ = drop_default_option_of_json

[@@@ocaml.warning "-39-11-27"]

let rec drop_default_option_to_json =
(fun x ->
match x with
| { a = x_a; b_opt = x_b_opt } ->
(Obj.magic
[%mel.obj
{
a = int_to_json x_a;
b_opt =
(match x_b_opt with
| Stdlib.Option.None -> Js.Undefined.empty
| Stdlib.Option.Some _ ->
Js.Undefined.return
((option_to_json int_to_json) x_b_opt));
}]
: Js.Json.t)
: drop_default_option -> Js.Json.t)

let _ = drop_default_option_to_json
end [@@ocaml.doc "@inline"] [@@merlin.hide]

6 changes: 6 additions & 0 deletions ppx/test/ppx_deriving_json_js_errors.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@


$ echo 'type t = { a: int option; [@drop_default] } [@@deriving json]' | ../browser/ppx_deriving_json_js_test.exe -impl -
Fatal error: exception Ppx_deriving_json_js__Ppx_deriving_tools.Error(_, "found [@drop_default] attribute without [@option]")
[2]

4 changes: 4 additions & 0 deletions ppx/test/ppx_deriving_json_native.e2e.t
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@
JSON REPRINT: {"a":1}
JSON DATA: ["A",{"a":1,"b":2}]
JSON REPRINT: ["A",{"a":1}]
JSON DATA: {"a":1}
JSON REPRINT: {"a":1}
JSON DATA: {"a":1,"b_opt":2}
JSON REPRINT: {"a":1,"b_opt":2}
Loading

0 comments on commit 97685ae

Please sign in to comment.