Skip to content

Instantly share code, notes, and snippets.

@fredshonorio
Last active April 13, 2021 08:34
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fredshonorio/76d7372ddb5600671bda84611de81ef9 to your computer and use it in GitHub Desktop.
Save fredshonorio/76d7372ddb5600671bda84611de81ef9 to your computer and use it in GitHub Desktop.
Decoders Vavr Code
package com.example;
import io.vavr.Function3;
import io.vavr.control.Either;
import java.util.function.Function;
public interface Decoder<T> {
Either<String, T> decode(JValue json);
default <U> Decoder<U> map(Function<T, U> f) {
return json -> this.decode(json).map(f);
}
default <U> Decoder<U> andThen(Function<T, Decoder<U>> f) {
return json ->
this.decode(json)
.map(decoded -> f.apply(decoded).decode(json))
.getOrElseGet(Either::left);
}
final static Decoder<String> JStringD_ = new Decoder<String>() {
@Override
public Either<String, String> decode(JValue json) {
return json.asJString()
.map(wrapper -> wrapper.value) // it's unlikely that our users will want the wrapper, so we can return the string directly
.toRight(() -> "expected a string, got: " + json.toString());
}
};
final static Decoder<String> JStringD = json ->
json
.asJString()
.map(wrapper -> wrapper.value)
.toRight(() -> "expected a string, got: " + json.toString());
final static Decoder<JValue.JObject> JObjectD = json ->
json
.asJObject()
.toRight(() -> "expected an object, got: " + json.toString());
public static <T> Decoder<T> field(String key, Decoder<T> valueDec) {
return json -> JObjectD.decode(json)
.flatMap(obj -> obj.get(key)
.toRight("missing field: " + key))
.flatMap(val -> valueDec.decode(val)
.mapLeft(decError -> "field " + key + ": " + decError)
);
}
public static <T> Decoder<T> oneOf(Decoder<T> first, Decoder<T> second) {
return json -> {
Either<String, T> fstRes = first.decode(json);
if (fstRes.isRight())
return fstRes;
Either<String, T> sndRes = second.decode(json);
if (sndRes.isRight())
return sndRes;
// both are left
return Either.left("both decoders failed: (" + fstRes.getLeft() + ") & (" + sndRes.getLeft() + ")");
};
}
public static <A, B, C, T> Decoder<T> map3(Decoder<A> aD, Decoder<B> bD, Decoder<C> cD, Function3<A, B, C, T> f) {
return
aD.andThen(_a ->
bD.andThen(_b ->
cD.map(_c ->
f.apply(_a, _b, _c))));
}
}
"io.vavr:vavr:0.9.2"
"com.fasterxml.jackson.core:jackson-core:2.9.2"
package com.example;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.vavr.control.Either;
import io.vavr.control.Try;
import java.io.IOException;
import java.util.LinkedHashMap;
public class JacksonParser {
private final JsonFactory factory = new JsonFactory();
public Either<String, JValue> parse(String string) {
return Try.of(() -> {
JsonParser p = factory.createParser(string);
JsonToken token;
while ((token = p.nextToken()) != null) {
if (token == JsonToken.START_OBJECT)
return object(p);
else if (token == JsonToken.START_ARRAY)
return array(p);
else if (token.isScalarValue())
return scalar(p);
}
throw new RuntimeException("Nothing parsed");
}).toEither()
.mapLeft(Throwable::getMessage);
}
private static JValue.JObject object(JsonParser p) throws IOException {
LinkedHashMap<String, JValue> map = new LinkedHashMap<>();
String fieldName;
while ((fieldName = p.nextFieldName()) != null) {
JsonToken token = p.nextValue();
if (token.isScalarValue())
map.put(fieldName, scalar(p));
else if (token == JsonToken.START_ARRAY)
map.put(fieldName, array(p));
else if (token == JsonToken.START_OBJECT)
map.put(fieldName, object(p));
}
return new JValue.JObject(io.vavr.collection.LinkedHashMap.ofAll(map));
}
private static JValue scalar(JsonParser p) throws IOException {
JsonToken token = p.getCurrentToken();
if (token == JsonToken.VALUE_STRING)
return new JValue.JString(p.getValueAsString());
else if (token.isNumeric())
return nyi("number parser");
else if (token.isBoolean())
return nyi("boolean parser");
else
return nyi("null parser");
}
private static JValue array(JsonParser p) {
return nyi("array parser");
}
private static JValue nyi(String msg) {
throw new RuntimeException("Not yet implemented: " + msg);
}
}
package com.example;
import io.vavr.collection.Map;
import io.vavr.control.Option;
public abstract class JValue {
private JValue() {} // private so it can't be extended outside of this module
public static class JString extends JValue {
public final String value;
public JString(String value) { this.value = value; }
// omitted toString, hashCode, equals, but they are necessary
}
public static class JObject extends JValue {
public final Map<String, JValue> entries;
public JObject(Map<String, JValue> entries) { this.entries = entries; }
// omitted toString, hashCode, equals, but they are necessary
public Option<JValue> get(String key) {
return entries.get(key); // this is a vavr map, so it returns Option<T>
}
}
public Option<JString> asJString() {
return Option.when(this instanceof JString, () -> (JString) this);
}
public Option<JObject> asJObject() {
return Option.when(this instanceof JObject, () -> (JObject) this);
}
}
package com.example;
import io.vavr.control.Either;
import io.vavr.control.Option;
import java.time.Instant;
import static com.example.Decoder.JStringD;
import static com.example.Decoder.field;
import static com.example.Decoder.map3;
public class Main {
static JacksonParser parser = new JacksonParser();
public static class StreeName {
final String streetName;
public StreeName(String streetName) {
this.streetName = streetName;
}
}
public static class User {
final String firstName;
final String lastName;
final Option<StreeName> streetName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.streetName = Option.none();
}
public User(String firstName, String lastName, StreeName streetName) {
this.firstName = firstName;
this.lastName = lastName;
this.streetName = Option.some(streetName);
}
}
public static void main(String[] args) {
JValue json = parser.parse("\"hello\"").get(); // you would obviously handle the error
JStringD.decode(json); // Right(hello)
Decoder<Integer> JIntegerD = __ -> Either.right(1);
Decoder<Instant> instantD = JIntegerD.map(seconds -> Instant.ofEpochSecond(seconds));
Decoder<Void> v1D = null;
Decoder<Void> v2D = null;
field("version", JStringD)
.andThen(version ->
version.equals("v1") ? v1D :
version.equals("v2") ? v2D :
fail("Unknown version " + version));
Decoder<User> userDecoder2 = field("firstName", JStringD)
.andThen(firstName -> field("secondName", JStringD)
.map(secondName -> new User(firstName, secondName))
);
Decoder<User> userDecoder = map3(
field("first_name", JStringD),
field("last_name", JStringD),
field("address", field("street", JStringD.map(StreeName::new))),
User::new
);
String j = "{\"first_name\":\"John\",\"last_name\":\"Arnold\",\"address\":{\"street\": \"1st Lane\",\"zip_code\":\"3\"}}";
User user = userDecoder.decode(parser.parse(j).get()).get();
}
public static <T> Decoder<T> fail(String s) {
return __ -> Either.left(s);
}
}
@timmattison
Copy link

This is so cool, thanks for sharing it. Is this Apache, MIT, or BSD licensed?

@fredshonorio
Copy link
Author

This code is Apache 2 licensed.
The JacksonParser is a trimmed down version of https://github.com/hamnis/immutable-json/blob/c6b39cb415c729224e0affe26120f68935708ec4/jackson/src/main/java/net/hamnaberg/json/jackson/JacksonStreamingParser.java

json-decoder has a more complete implementation but I haven't bothered to publish to a bintray alternative. immutable-json is probably an all-around better alternative.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment