- Types and classes
- Parsing
- Encoding
- Transforming
- Inserting
- Removing
- Updating
- Retrieving
- Merging
- Creating
- Casting
- Reducing
- Affixing
- Traversing (not supported yet)
- Converting
- JSON to Object conversion
- Object to JSON conversion
- Automatic conversion (not supported)
- Error handling
- Type representing a correct json value.
- Type representing a json currently undergoing transformation
- Supports the primary manipulative functions
- Not a fixed JSON, transformation has to be terminated to get a
Json
out of it (see here)
- Left-biased disjunctive type used for representing successful and erroneous computations
- The left-hand side is seen as the error type, whilst the right-hand side as the success type
- An interface describing the safe conversion from an input type
I
to an output typeO
that may fail with an error of typeE
- Equivalent to
Function<I, Either<E, O>>
- Concrete extension of
Convert
for readingJson
- Equivalent to
Convert<String, Json, A>
- Concrete extension of
Convert
from writingJson
- Equivalent to
Convert<String, A, Json>
- A helper class to aid in writing
Read<A>
instances
- A helper class to aid in writing
Write<A>
instances
Generally, parsing reads directly into a JsonT
.
import static com.ravram.nemesis.Json.*;
parse("{ \"hello\" : \"world\" }") // JsonT
You can however also parse directly to an arbitrary type A
, given an instance of Read<A>
for that type.
For more information on Read
, take a look here.
final String myJson = ...;
final Read<MyType> myTypeReader = ...;
parseAs(myTypeReader, myJson);
Any Json
can be encoded to a proper Json string by calling encode
.
Note: Calling toString
on these objects will NOT write to a valid json.
Json myJson = ...;
myJson.encode();
Assume we start with the following:
import static com.ravram.nemesis.Json.*;
import static com.ravram.nemesis.JsonT;
JsonT jsonT = parse("{ \"hello\" : \"world\" }")
The simplest insertion is of either Json
or JsonT
values.
JsonT jsonT2 = parse("{ \"a\" : 1 }");
jsonT.insertJson(jsonT2, "node");
{
"hello": "world",
"node": {
"a": 1
}
}
Any raw values can be directly inserted into a JsonT
, as long as they are part of the JSON specification.
jsonT
.insertValue(1, "field-1")
.insertValue(true, "field-2")
.insertValue("bla", "field-3")
.insertValue(null, "field-4")
.insertValue(Arrays.asList(1, 2, 3, 4)), "field-5")
.insertValue(new int[]{1, 2, 3, 4}, "field-6")
.insertValue(new HashSet(Arrays.asList(1, 2, 3, 4)), "field-7")
.insertValue(Map.of("a", "b"), "field-8");
{
"hello": "world",
"field-1": 1,
"field-2": true,
"field-3": "bla",
"field-4": null,
"field-5": [
1,
2,
3,
4
],
"field-6": [
1,
2,
3,
4
],
"field-7": [
1,
2,
3,
4
],
"field-8": {
"a": "b"
}
For arbitrary types, you'll have to provide a Write<A>
instance.
For more information on Write
, take a look here.
import static com.ravram.nemesis.Json.*;
import static com.ravram.nemesis.Write;
static class Point {
final int a;
final int b;
public Point(final int a, final int b) {
this.a = a;
this.b = b;
}
}
Write<Point> writePoint = point ->
write(point).using(
"a", p -> Write.INT.apply(p.a),
"b", p -> Write.INT.apply(p.b));
jsonT.insertValue(new Point(1,1), writePoint, "point")
{
"hello": "world",
"point": {
"a": 1,
"b": 2
}
}
Values can be inserted with arbitrary nestedness. Structure will automatically be created wherever none exists.
Note: This only works on JSON objects.
jsonT.insertValue(true, "is", "this", "deep", "enough")
{
"hello": "world",
"is": {
"this": {
"deep": {
"enough": true
}
}
}
}
Entries can only be deleted at the top level. (Support for removal at arbitrary nestedness is on the way)
Note: This only works on JSON objects.
jsonT
.insertValue(1, "value")
.remove("hello", "value");
{}
Updates are supported for any value at any degree of nestedness.
Can either be performed directly on a JsonT
node or on a proper type A
, provided a Read<A>
instance.
For more information on Read
, take a look here.
import com.ravram.nemesis.Read;
jsonT
.insertValue(1, "one", "level")
.updateValue(Read.INT, n -> n + 1, "one", "level");
{
"hello": "world",
"one": {
"level": 2
}
}
JSON can be retrieved from any level and any structure.
Note: This returns a JsonT
to leverage safety and further composition.
jsonT
.insertValue(Arrays.asList(1, 2, 3), "one", "level")
.getJson("one", "level", 2);
Values of any type A
can be retrieved from any level and any structure, provided a Read<A>
for that type.
For more information on Read
, take a look here.
import com.ravram.nemesis.Read;
jsonT
.insertValue(true, "one", "level")
.getValue(Read.BOOLEAN, "one", "level", 2);
Two JsonT
s can be merged together if they're both either JSON objects or arrays respectively.
JsonT jsonObj1 = parse("{ \"oh\" : \"my\" }");
JsonT jsonObj2 = parse ("{ \"well\" : \"hi\" }")
JsonT jsonArr1 = parse("[1, 2, 3, 4]");
JsonT jsonArr2 = parse("[5, 6, 7, 8]");
jsonObj1.mergeJson(jsonObj2);
jsonArr1.mergeJson(jsonArr2);
{
"oh": "my",
"well": "hi"
}
[1, 2, 3, 4, 5, 6, 7, 8]
A JsonT
can be materialised to a concrete type A
provided a Read<A>
instance for that type.
For more information on Read
, take a look here.
There are a bunch of default Read<A>
and Write<A>
instances for many types.
These can be found statically in the respective Read<A>
and Write<A>
classes.
A list of all the defaults can be found here.
import com.ravram.nemesis.Read;
jsonT.getJson("hello").as(Read.STRING); // Either<String, String>
or
jsonT.getValue(Read.STRING, "hello"); // Either<String, String>
Like previously mentioned, any JsonT
can be coerced to an arbitrary type A
given
that one constructs a Read<A>
instance for that type.
For more information on Read
, take a look here.
import com.ravram.nemesis.Read;
static class Greeting {
public final String value;
public Greeting(final String value) {
this.value = value;
}
}
Convert<Json, Greeting> converter = json ->
convert(json).using(
json -> json.transform().getValue(Read.STRING, "hello"),
value -> new Greeting(value));
Any JsonT
can be reduced shallowly for both JSON objects and JSON arrays.
Object reduction occurs at the upper-most level of entries and doesn't recursively traverse down the structure.
For tree-wise traversal, please take a look here.
Because the reduction may very well need to coerce the inner JsonT
to some concrete type, the reduction function
is required to enforce a return of Either<String, A>
.
Signature:
Function3<A, String, JsonT, Either<String, A>>
import com.ravram.nemesis.Write;
jsonT.reduceObj("Greeting:",(inter,key,jsonValue)->{
return jsonValue.as(Write.STRING).map(value->inter+" "+key+" "+value);
});
"Greeting: hello world"
Array reduction only occurs at the sequence level and doesn't recursively traverse down the structure.
For tree-wise traversal, please take a look here.
The reducing function receives the intermediate result, the current element's index and the element's value as JsonT
.
Because the reduction may very well need to coerce the inner JsonT
to some concrete type, the reduction function
is required to enforce a return of Either<String, A>
.
Signature:
Function3<A, Integer, JsonT, Either<String, A>>
import com.ravram.nemesis.Read;
JsonT json = parse("[{\"value\" : 1}, {\"value\" : 2}, {\"value\" : 3}, {\"value\" : 4}]");
json.reduceArr(0, (inter, index, jsonValue) -> {
return jsonValue.getAs(Read.INT, "value").map(x -> x + inter);
});
Json's can be manually created from an empty node.
empty.transform().insertValue("hello", "world");
{ "hello" : "world" }
To materialise a JsonT
to a concrete Json
type, it's transformation has to be terminated.
This yields an Either<String, Json>
indicating either a failed (String
) successful (Json
) transformation.
Yes, the error is a String
. Don't worry, it contains more information that you may expect.
jsonT.affix()
NOT YET SUPPORTED
Conversion from JSON to Java types and vice-versa is modeled abstractly via the functional interface Convert<E, I, O>
.
It represents the conversion of some type A
into a type B
which may fail with an error E
.
Two interfaces are derived from it that specifically model reading and writing respectively:
-
Read<A>
(Convert<String, Json, A>
)- Reads types
A
fromJson
- Reads types
-
Write<A>
(Convert<String, A, Json>
)- Writes types
A
toJson
- Writes types
Converting Json
to an arbitrary type A
and vice versa is thusdone by defining Read
and Write
instances for that type and using them wherever needed.
nemesis provides Read
and Write
instances for a number of basic types.
These can be found statically in Read
and Write
respectively.
Here's a list:
Integer
Long
Float
Double
String
Boolean
Null
UUID
ZonedDateTime
List
(bothJson
and arbitrary types)Set
(bothJson
and arbitrary types)Map
(bothJson
and arbitrary types)
When it comes to creating your Read
and Write
instances, nemesis offers some help.
There's a read
function which works like this:
Read<MyType> reader = json ->
read(json)
.using (
// the first n - 1 arguments are individual `Read` instances
// with which you extract and convert individual attributes of your type
.., // Read<A>
.., // Read<B>
.., // Read<C>
..,
// the last argument is a function containing all converted attribues
// which you use to intantiate your type
(a, b, c) -> new MyType(a, b, c))
Example:
import com.ravram.nemesis.Read;
static class Coord {
public final int s;
public final int e;
public Coord(final int s, final int e) {
this.s = s;
this.e = e;
}
}
static class Line {
public final Coord x;
public final Coord y;
public Line(final Coord x, final Coord y) {
this.x = x;
this.y = y;
}
}
static class Figure {
public final List<Line> lines;
public Figure(final List<Line> lines) {
this.lines = lines;
}
}
Read<Coord> coord = json ->
read(json).using(
js -> js.transform().getValue(Read.INT, "s"),
js -> js.transform().getValue(Read.INT, "e"),
(s, e) -> new Coord(s, e));
Read<Line> line = json ->
read(json).using(
js -> js.transform().getValue(coord, "x"),
js -> js.transform().getValue(coord, "y"),
(x, y) -> new Line(x, y));
Read<Figure> figure = json ->
read(json).using(
js -> js.transform().getValue(readList(line), ),
lines -> new Figure(lines));
There's a write
function which works like this:
Write<MyType> writer = json ->
write(json)
.using (
// arguments are pairs of `String` and `Convert<MyType, Json>`
// `String` indicates the key an attribute of `MyType` should have in the json
// `Convert<MyType, Json>` extracts that attribute and transforms it to a json
.., .. // String, Write<MyType>
.., .. // String, Write<MyType>
.., .. // String, Write<MyType>)
Example:
import com.ravram.nemesis.Write;
static class Coord {
public final int s;
public final int e;
public Coord(final int s, final int e) {
this.s = s;
this.e = e;
}
}
static class Line {
public final Coord x;
public final Coord y;
public Line(final Coord x, final Coord y) {
this.x = x;
this.y = y;
}
}
static class Figure {
public final List<Line> lines;
public Figure(final List<Line> lines) {
this.lines = lines;
}
}
Write<Coord> jcoord = coord ->
write(coord).using(
"s", c -> Write.INT.convert(c.s),
"e", c -> Write.INT.convert(c.e));
Write<Line> jline = line ->
write(line).using(
"x", l -> jcoord.convert(l.x),
"y", l -> jcoord.convert(l.y));
Write<Figure> jfigure = figure ->
write(figure).using(
"lines", f -> writeList(jline).convert(f.lines));
I just can't be bothered.
nemesis luckily doesn't throw exceptions, but rather follows a more data-oriented approach to error handling.
Materialisations of a JsonT
always yield an Either
, where the error type is set to String
. (see here)
The Either
data structure is a left-biased version of the typical one you may find in the wild.
Either is a simple data type or interface, that has two implementations (often called Left
and Right
).
The idea behind it is that, whilst the interface itself represents two things at the same time,
a concrete instance of it can either only be one or the other (Left
or Right
), but not both at the same time.
It thus represents a disjuction and is fairly handy for abstractly describing two things at the same time, that at runtime break down to just one. For example, explicitly denoting that a function may fail or succeed upon execution.
interface Either<E, V> {}
public class Right<E, V> implements Either<E, V> {
public final V value;
public Right(final V value) {
this.value = value;
}
}
public class Left<E, V> implements Either<E, V> {
public final E error;
public final Left(final E error) {
this.error = error;
}
}