Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ppx: add [@drop_default] for record fields #17

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens now if we use drop_default in a non option field? Also, would it be worth to test this case? To have it documented "internally".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a test case

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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it option (line 254) or json.option?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s both, in ppxlib you register attributes fully qualified but they can be used both ways.

} [@@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
Loading