Decoders Vavr Code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)))); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"io.vavr:vavr:0.9.2" | |
"com.fasterxml.jackson.core:jackson-core:2.9.2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
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
This is so cool, thanks for sharing it. Is this Apache, MIT, or BSD licensed?