Skip to content

Commit

Permalink
Merge pull request #12 from melange-community/add-native-pkg
Browse files Browse the repository at this point in the history
Add `melange-json-native` pkg
  • Loading branch information
jchavarri authored Aug 28, 2024
2 parents c9e9b4e + 8aa9560 commit 8a7a46f
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- PPX: Qualify usages of infix operators with `Stdlib`
([#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))

## 1.2.0 (2024-08-16)

Expand Down
115 changes: 85 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ Compositional JSON encode/decode library and PPX for

Based on [@glennsl/bs-json](https://github.com/glennsl/bs-json).

The Decode module in particular provides a basic set of decoder functions to be composed into more complex decoders. A
decoder is a function that takes a `Js.Json.t` and either returns a value of the desired type if successful or raises a
`DecodeError` exception if not. Other functions accept a decoder and produce another decoder. Like `array`, which when
given a decoder for type `t` will return a decoder that tries to produce a value of type `t array`. So to decode an
`int array` you combine `Json.Decode.int` with `Json.Decode.array` into `Json.Decode.(array int)`. An array of arrays of
ints? `Json.Decode.(array (array int))`. Dict containing arrays of ints? `Json.Decode.(dict (array int))`.
The Decode module in particular provides a basic set of decoder functions to be
composed into more complex decoders. A decoder is a function that takes a
`Js.Json.t` and either returns a value of the desired type if successful or
raises a `DecodeError` exception if not. Other functions accept a decoder and
produce another decoder. Like `array`, which when given a decoder for type `t`
will return a decoder that tries to produce a value of type `t array`. So to
decode an `int array` you combine `Json.Decode.int` with `Json.Decode.array`
into `Json.Decode.(array int)`. An array of arrays of ints? `Json.Decode.(array
(array int))`. Dict containing arrays of ints? `Json.Decode.(dict (array int))`.

## Example

Expand Down Expand Up @@ -49,10 +52,11 @@ let line = data |> Json.parseOrRaise
|> Decode.line;
```

NOTE: `Json.Decode.{ ... }` creates an ordinary record, but also opens the `Json.Decode` module locally, within the
scope delimited by the curly braces, so we don't have to qualify the functions we use from it, like `field`, `int` and
`optional` here. You can also use `Json.Decode.( ... )` to open the module locally within the parentheses, if you're not
creating a record.
NOTE: `Json.Decode.{ ... }` creates an ordinary record, but also opens the
`Json.Decode` module locally, within the scope delimited by the curly braces, so
we don't have to qualify the functions we use from it, like `field`, `int` and
`optional` here. You can also use `Json.Decode.( ... )` to open the module
locally within the parentheses, if you're not creating a record.

See [examples](./examples/) for more.

Expand Down Expand Up @@ -88,14 +92,17 @@ For the moment, please see the interface files:

### Writing custom decoders and encoders

If you look at the type signature of `Json.Decode.array`, for example, you'll see it takes an `'a decoder` and returns an
`'a array decoder`. `'a decoder` is just an alias for `Js.Json.t -> 'a`, so if we expand the type signature of `array`
we'll get `(Js.Json.t -> 'a) -> Js.Json.t -> 'a array`. We can now see that it is a function that takes a decoder and
returns a function, itself a decoder. Applying the `int` decoder to `array` will give us an `int array decoder`, a
function `Js.Json.t -> int array`.
If you look at the type signature of `Json.Decode.array`, for example, you'll
see it takes an `'a decoder` and returns an `'a array decoder`. `'a decoder` is
just an alias for `Js.Json.t -> 'a`, so if we expand the type signature of
`array` we'll get `(Js.Json.t -> 'a) -> Js.Json.t -> 'a array`. We can now see
that it is a function that takes a decoder and returns a function, itself a
decoder. Applying the `int` decoder to `array` will give us an `int array
decoder`, a function `Js.Json.t -> int array`.

If you've written a function that takes just `Js.Json.t` and returns user-defined types of your own, you've already been
writing composable decoders! Let's look at `Decode.point` from the example above:
If you've written a function that takes just `Js.Json.t` and returns
user-defined types of your own, you've already been writing composable decoders!
Let's look at `Decode.point` from the example above:

```reason
let point = json => {
Expand All @@ -107,14 +114,16 @@ let point = json => {
};
```

This is a function `Js.Json.t -> point`, or a `point decoder`. So if we'd like to decode an array of points, we can just
pass it to `Json.Decode.array` to get a `point array decoder` in return.
This is a function `Js.Json.t -> point`, or a `point decoder`. So if we'd like
to decode an array of points, we can just pass it to `Json.Decode.array` to get
a `point array decoder` in return.

#### Builders

To write a decoder _builder_ like `Json.Decode.array` we need to take another decoder as an argument, and thanks to
currying we just need to apply it where we'd otherwise use a fixed decoder. Say we want to be able to decode both
`int point`s and `float point`s. First we'd have to parameterize the type:
To write a decoder _builder_ like `Json.Decode.array` we need to take another
decoder as an argument, and thanks to currying we just need to apply it where
we'd otherwise use a fixed decoder. Say we want to be able to decode both `int
point`s and `float point`s. First we'd have to parameterize the type:

```reason
type point('a) = {
Expand All @@ -123,7 +132,8 @@ type point('a) = {
}
```

Then we can change our `point` function from above to take and use a decoder argument:
Then we can change our `point` function from above to take and use a decoder
argument:

```reason
let point = (decodeNumber, json) => {
Expand All @@ -144,14 +154,15 @@ let floatPoint = point(Json.Decode.float);

#### Encoders

Encoders work exactly the same way, just in reverse. `'a encoder` is just an alias for `'a -> Js.Json.t`, and this also
transfers to composition: `'a encoder -> 'a array encoder` expands to `('a -> Js.Json.t) -> 'a array -> Js.Json.t`.
Encoders work exactly the same way, just in reverse. `'a encoder` is just an
alias for `'a -> Js.Json.t`, and this also transfers to composition: `'a encoder
-> 'a array encoder` expands to `('a -> Js.Json.t) -> 'a array -> Js.Json.t`.

## PPX
## PPX for Melange

A [ppx deriver
plugin](https://ocaml.org/docs/metaprogramming#attributes-and-derivers) is
provided to automatically convert OCaml values to and from JSON.
provided to automatically convert Melange values to and from JSON.

### Installation

Expand Down Expand Up @@ -265,11 +276,55 @@ let json = to_json B
(* "bbb" *)
```

## PPX for OCaml native

A similar PPX is exposed in the `melange-json-native` package, which works with
the `yojson` JSON representation instead of `Js.Json.t`.

### Installation

The PPX is included in `melange-json-native` package, so that package will have
to be installed first:

```sh
opam install melange-json-native
```

To use it, add the `dune` configuration to your project:

```dune
(executable
...
(preprocess (pps melange-json-native.ppx)))
```

### Usage

From the usage perspective, the PPX is similar to the Melange one:

```ocaml
type t = {
a: int;
b: string;
} [@@deriving json]
```

This will generate the following pair of functions:

```ocaml
val of_json : Yojson.Basic.json -> t
val to_json : t -> Yojson.Basic.json
```

Refer to the [PPX for Melange](#ppx-for-melange) section for more details on
usage patterns.

## License

This work is dual-licensed under LGPL 3.0 and MPL 2.0.
You can choose between one of them if you use this work.
This work is dual-licensed under LGPL 3.0 and MPL 2.0. You can choose between
one of them if you use this work.

Please see LICENSE.LGPL-3.0 and LICENSE.MPL-2.0 for the full text of each license.
Please see LICENSE.LGPL-3.0 and LICENSE.MPL-2.0 for the full text of each
license.

`SPDX-License-Identifier: LGPL-3.0 OR MPL-2.0`
18 changes: 13 additions & 5 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@

(package
(name melange-json)
(synopsis
"Compositional JSON encode/decode library and PPX for Melange, with native compatibility")
(synopsis "Compositional JSON encode/decode library and PPX for Melange")
(description
"Provides encoders and decoders to convert JSON values into typed values. With the possibility to create custom encoders and decoders and automate them with a PPX.")
"Provides tools for converting JSON to typed OCaml values in Melange. It includes custom encoders, decoders, and a PPX for automating these conversions.")
(depends
ocaml
(melange
Expand All @@ -37,8 +36,17 @@
(>= "3.10.0")
:with-test))
ppxlib
(yojson
(>= "1.6.0")) ; only used for the native version
(opam-check-npm-deps :with-test) ; todo: use with-dev-setup once opam 2.2 is out
(ocaml-lsp-server :with-test)
(ocamlformat :with-test)))

(package
(name melange-json-native)
(synopsis "Compositional JSON encode/decode PPX for OCaml")
(description
"A PPX for OCaml that automates encoding and decoding JSON into typed values. It supports custom encoders and decoders, and integrates with Yojson")
(depends
ocaml
ppxlib
(yojson
(>= "1.6.0"))))
35 changes: 35 additions & 0 deletions melange-json-native.opam
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "Compositional JSON encode/decode PPX for OCaml"
description:
"A PPX for OCaml that automates encoding and decoding JSON into typed values. It supports custom encoders and decoders, and integrates with Yojson"
maintainer: [
"Antonio Nuno Monteiro <[email protected]>"
"Javier Chávarri <[email protected]>"
]
authors: ["glennsl" "Andrey Popp"]
license: ["LGPL-3.0-only" "MPL-2.0"]
homepage: "https://github.com/melange-community/melange-json/"
bug-reports: "https://github.com/melange-community/melange-json/issues"
depends: [
"dune" {>= "3.9"}
"ocaml"
"ppxlib"
"yojson" {>= "1.6.0"}
"odoc" {with-doc}
]
build: [
["dune" "subst"] {dev}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://github.com/melange-community/melange-json.git"
6 changes: 2 additions & 4 deletions melange-json.opam
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis:
"Compositional JSON encode/decode library and PPX for Melange, with native compatibility"
synopsis: "Compositional JSON encode/decode library and PPX for Melange"
description:
"Provides encoders and decoders to convert JSON values into typed values. With the possibility to create custom encoders and decoders and automate them with a PPX."
"Provides tools for converting JSON to typed OCaml values in Melange. It includes custom encoders, decoders, and a PPX for automating these conversions."
maintainer: [
"Antonio Nuno Monteiro <[email protected]>"
"Javier Chávarri <[email protected]>"
Expand All @@ -19,7 +18,6 @@ depends: [
"melange-jest" {with-test}
"reason" {>= "3.10.0" & with-test}
"ppxlib"
"yojson" {>= "1.6.0"}
"opam-check-npm-deps" {with-test}
"ocaml-lsp-server" {with-test}
"ocamlformat" {with-test}
Expand Down
8 changes: 4 additions & 4 deletions ppx/native/dune
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
(library
(public_name melange-json.ppx-native)
(public_name melange-json-native.ppx)
(name ppx_deriving_json_native)
(modules
:standard
\
ppx_deriving_json_runtime
ppx_deriving_json_native_test)
(libraries ppxlib)
(ppx_runtime_libraries melange-json.ppx-runtime-native)
(ppx_runtime_libraries melange-json-native.ppx-runtime)
(preprocess
(pps ppxlib.metaquot))
(kind ppx_deriver))

(library
(public_name melange-json.ppx-runtime-native)
(public_name melange-json-native.ppx-runtime)
(name ppx_deriving_json_native_runtime)
(wrapped false)
(modules ppx_deriving_json_runtime)
Expand All @@ -22,7 +22,7 @@
(executable
(name ppx_deriving_json_native_test)
(modules ppx_deriving_json_native_test)
(libraries melange-json.ppx-native ppxlib))
(libraries melange-json-native.ppx ppxlib))

(rule
(target ppx_deriving_json_native.mli)
Expand Down
2 changes: 1 addition & 1 deletion ppx/test/ppx_deriving_json_native.e2e.t
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> (name main)
> (libraries yojson)
> (flags :standard -w -37-69 -open Ppx_deriving_json_runtime.Primitives)
> (preprocess (pps melange-json.ppx-native)))' > dune
> (preprocess (pps melange-json-native.ppx)))' > dune

$ echo '
> open Example
Expand Down

0 comments on commit 8a7a46f

Please sign in to comment.