From b24fac6810c1a710d3d8874258a093581317a154 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Mon, 12 Aug 2024 21:46:38 +0200 Subject: [PATCH 01/88] Add ClassCodec with parameterized field support --- .../java/com/fauna/annotation/FaunaField.java | 2 + .../com/fauna/annotation/FaunaFieldImpl.java | 8 +- src/main/java/com/fauna/codec/ClassCodec.java | 32 --- src/main/java/com/fauna/codec/Codec.java | 7 +- .../java/com/fauna/codec/CodecProvider.java | 4 +- .../java/com/fauna/codec/CodecRegistry.java | 3 +- .../com/fauna/codec/CodecRegistryKey.java | 33 ++- .../com/fauna/codec/DefaultCodecProvider.java | 53 ++++- .../com/fauna/codec/DefaultCodecRegistry.java | 11 +- .../com/fauna/codec/codecs/BaseCodec.java | 24 ++ .../com/fauna/codec/codecs/ClassCodec.java | 209 +++++++++++++++++ .../com/fauna/codec/codecs/DynamicCodec.java | 218 ++++++++++++++++++ .../java/com/fauna/codec/codecs/IntCodec.java | 39 ++++ .../com/fauna/codec/codecs/ListCodec.java | 56 +++++ .../java/com/fauna/codec/codecs/MapCodec.java | 76 ++++++ .../com/fauna/codec/codecs/PageCodec.java | 84 +++++++ .../com/fauna/codec/codecs/StringCodec.java | 38 +++ .../java/com/fauna/mapping/FieldInfo.java | 26 ++- .../java/com/fauna/response/QuerySuccess.java | 1 - .../fauna/serialization/BaseDeserializer.java | 4 - .../fauna/serialization/UTF8FaunaParser.java | 15 +- .../beans/ClassWithParameterizedFields.java | 20 ++ .../com/fauna/codec/CodecRegistryKeyTest.java | 42 ++++ .../com/fauna/codec/CodecRegistryTest.java | 21 ++ .../fauna/codec/DefaultCodecProviderTest.java | 28 +++ src/test/java/com/fauna/codec/Helpers.java | 20 ++ .../fauna/codec/codecs/ClassCodecTest.java | 24 ++ .../com/fauna/codec/codecs/IntCodecTest.java | 23 ++ .../com/fauna/codec/codecs/ListCodecTest.java | 26 +++ .../com/fauna/codec/codecs/MapCodecTest.java | 27 +++ .../fauna/codec/codecs/StringCodecTest.java | 24 ++ .../serialization/UTF8FaunaParserTest.java | 11 +- 32 files changed, 1128 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/com/fauna/codec/ClassCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/BaseCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/ClassCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/DynamicCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/IntCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/ListCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/MapCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/PageCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/StringCodec.java create mode 100644 src/test/java/com/fauna/beans/ClassWithParameterizedFields.java create mode 100644 src/test/java/com/fauna/codec/CodecRegistryKeyTest.java create mode 100644 src/test/java/com/fauna/codec/CodecRegistryTest.java create mode 100644 src/test/java/com/fauna/codec/DefaultCodecProviderTest.java create mode 100644 src/test/java/com/fauna/codec/Helpers.java create mode 100644 src/test/java/com/fauna/codec/codecs/ClassCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/IntCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/ListCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/MapCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/StringCodecTest.java diff --git a/src/main/java/com/fauna/annotation/FaunaField.java b/src/main/java/com/fauna/annotation/FaunaField.java index 4658d663..346dde95 100644 --- a/src/main/java/com/fauna/annotation/FaunaField.java +++ b/src/main/java/com/fauna/annotation/FaunaField.java @@ -14,5 +14,7 @@ String name() default ""; + Class typeArgument() default void.class; + boolean nullable() default false; } \ No newline at end of file diff --git a/src/main/java/com/fauna/annotation/FaunaFieldImpl.java b/src/main/java/com/fauna/annotation/FaunaFieldImpl.java index 17aca6e0..9e1ea69b 100644 --- a/src/main/java/com/fauna/annotation/FaunaFieldImpl.java +++ b/src/main/java/com/fauna/annotation/FaunaFieldImpl.java @@ -1,6 +1,5 @@ package com.fauna.annotation; -import com.fauna.enums.FaunaType; import java.lang.reflect.Field; public class FaunaFieldImpl implements FaunaField { @@ -19,9 +18,14 @@ public String name() { : field.getName(); } + @Override + public Class typeArgument() { + return annotation != null ? annotation.typeArgument() : null; + } + @Override public boolean nullable() { - return annotation != null ? annotation.nullable() : false; + return annotation != null && annotation.nullable(); } @Override diff --git a/src/main/java/com/fauna/codec/ClassCodec.java b/src/main/java/com/fauna/codec/ClassCodec.java deleted file mode 100644 index 40dca35c..00000000 --- a/src/main/java/com/fauna/codec/ClassCodec.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.fauna.codec; - -import com.fauna.mapping.MappingContext; -import com.fauna.mapping.MappingInfo; -import com.fauna.serialization.UTF8FaunaGenerator; -import com.fauna.serialization.UTF8FaunaParser; - -import java.io.IOException; - -public class ClassCodec implements Codec { - - private final MappingInfo info; - - public ClassCodec(MappingInfo info) { - this.info = info; - } - - @Override - public T decode(UTF8FaunaParser parser) throws IOException { - return null; - } - - @Override - public void encode(UTF8FaunaGenerator writer, T obj, MappingContext context) throws IOException { - - } - - @Override - public Class getCodecClass() { - return null; - } -} diff --git a/src/main/java/com/fauna/codec/Codec.java b/src/main/java/com/fauna/codec/Codec.java index 90374c93..f7a41184 100644 --- a/src/main/java/com/fauna/codec/Codec.java +++ b/src/main/java/com/fauna/codec/Codec.java @@ -1,6 +1,5 @@ package com.fauna.codec; -import com.fauna.mapping.MappingContext; import com.fauna.serialization.UTF8FaunaGenerator; import com.fauna.serialization.UTF8FaunaParser; @@ -10,8 +9,6 @@ public interface Codec { T decode(UTF8FaunaParser parser) throws IOException; - - void encode(UTF8FaunaGenerator gen, T obj, MappingContext context) throws IOException; - - Class getCodecClass(); + void encode(UTF8FaunaGenerator gen, T obj) throws IOException; + Class getCodecClass(); } diff --git a/src/main/java/com/fauna/codec/CodecProvider.java b/src/main/java/com/fauna/codec/CodecProvider.java index 574ca397..83a450f3 100644 --- a/src/main/java/com/fauna/codec/CodecProvider.java +++ b/src/main/java/com/fauna/codec/CodecProvider.java @@ -1,6 +1,6 @@ package com.fauna.codec; public interface CodecProvider { - Codec get(CodecRegistry registry, Class clazz); - Codec get(CodecRegistry registry, Class clazz, Class subClazz); + Codec get(Class clazz); + Codec get(Class clazz, Class typeArg); } diff --git a/src/main/java/com/fauna/codec/CodecRegistry.java b/src/main/java/com/fauna/codec/CodecRegistry.java index 6ea3d76b..74c2634b 100644 --- a/src/main/java/com/fauna/codec/CodecRegistry.java +++ b/src/main/java/com/fauna/codec/CodecRegistry.java @@ -3,7 +3,6 @@ public interface CodecRegistry { Codec get(CodecRegistryKey key); - void add(CodecRegistryKey key, Codec codec); + void put(CodecRegistryKey key, Codec codec); boolean contains(CodecRegistryKey key); - } diff --git a/src/main/java/com/fauna/codec/CodecRegistryKey.java b/src/main/java/com/fauna/codec/CodecRegistryKey.java index 2048574d..8d70553d 100644 --- a/src/main/java/com/fauna/codec/CodecRegistryKey.java +++ b/src/main/java/com/fauna/codec/CodecRegistryKey.java @@ -1,16 +1,37 @@ package com.fauna.codec; +import java.lang.reflect.Type; +import java.util.Objects; + public class CodecRegistryKey { private final Class base; - private final Class subtype; - public CodecRegistryKey(Class clazz, Class subtype) { + private final Type typeArg; + public CodecRegistryKey(Class clazz, Type typeArg) { base = clazz; - this.subtype = subtype; + this.typeArg = typeArg; + } + + public static CodecRegistryKey from(Class clazz) { + return new CodecRegistryKey(clazz, null); } - public static CodecRegistryKey from(Class clazz, Class subtype) { - return new CodecRegistryKey(clazz, subtype); + public static CodecRegistryKey from(Class clazz, Type typeArg) { + return new CodecRegistryKey(clazz, typeArg); } - // TODO: Override equals/hash + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof CodecRegistryKey)) + return false; + CodecRegistryKey other = (CodecRegistryKey)o; + + return base == other.base && typeArg == other.typeArg; + } + + @Override + public final int hashCode() { + return Objects.hash(base, typeArg); + } } diff --git a/src/main/java/com/fauna/codec/DefaultCodecProvider.java b/src/main/java/com/fauna/codec/DefaultCodecProvider.java index 3a693cb1..74e556af 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecProvider.java +++ b/src/main/java/com/fauna/codec/DefaultCodecProvider.java @@ -1,25 +1,60 @@ package com.fauna.codec; +import com.fauna.codec.codecs.*; + +import java.util.List; +import java.util.Map; + public class DefaultCodecProvider implements CodecProvider { - @Override - public Codec get(CodecRegistry registry, Class clazz) { - return get(registry, clazz, null); + + private final CodecRegistry registry; + + public DefaultCodecProvider(CodecRegistry registry) { + var dynamic = new DynamicCodec(this); + registry.put(CodecRegistryKey.from(String.class), StringCodec.singleton); + registry.put(CodecRegistryKey.from(Integer.class), IntCodec.singleton); + registry.put(CodecRegistryKey.from(int.class), IntCodec.singleton); + registry.put(CodecRegistryKey.from(Object.class), dynamic); + this.registry = registry; + } + + public Codec get(Class clazz) { + return get(clazz, null); } @Override - public Codec get(CodecRegistry registry, Class clazz, Class subClazz) { - CodecRegistryKey key = CodecRegistryKey.from(clazz, subClazz); + public Codec get(Class clazz, Class typeArg) { + CodecRegistryKey key = CodecRegistryKey.from(clazz, typeArg); if (!registry.contains(key)) { - Codec codec = generate(clazz, subClazz); - registry.add(key, codec); + synchronized (registry) { + // Check again + if (!registry.contains(key)) { + var codec = generate(clazz, typeArg); + registry.put(key, codec); + } + } } return registry.get(key); } - private Codec generate(Class clazz, Class subClazz) { - return null; + @SuppressWarnings({"unchecked"}) + private Codec generate(Class clazz, Class typeArg) { + var ta = (Class) (typeArg == null ? Object.class : typeArg); + if (clazz == List.class) { + Codec elemCodec = this.get(ta, null); + + return (Codec) new ListCodec>(elemCodec); + } + + if (clazz == Map.class) { + Codec valueCodec = this.get(ta, null); + + return (Codec) new MapCodec>(valueCodec); + } + + return new ClassCodec<>(clazz, this); } } diff --git a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java index 8c1e4d45..82229c09 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java +++ b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java @@ -1,13 +1,11 @@ package com.fauna.codec; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; public class DefaultCodecRegistry implements CodecRegistry { + private static final ConcurrentHashMap> codecs = new ConcurrentHashMap<>(); - private static final HashMap> codecs = new HashMap<>(); - - public DefaultCodecRegistry() { - } + public DefaultCodecRegistry() {} @Override @SuppressWarnings("unchecked") @@ -16,8 +14,7 @@ public Codec get(CodecRegistryKey key) { } @Override - public void add(CodecRegistryKey key, Codec codec) { - if (codecs.containsKey(key)) return; + public void put(CodecRegistryKey key, Codec codec) { codecs.put(key, codec); } diff --git a/src/main/java/com/fauna/codec/codecs/BaseCodec.java b/src/main/java/com/fauna/codec/codecs/BaseCodec.java new file mode 100644 index 00000000..0145db0f --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/BaseCodec.java @@ -0,0 +1,24 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.enums.FaunaTokenType; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public abstract class BaseCodec implements Codec { + + protected Set TAGS = new HashSet<>(Arrays.asList( + "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", "@doc", "@set", "@object" + )); + + protected String unexpectedTokenExceptionMessage(FaunaTokenType token) { + return String.format("Unexpected token `%s` decoding with `%s`", token, this.getClass()); + } + + protected String unsupportedTypeMessage(Type type){ + return String.format("Cannot encode `%s` with `%s`", type, this.getClass()); + } +} diff --git a/src/main/java/com/fauna/codec/codecs/ClassCodec.java b/src/main/java/com/fauna/codec/codecs/ClassCodec.java new file mode 100644 index 00000000..bff28e0f --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/ClassCodec.java @@ -0,0 +1,209 @@ +package com.fauna.codec.codecs; + +import com.fauna.annotation.FaunaField; +import com.fauna.annotation.FaunaFieldImpl; +import com.fauna.codec.CodecProvider; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.ClientException; +import com.fauna.exception.NullDocumentException; +import com.fauna.mapping.FieldInfo; +import com.fauna.mapping.FieldName; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; +import com.fauna.types.Module; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ClassCodec extends BaseCodec { + private static final String ID_FIELD = "id"; + private static final String NAME_FIELD = "name"; + private static final String COLL_FIELD = "coll"; + private static final String EXISTS_FIELD = "exists"; + private static final String CAUSE_FIELD = "cause"; + + + private final Type type; + private final List fields; + private final Map fieldsByName; + private final boolean shouldEscapeObject; + + public ClassCodec(Type ty, CodecProvider provider) { + this.type = ty; + + List fieldsList = new ArrayList<>(); + Map byNameMap = new HashMap<>(); + + for (Field field : ((Class) ty).getDeclaredFields()) { + if (field.getAnnotation(FaunaField.class) == null) { + continue; + } + + var attr = new FaunaFieldImpl(field, field.getAnnotation(FaunaField.class)); + + var name = !attr.name().isEmpty() ? attr.name() : FieldName.canonical(field.getName()); + + if (byNameMap.containsKey(name)) { + throw new IllegalArgumentException( + "Duplicate field name " + name + " in " + ty); + } + + var ta = attr.typeArgument() != void.class ? attr.typeArgument() : null; + var codec = provider.get(field.getType(), ta); + FieldInfo info = new FieldInfo(field, name, codec); + fieldsList.add(info); + byNameMap.put(info.getName(), info); + } + + this.shouldEscapeObject = TAGS.stream().anyMatch(byNameMap.keySet()::contains); + this.fields = List.copyOf(fieldsList); + this.fieldsByName = Map.copyOf(byNameMap); + } + + @Override + @SuppressWarnings("unchecked") + public T decode(UTF8FaunaParser parser) { + try { + FaunaTokenType endToken = parser.getCurrentTokenType().getEndToken(); + Object instance = createInstance(); + setFields(instance, parser, endToken); + return (T) instance; + } catch (IOException exc) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void encode(UTF8FaunaGenerator gen, T obj) throws IOException { + if (shouldEscapeObject) { + gen.writeStartEscapedObject(); + } else { + gen.writeStartObject(); + } + for (FieldInfo fi : fields) { + if (!fi.getName().startsWith("this$")) { + gen.writeFieldName(fi.getName()); + try { + fi.getProperty().setAccessible(true); + var value = fi.getProperty().get(obj); + fi.getCodec().encode(gen, value); + } catch (IllegalAccessException e) { + throw new ClientException("Error accessing field: " + fi.getName(), + e); + } + } + } + if (shouldEscapeObject) { + gen.writeEndEscapedObject(); + } else { + gen.writeEndObject(); + } + } + + @Override + public Class getCodecClass() { + return null; + } + + private Object createInstance() { + try { + Class clazz = Class.forName(type.getTypeName()); + Constructor constructor = clazz.getConstructor(); + return constructor.newInstance(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void setFields(Object instance, UTF8FaunaParser parser, + FaunaTokenType endToken) throws IOException, IllegalAccessException { + String id = null; + String name = null; + Module coll = null; + boolean exists = true; + String cause = null; + + while (parser.read() && parser.getCurrentTokenType() != endToken) { + if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + String fieldName = parser.getValueAsString(); + parser.read(); + + if (fieldName.equals(ID_FIELD) + && parser.getCurrentTokenType() == FaunaTokenType.STRING) { + id = parser.getValueAsString(); + trySetId(instance, parser.getValueAsString()); + } else if (fieldName.equals(NAME_FIELD) + && parser.getCurrentTokenType() == FaunaTokenType.STRING) { + name = parser.getValueAsString(); + trySetName(instance, parser.getValueAsString()); + } else { + if (fieldName.equals(COLL_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.MODULE) { + coll = parser.getValueAsModule(); + } + + if (fieldName.equals(EXISTS_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.FALSE) { + exists = parser.getValueAsBoolean(); + } + + if (fieldName.equals(CAUSE_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.STRING) { + cause = parser.getValueAsString(); + } + + FieldInfo field = fieldsByName.get(fieldName); + if (field != null) { + field.getProperty().setAccessible(true); + try { + field.getProperty() + .set(instance, field.getCodec().decode(parser)); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } else { + parser.skip(); + } + } + } + + if (endToken == FaunaTokenType.END_REF && !exists) { + throw new NullDocumentException(id != null ? id : name, coll, cause); + } + } + + private void trySetId(Object instance, String id) throws IllegalAccessException { + FieldInfo field = fieldsByName.get(ID_FIELD); + if (field != null) { + field.getProperty().setAccessible(true); + if (field.getType() == Long.class) { + field.getProperty().set(instance, Long.parseLong(id)); + } else if (field.getType() == String.class) { + field.getProperty().set(instance, id); + } else { + throw new ClientException(String.format("Unsupported type for Id field: %s", field.getType())); + } + } + } + + private void trySetName(Object instance, String name) throws IllegalAccessException { + FieldInfo field = fieldsByName.get(NAME_FIELD); + if (field != null) { + field.getProperty().setAccessible(true); + if (field.getType() == String.class) { + field.getProperty().set(instance, name); + } else { + throw new ClientException(String.format("Unsupported type for Name field: %s", field.getType())); + } + } + } +} diff --git a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java new file mode 100644 index 00000000..fc1183b6 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java @@ -0,0 +1,218 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.codec.CodecProvider; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.ClientException; +import com.fauna.exception.NullDocumentException; +import com.fauna.serialization.*; +import com.fauna.types.*; +import com.fauna.types.Module; + +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DynamicCodec extends BaseCodec { + private final ListCodec> list = new ListCodec<>(this); + private final PageCodec> page = new PageCodec<>(this); + private final MapCodec> map = new MapCodec<>(this); + private final CodecProvider provider; + + public DynamicCodec(CodecProvider provider) { + + this.provider = provider; + } + + @Override + public Object decode(UTF8FaunaParser parser) throws IOException { + Object value = null; + switch (parser.getCurrentTokenType()) { + case NULL: + break; + case START_OBJECT: + value = map.decode(parser); + break; + case START_ARRAY: + value = list.decode(parser); + break; + case START_PAGE: + value = page.decode(parser); + break; + case START_REF: + value = decodeRef(parser); + break; + case START_DOCUMENT: + value = decodeDocument(parser); + break; + case MODULE: + value = parser.getValueAsModule(); + break; + case INT: + value = parser.getValueAsInt(); + break; + case STRING: + value = parser.getValueAsString(); + break; + case DATE: + value = parser.getValueAsLocalDate(); + break; + case TIME: + value = parser.getValueAsTime(); + break; + case DOUBLE: + value = parser.getValueAsDouble(); + break; + case LONG: + value = parser.getValueAsLong(); + break; + case TRUE: + case FALSE: + value = parser.getValueAsBoolean(); + break; + default: + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + return value; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void encode(UTF8FaunaGenerator gen, Object obj) throws IOException { + + // TODO: deal with Object.class loop + Codec codec = provider.get(obj.getClass()); + codec.encode(gen, obj); + } + + @Override + public Class getCodecClass() { + return null; + } + + private Object decodeDocument(UTF8FaunaParser parser) + throws IOException { + Map data = new HashMap<>(); + String id = null; + String name = null; + Instant ts = null; + Module coll = null; + + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_DOCUMENT) { + if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + String fieldName = parser.getValueAsString(); + parser.read(); + switch (fieldName) { + case "id": + id = parser.getValueAsString(); + break; + case "name": + name = parser.getValueAsString(); + break; + case "ts": + ts = parser.getValueAsTime(); + break; + case "coll": + coll = parser.getValueAsModule(); + break; + default: + var v = this.decode(parser); + if (v != null) data.put(fieldName, v); + break; + } + } + + if (id != null && coll != null && ts != null) { + if (name != null) { + data.put("name", name); + } + return new Document(id, coll, ts, data); + } + + if (name != null && coll != null && ts != null) { + return new NamedDocument(name, coll, ts, data); + } + + if (id != null) { + data.put("id", id); + } + if (name != null) { + data.put("name", name); + } + if (coll != null) { + data.put("coll", coll); + } + if (ts != null) { + data.put("ts", ts); + } + return data; + } + + private Object decodeRef(UTF8FaunaParser parser) + throws IOException { + String id = null; + String name = null; + Module coll = null; + boolean exists = true; + String cause = null; + Map allProps = new HashMap<>(); + + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_REF) { + if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + String fieldName = parser.getValueAsString(); + parser.read(); + switch (fieldName) { + case "id": + id = parser.getValueAsString(); + allProps.put("id", id); + break; + case "name": + name = parser.getValueAsString(); + allProps.put("name", name); + break; + case "coll": + coll = parser.getValueAsModule(); + allProps.put("coll", coll); + break; + case "exists": + exists = parser.getValueAsBoolean(); + allProps.put("exists", exists); + break; + case "cause": + cause = parser.getValueAsString(); + allProps.put("cause", cause); + break; + default: + allProps.put(fieldName, this.decode(parser)); + break; + } + } + + if (id != null && coll != null) { + if (exists) { + return new DocumentRef(id, coll); + } + + throw new NullDocumentException(id, coll, cause); + } + + if (name != null && coll != null) { + if (exists) { + return new NamedDocumentRef(name, coll); + } + + throw new NullDocumentException(name, coll, cause); + } + + return allProps; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/IntCodec.java b/src/main/java/com/fauna/codec/codecs/IntCodec.java new file mode 100644 index 00000000..bf910197 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/IntCodec.java @@ -0,0 +1,39 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class IntCodec extends BaseCodec { + + public static final IntCodec singleton = new IntCodec(); + + @Override + public Integer decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + case LONG: + return parser.getValueAsInt(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Integer obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeIntValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Integer.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/ListCodec.java b/src/main/java/com/fauna/codec/codecs/ListCodec.java new file mode 100644 index 00000000..eba3ada8 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/ListCodec.java @@ -0,0 +1,56 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ListCodec> extends BaseCodec { + + private final Codec elementCodec; + + public ListCodec(Codec elementCodec) { + this.elementCodec = elementCodec; + } + + @Override + public L decode(UTF8FaunaParser parser) throws IOException { + if (parser.getCurrentTokenType() != FaunaTokenType.START_ARRAY) { + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + List list = new ArrayList<>(); + + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_ARRAY) { + E value = elementCodec.decode(parser); + list.add(value); + } + + return (L) list; + } + + @Override + public void encode(UTF8FaunaGenerator gen, L obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + return; + } + + gen.writeStartArray(); + + for (E elem : obj) { + elementCodec.encode(gen, elem); + } + gen.writeEndArray(); + } + + @Override + public Class getCodecClass() { + return List.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/MapCodec.java b/src/main/java/com/fauna/codec/codecs/MapCodec.java new file mode 100644 index 00000000..70404579 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/MapCodec.java @@ -0,0 +1,76 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MapCodec> extends BaseCodec { + + private final Codec valueCodec; + + public MapCodec(Codec valueCodec) { + this.valueCodec = valueCodec; + } + + @Override + @SuppressWarnings("unchecked") + public L decode(UTF8FaunaParser parser) throws IOException { + if (parser.getCurrentTokenType() != FaunaTokenType.START_OBJECT) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + Map map = new HashMap<>(); + + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_OBJECT) { + if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + + String fieldName = parser.getValueAsString(); + parser.read(); + V value = valueCodec.decode(parser); + map.put(fieldName, value); + } + + return (L) map; + } + + @Override + public void encode(UTF8FaunaGenerator gen, L obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + return; + } + + boolean shouldEscape = obj.keySet().stream().anyMatch(TAGS::contains); + if (shouldEscape) { + gen.writeStartEscapedObject(); + } else { + gen.writeStartObject(); + } + + for (Map.Entry entry : obj.entrySet()) { + gen.writeFieldName(entry.getKey().toString()); + + valueCodec.encode(gen, entry.getValue()); + } + + if (shouldEscape) { + gen.writeEndEscapedObject(); + } else { + gen.writeEndObject(); + } + } + + @Override + public Class getCodecClass() { + return List.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/PageCodec.java b/src/main/java/com/fauna/codec/codecs/PageCodec.java new file mode 100644 index 00000000..c99c0504 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/PageCodec.java @@ -0,0 +1,84 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; +import com.fauna.types.Page; + +import java.io.IOException; +import java.util.List; + +public class PageCodec> extends BaseCodec { + + private final Codec elementCodec; + private final Codec> listCodec; + + public PageCodec(Codec elementCodec) { + this.elementCodec = elementCodec; + this.listCodec = new ListCodec<>(elementCodec); + } + + @Override + public L decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case START_PAGE: + return decodePage(parser, FaunaTokenType.END_PAGE); + case START_OBJECT: + return decodePage(parser, FaunaTokenType.END_OBJECT); + case START_DOCUMENT: + return wrapDocumentInPage(parser); + default: + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, L obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + return; + } + + throw new ClientException(this.unsupportedTypeMessage(obj.getClass())); + } + + @Override + public Class getCodecClass() { + return Page.class; + } + + + @SuppressWarnings("unchecked") + private L decodePage(UTF8FaunaParser parser, FaunaTokenType endToken) throws IOException { + List data = null; + String after = null; + + while (parser.read() && parser.getCurrentTokenType() != endToken) { + String fieldName = parser.getValueAsString(); + parser.read(); + + switch (fieldName) { + case "data": + data = listCodec.decode(parser); + break; + case "after": + after = parser.getValueAsString(); + break; + } + } + + if (data == null) { + throw new ClientException("No page data found while deserializing into Page<>"); + } + + return (L) new Page<>(data, after); + } + + @SuppressWarnings("unchecked") + private L wrapDocumentInPage(UTF8FaunaParser parser) throws IOException { + E elem = this.elementCodec.decode(parser); + return (L) new Page<>(List.of(elem), null); + } +} diff --git a/src/main/java/com/fauna/codec/codecs/StringCodec.java b/src/main/java/com/fauna/codec/codecs/StringCodec.java new file mode 100644 index 00000000..3eebf59f --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/StringCodec.java @@ -0,0 +1,38 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class StringCodec extends BaseCodec { + + public static final StringCodec singleton = new StringCodec(); + + @Override + public String decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case STRING: + return parser.getValueAsString(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, String obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeStringValue(obj); + } + } + + @Override + public Class getCodecClass() { + return String.class; + } +} diff --git a/src/main/java/com/fauna/mapping/FieldInfo.java b/src/main/java/com/fauna/mapping/FieldInfo.java index e052e090..cc62853c 100644 --- a/src/main/java/com/fauna/mapping/FieldInfo.java +++ b/src/main/java/com/fauna/mapping/FieldInfo.java @@ -1,9 +1,11 @@ package com.fauna.mapping; import com.fauna.annotation.FaunaFieldImpl; +import com.fauna.codec.Codec; import com.fauna.interfaces.IDeserializer; import com.fauna.serialization.Deserializer; import com.fauna.serialization.NullableDeserializer; + import java.lang.reflect.Field; public final class FieldInfo { @@ -11,9 +13,14 @@ public final class FieldInfo { private final String name; private final Field property; private final Class type; - private final boolean isNullable; + private Codec codec; + + // TODO: remove this when we flip to using codecs private final MappingContext ctx; + // TODO: remove this when we flip to using codecs private IDeserializer deserializer; + // TODO: remove this when we flip to using codecs + private boolean isNullable; public FieldInfo(MappingContext ctx, FaunaFieldImpl attr, Field prop) { this.ctx = ctx; @@ -21,7 +28,18 @@ public FieldInfo(MappingContext ctx, FaunaFieldImpl attr, Field prop) { attr != null && attr.name() != null ? attr.name() : FieldName.canonical(prop.getName()); this.property = prop; this.type = prop.getType(); - this.isNullable = attr != null ? attr.nullable() : false; + this.isNullable = attr != null && attr.nullable(); + this.codec = null; + } + + public FieldInfo(Field prop, String name, Codec codec) { + this.name = name; + this.property = prop; + this.type = prop.getType(); + this.codec = codec; + + // TODO: remove this when we've flipped to using codecs + this.ctx = null; } public String getName() { @@ -47,4 +65,8 @@ public IDeserializer getDeserializer() { return deserializer; } } + + public Codec getCodec() { + return this.codec; + } } diff --git a/src/main/java/com/fauna/response/QuerySuccess.java b/src/main/java/com/fauna/response/QuerySuccess.java index c1c7070d..44615d64 100644 --- a/src/main/java/com/fauna/response/QuerySuccess.java +++ b/src/main/java/com/fauna/response/QuerySuccess.java @@ -26,7 +26,6 @@ public QuerySuccess(IDeserializer deserializer, JsonNode json, QueryStats sta if ((elem = json.get(ResponseFields.DATA_FIELD_NAME)) != null) { // FIXME: avoid converting the parsed `elem` to a string and re-parsing the JSON. UTF8FaunaParser reader = new UTF8FaunaParser(elem.toString()); - reader.read(); this.data = deserializer.deserialize(reader); } diff --git a/src/main/java/com/fauna/serialization/BaseDeserializer.java b/src/main/java/com/fauna/serialization/BaseDeserializer.java index 3b620014..c589f721 100644 --- a/src/main/java/com/fauna/serialization/BaseDeserializer.java +++ b/src/main/java/com/fauna/serialization/BaseDeserializer.java @@ -1,6 +1,5 @@ package com.fauna.serialization; -import com.fauna.enums.FaunaTokenType; import com.fauna.interfaces.IDeserializer; import java.io.IOException; @@ -9,9 +8,6 @@ public abstract class BaseDeserializer implements IDeserializer { @Override public T deserialize(UTF8FaunaParser parser) throws IOException { - if (parser.getCurrentTokenType() == FaunaTokenType.NONE) { - parser.read(); - } return doDeserialize(parser); } diff --git a/src/main/java/com/fauna/serialization/UTF8FaunaParser.java b/src/main/java/com/fauna/serialization/UTF8FaunaParser.java index dc2dd83e..9a01d406 100644 --- a/src/main/java/com/fauna/serialization/UTF8FaunaParser.java +++ b/src/main/java/com/fauna/serialization/UTF8FaunaParser.java @@ -56,6 +56,9 @@ public UTF8FaunaParser(String str) throws IOException { InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); JsonFactory factory = new JsonFactory(); this.jsonParser = factory.createParser(inputStream); + if (getCurrentTokenType() == FaunaTokenType.NONE) { + read(); + } } private enum InternalTokenType { @@ -65,6 +68,9 @@ private enum InternalTokenType { public UTF8FaunaParser(InputStream body) throws IOException { JsonFactory factory = new JsonFactory(); this.jsonParser = factory.createParser(body); + if (getCurrentTokenType() == FaunaTokenType.NONE) { + read(); + } } public UTF8FaunaParser(JsonParser jsonParser) { @@ -277,6 +283,13 @@ private void validateTaggedType(FaunaTokenType type) { } } + private void validateTaggedTypes(FaunaTokenType... types) { + if (!Arrays.asList(types).contains(currentFaunaTokenType)) + throw new IllegalStateException( + "CurrentTokenType is a " + currentFaunaTokenType.toString() + + ", not in " + Arrays.toString(types) + "."); + } + public Character getValueAsCharacter() { validateTaggedType(FaunaTokenType.INT); return Character.valueOf((char) Integer.parseInt(taggedTokenValue)); @@ -310,7 +323,7 @@ public Short getValueAsShort() { } public Integer getValueAsInt() { - validateTaggedType(FaunaTokenType.INT); + validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); try { return Integer.parseInt(taggedTokenValue); } catch (NumberFormatException e) { diff --git a/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java new file mode 100644 index 00000000..f6cbec09 --- /dev/null +++ b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java @@ -0,0 +1,20 @@ +package com.fauna.beans; + +import com.fauna.annotation.FaunaField; +import com.fauna.annotation.FaunaObject; + +import java.util.List; +import java.util.Map; + +@FaunaObject +public class ClassWithParameterizedFields { + + @FaunaField(name = "first_name") + public String firstName = "Baz"; + + @FaunaField(name = "a_list", typeArgument = String.class) + public List list = List.of("item1"); + + @FaunaField(name = "a_map", typeArgument = Integer.class) + public Map map = Map.of("key1", 42); +} \ No newline at end of file diff --git a/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java b/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java new file mode 100644 index 00000000..cbdea960 --- /dev/null +++ b/src/test/java/com/fauna/codec/CodecRegistryKeyTest.java @@ -0,0 +1,42 @@ +package com.fauna.codec; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class CodecRegistryKeyTest { + + + @Test + public void equals_classAndSubClassNotNullAreEqual() { + CodecRegistryKey key1 = CodecRegistryKey.from(String.class, Integer.class); + CodecRegistryKey key2 = CodecRegistryKey.from(String.class, Integer.class); + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void equals_classAndSubclassNullAreEqual() { + CodecRegistryKey key1 = CodecRegistryKey.from(null, null); + CodecRegistryKey key2 = CodecRegistryKey.from(null, null); + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void equals_differentClassesNotEqual() { + CodecRegistryKey key1 = CodecRegistryKey.from(String.class, Integer.class); + CodecRegistryKey key2 = CodecRegistryKey.from(Object.class, Integer.class); + assertNotEquals(key1, key2); + assertNotEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + public void equals_differentSubClassesNotEqual() { + CodecRegistryKey key1 = CodecRegistryKey.from(String.class, Integer.class); + CodecRegistryKey key2 = CodecRegistryKey.from(String.class, Object.class); + assertNotEquals(key1, key2); + assertNotEquals(key1.hashCode(), key2.hashCode()); + } +} diff --git a/src/test/java/com/fauna/codec/CodecRegistryTest.java b/src/test/java/com/fauna/codec/CodecRegistryTest.java new file mode 100644 index 00000000..b73c087a --- /dev/null +++ b/src/test/java/com/fauna/codec/CodecRegistryTest.java @@ -0,0 +1,21 @@ +package com.fauna.codec; + +import com.fauna.codec.codecs.IntCodec; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class CodecRegistryTest { + + CodecRegistry reg = new DefaultCodecRegistry(); + + @Test + public void put_addsCodecWithKey() { + CodecRegistryKey key = CodecRegistryKey.from(String.class, Integer.class); + Codec codec = new IntCodec(); + reg.put(key, codec); + Codec result = reg.get(key); + assertEquals(codec, result); + } +} diff --git a/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java b/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java new file mode 100644 index 00000000..362f8b33 --- /dev/null +++ b/src/test/java/com/fauna/codec/DefaultCodecProviderTest.java @@ -0,0 +1,28 @@ +package com.fauna.codec; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class DefaultCodecProviderTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + public void get_returnsRegisteredCodec() { + Codec codec = cp.get(Integer.class, null); + assertNotNull(codec); + assertEquals(Integer.class ,codec.getCodecClass()); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void get_generatesListCodec() { + Codec> codec = (Codec>) (Codec) cp.get(List.class, Integer.class); + assertNotNull(codec); + assertEquals(List.class, codec.getCodecClass()); + } +} diff --git a/src/test/java/com/fauna/codec/Helpers.java b/src/test/java/com/fauna/codec/Helpers.java new file mode 100644 index 00000000..5ae302d5 --- /dev/null +++ b/src/test/java/com/fauna/codec/Helpers.java @@ -0,0 +1,20 @@ +package com.fauna.codec; + +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class Helpers { + + public static T decode(Codec codec, String val) throws IOException { + var parser = new UTF8FaunaParser(val); + return codec.decode(parser); + } + + public static String encode(Codec codec, T obj) throws IOException { + var gen = new UTF8FaunaGenerator(); + codec.encode(gen, obj); + return gen.serialize(); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java new file mode 100644 index 00000000..8890f031 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java @@ -0,0 +1,24 @@ +package com.fauna.codec.codecs; + +import com.fauna.beans.ClassWithParameterizedFields; +import com.fauna.codec.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ClassCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + public void roundtrip_classWithParameterizedFields() throws IOException { + var wire = "{\"first_name\":\"foo\",\"a_list\":[\"item1\"],\"a_map\":{\"key1\":{\"@int\":\"42\"}}}"; + var codec = cp.get(ClassWithParameterizedFields.class); + ClassWithParameterizedFields decoded = Helpers.decode(codec, wire); + var encoded = Helpers.encode(codec, decoded); + assertEquals(wire, encoded); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/IntCodecTest.java b/src/test/java/com/fauna/codec/codecs/IntCodecTest.java new file mode 100644 index 00000000..4bc2ffed --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/IntCodecTest.java @@ -0,0 +1,23 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IntCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + public void roundtrip_int() throws IOException { + var wire = "{\"@int\":\"42\"}"; + var codec = cp.get(int.class); + int decoded = Helpers.decode(codec, wire); + var encoded = Helpers.encode(codec, decoded); + assertEquals(wire, encoded); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/ListCodecTest.java b/src/test/java/com/fauna/codec/codecs/ListCodecTest.java new file mode 100644 index 00000000..6203fc46 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/ListCodecTest.java @@ -0,0 +1,26 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class ListCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + @SuppressWarnings("unchecked") + public void roundtrip_listOfIntegers() throws IOException { + var wire = "[{\"@int\":\"42\"}]"; + var codec = cp.get(List.class, Integer.class); + List decoded = Helpers.decode(codec, wire); + var encoded = Helpers.encode(codec, decoded); + assertEquals(wire, encoded); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/MapCodecTest.java b/src/test/java/com/fauna/codec/codecs/MapCodecTest.java new file mode 100644 index 00000000..8578a9f2 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/MapCodecTest.java @@ -0,0 +1,27 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class MapCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + @SuppressWarnings("unchecked") + public void roundtrip_mapOfIntegers() throws IOException { + var wire = "{\"key1\":{\"@int\":\"42\"}}"; + var codec = cp.get(Map.class, Integer.class); + Map decoded = Helpers.decode(codec, wire); + var encoded = Helpers.encode(codec, decoded); + assertEquals(wire, encoded); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/StringCodecTest.java b/src/test/java/com/fauna/codec/codecs/StringCodecTest.java new file mode 100644 index 00000000..950a96c4 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/StringCodecTest.java @@ -0,0 +1,24 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StringCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + public void roundtrip_string() throws IOException { + var wire = "\"disco\""; + var codec = cp.get(String.class); + String decoded = Helpers.decode(codec, wire); + var encoded = Helpers.encode(codec, decoded); + assertEquals(wire, encoded); + } +} diff --git a/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java b/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java index afaa9ac6..92abd7fa 100644 --- a/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java +++ b/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java @@ -68,10 +68,8 @@ public void testUnexpectedEndDuringAdvance() throws IOException { String json = "{\"@int\": \"123\""; InputStream inputStream = new ByteArrayInputStream(json.getBytes()); - UTF8FaunaParser reader = new UTF8FaunaParser(inputStream); - Exception ex = assertThrows(ClientException.class, - () -> reader.read()); + () -> new UTF8FaunaParser(inputStream)); assertEquals("Failed to advance underlying JSON reader.", ex.getMessage()); } @@ -525,7 +523,6 @@ public void skipValues() throws IOException { for (String test : tests) { UTF8FaunaParser reader = new UTF8FaunaParser(new ByteArrayInputStream(test.getBytes())); - reader.read(); reader.skip(); assertFalse(reader.read()); } @@ -535,7 +532,6 @@ public void skipValues() throws IOException { public void skipNestedEscapedObject() throws IOException { String test = "{\"@object\": {\"inner\": {\"@object\": {\"foo\": \"bar\"}}, \"k2\": {}}}"; UTF8FaunaParser reader = new UTF8FaunaParser(new ByteArrayInputStream(test.getBytes())); - reader.read(); // {"@object":{ assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // inner assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -552,7 +548,6 @@ public void skipNestedEscapedObject() throws IOException { public void skipNestedObject() throws IOException { String test = "{\"k\":{\"inner\":{}},\"k2\":{}}"; UTF8FaunaParser reader = new UTF8FaunaParser(new ByteArrayInputStream(test.getBytes())); - reader.read(); // { assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // k assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -569,7 +564,6 @@ public void skipNestedObject() throws IOException { public void skipNestedArrays() throws IOException { String test = "{\"k\":[\"1\",\"2\"],\"k2\":{}}"; UTF8FaunaParser reader = new UTF8FaunaParser(new ByteArrayInputStream(test.getBytes())); - reader.read(); // { assertEquals(FaunaTokenType.START_OBJECT, reader.getCurrentTokenType()); reader.read(); // k assertEquals(FaunaTokenType.FIELD_NAME, reader.getCurrentTokenType()); @@ -585,7 +579,6 @@ public void skipNestedArrays() throws IOException { private static void assertReader(UTF8FaunaParser reader, List> tokens) throws IOException { for (Map.Entry entry : tokens) { - reader.read(); assertNotNull(entry.getKey()); assertNotNull(reader.getCurrentTokenType()); assertEquals(entry.getKey(), reader.getCurrentTokenType()); @@ -621,6 +614,8 @@ private static void assertReader(UTF8FaunaParser reader, assertNull(entry.getValue()); break; } + + reader.read(); } assertFalse(reader.read()); From 3fadd8160eb43875adfa798aff97d1eba89c6b07 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Tue, 13 Aug 2024 14:38:00 +0200 Subject: [PATCH 02/88] PR feedback --- .../com/fauna/codec/CodecRegistryKey.java | 13 +- .../com/fauna/codec/DefaultCodecProvider.java | 9 +- .../com/fauna/codec/DefaultCodecRegistry.java | 5 +- .../com/fauna/codec/codecs/BaseCodec.java | 5 +- .../com/fauna/codec/codecs/ClassCodec.java | 115 ++++++++---------- .../com/fauna/codec/codecs/DynamicCodec.java | 49 +++----- .../com/fauna/codec/codecs/InternalRef.java | 93 ++++++++++++++ .../java/com/fauna/codec/codecs/MapCodec.java | 7 +- .../com/fauna/codec/codecs/PageCodec.java | 15 +-- 9 files changed, 188 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/fauna/codec/codecs/InternalRef.java diff --git a/src/main/java/com/fauna/codec/CodecRegistryKey.java b/src/main/java/com/fauna/codec/CodecRegistryKey.java index 8d70553d..89d56ed5 100644 --- a/src/main/java/com/fauna/codec/CodecRegistryKey.java +++ b/src/main/java/com/fauna/codec/CodecRegistryKey.java @@ -20,14 +20,15 @@ public static CodecRegistryKey from(Class clazz, Type typeArg) { } @Override - public boolean equals(Object o) { - if (o == this) + public boolean equals(Object other) { + if (other == this) { return true; - if (!(o instanceof CodecRegistryKey)) + } else if (other instanceof CodecRegistryKey) { + CodecRegistryKey otherCRK = (CodecRegistryKey) other; + return this.base == otherCRK.base && this.typeArg == otherCRK.typeArg; + } else { return false; - CodecRegistryKey other = (CodecRegistryKey)o; - - return base == other.base && typeArg == other.typeArg; + } } @Override diff --git a/src/main/java/com/fauna/codec/DefaultCodecProvider.java b/src/main/java/com/fauna/codec/DefaultCodecProvider.java index 74e556af..197ee7f4 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecProvider.java +++ b/src/main/java/com/fauna/codec/DefaultCodecProvider.java @@ -28,13 +28,8 @@ public Codec get(Class clazz, Class typeArg) { CodecRegistryKey key = CodecRegistryKey.from(clazz, typeArg); if (!registry.contains(key)) { - synchronized (registry) { - // Check again - if (!registry.contains(key)) { - var codec = generate(clazz, typeArg); - registry.put(key, codec); - } - } + var codec = generate(clazz, typeArg); + registry.put(key, codec); } return registry.get(key); diff --git a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java index 82229c09..ef0c26f6 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java +++ b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java @@ -8,9 +8,10 @@ public class DefaultCodecRegistry implements CodecRegistry { public DefaultCodecRegistry() {} @Override - @SuppressWarnings("unchecked") public Codec get(CodecRegistryKey key) { - return (Codec) codecs.get(key); + @SuppressWarnings("unchecked") + var codec = (Codec) codecs.get(key); + return codec; } @Override diff --git a/src/main/java/com/fauna/codec/codecs/BaseCodec.java b/src/main/java/com/fauna/codec/codecs/BaseCodec.java index 0145db0f..9eba2e41 100644 --- a/src/main/java/com/fauna/codec/codecs/BaseCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BaseCodec.java @@ -4,6 +4,7 @@ import com.fauna.enums.FaunaTokenType; import java.lang.reflect.Type; +import java.text.MessageFormat; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -15,10 +16,10 @@ public abstract class BaseCodec implements Codec { )); protected String unexpectedTokenExceptionMessage(FaunaTokenType token) { - return String.format("Unexpected token `%s` decoding with `%s`", token, this.getClass()); + return MessageFormat.format("Unexpected token `{0}` decoding with `{1}`", token, this.getClass()); } protected String unsupportedTypeMessage(Type type){ - return String.format("Cannot encode `%s` with `%s`", type, this.getClass()); + return MessageFormat.format("Cannot encode `{0}` with `{1}`", type, this.getClass()); } } diff --git a/src/main/java/com/fauna/codec/codecs/ClassCodec.java b/src/main/java/com/fauna/codec/codecs/ClassCodec.java index bff28e0f..691f426e 100644 --- a/src/main/java/com/fauna/codec/codecs/ClassCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ClassCodec.java @@ -2,6 +2,7 @@ import com.fauna.annotation.FaunaField; import com.fauna.annotation.FaunaFieldImpl; +import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.enums.FaunaTokenType; import com.fauna.exception.ClientException; @@ -10,11 +11,11 @@ import com.fauna.mapping.FieldName; import com.fauna.serialization.UTF8FaunaGenerator; import com.fauna.serialization.UTF8FaunaParser; -import com.fauna.types.Module; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; @@ -67,22 +68,23 @@ public ClassCodec(Type ty, CodecProvider provider) { } @Override - @SuppressWarnings("unchecked") public T decode(UTF8FaunaParser parser) { try { FaunaTokenType endToken = parser.getCurrentTokenType().getEndToken(); Object instance = createInstance(); setFields(instance, parser, endToken); - return (T) instance; + @SuppressWarnings("unchecked") + T typed = (T) instance; + return typed; } catch (IOException exc) { throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); - } catch (IllegalAccessException e) { + } catch (IllegalAccessException | ClassNotFoundException | InvocationTargetException | InstantiationException | + NoSuchMethodException e) { throw new RuntimeException(e); } } @Override - @SuppressWarnings("unchecked") public void encode(UTF8FaunaGenerator gen, T obj) throws IOException { if (shouldEscapeObject) { gen.writeStartEscapedObject(); @@ -94,8 +96,11 @@ public void encode(UTF8FaunaGenerator gen, T obj) throws IOException { gen.writeFieldName(fi.getName()); try { fi.getProperty().setAccessible(true); - var value = fi.getProperty().get(obj); - fi.getCodec().encode(gen, value); + @SuppressWarnings("unchecked") + T value = (T) fi.getProperty().get(obj); + @SuppressWarnings("unchecked") + Codec codec = fi.getCodec(); + codec.encode(gen, value); } catch (IllegalAccessException e) { throw new ClientException("Error accessing field: " + fi.getName(), e); @@ -114,23 +119,16 @@ public Class getCodecClass() { return null; } - private Object createInstance() { - try { - Class clazz = Class.forName(type.getTypeName()); - Constructor constructor = clazz.getConstructor(); - return constructor.newInstance(); - } catch (Exception ex) { - throw new RuntimeException(ex); - } + private Object createInstance() throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException { + Class clazz = Class.forName(type.getTypeName()); + Constructor constructor = clazz.getConstructor(); + return constructor.newInstance(); } private void setFields(Object instance, UTF8FaunaParser parser, FaunaTokenType endToken) throws IOException, IllegalAccessException { - String id = null; - String name = null; - Module coll = null; - boolean exists = true; - String cause = null; + + InternalRef.Builder refBuilder = new InternalRef.Builder(); while (parser.read() && parser.getCurrentTokenType() != endToken) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { @@ -140,70 +138,59 @@ private void setFields(Object instance, UTF8FaunaParser parser, String fieldName = parser.getValueAsString(); parser.read(); - if (fieldName.equals(ID_FIELD) - && parser.getCurrentTokenType() == FaunaTokenType.STRING) { - id = parser.getValueAsString(); - trySetId(instance, parser.getValueAsString()); - } else if (fieldName.equals(NAME_FIELD) - && parser.getCurrentTokenType() == FaunaTokenType.STRING) { - name = parser.getValueAsString(); - trySetName(instance, parser.getValueAsString()); - } else { - if (fieldName.equals(COLL_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.MODULE) { - coll = parser.getValueAsModule(); - } - - if (fieldName.equals(EXISTS_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.FALSE) { - exists = parser.getValueAsBoolean(); - } - - if (fieldName.equals(CAUSE_FIELD) && parser.getCurrentTokenType() == FaunaTokenType.STRING) { - cause = parser.getValueAsString(); - } + if (endToken == FaunaTokenType.END_REF) { + refBuilder = refBuilder.withField(fieldName, parser); + } - FieldInfo field = fieldsByName.get(fieldName); - if (field != null) { - field.getProperty().setAccessible(true); - try { - field.getProperty() - .set(instance, field.getCodec().decode(parser)); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } else { - parser.skip(); - } + if (fieldName.equals(ID_FIELD)) { + trySetId(fieldName, instance, parser); + } else if (fieldName.equals(NAME_FIELD)) { + trySetName(fieldName, instance, parser); + } else { + trySetField(fieldName, instance, parser); } } - if (endToken == FaunaTokenType.END_REF && !exists) { - throw new NullDocumentException(id != null ? id : name, coll, cause); - } + refBuilder.build().throwIfNotExists(); } - private void trySetId(Object instance, String id) throws IllegalAccessException { - FieldInfo field = fieldsByName.get(ID_FIELD); + private void trySetId(String fieldName, Object instance, UTF8FaunaParser parser) throws IllegalAccessException { + if (parser.getCurrentTokenType() != FaunaTokenType.STRING) return; + + FieldInfo field = fieldsByName.get(fieldName); if (field != null) { - field.getProperty().setAccessible(true); + + String id = parser.getValueAsString(); + field.getProperty().setAccessible(true); + if (field.getType() == Long.class) { field.getProperty().set(instance, Long.parseLong(id)); } else if (field.getType() == String.class) { field.getProperty().set(instance, id); - } else { - throw new ClientException(String.format("Unsupported type for Id field: %s", field.getType())); } } } - private void trySetName(Object instance, String name) throws IllegalAccessException { - FieldInfo field = fieldsByName.get(NAME_FIELD); + private void trySetName(String fieldName,Object instance, UTF8FaunaParser parser) throws IllegalAccessException { + if (parser.getCurrentTokenType() != FaunaTokenType.STRING) return; + + FieldInfo field = fieldsByName.get(fieldName); if (field != null) { + String name = parser.getValueAsString(); field.getProperty().setAccessible(true); if (field.getType() == String.class) { field.getProperty().set(instance, name); - } else { - throw new ClientException(String.format("Unsupported type for Name field: %s", field.getType())); } } } + + private void trySetField(String fieldName, Object instance, UTF8FaunaParser parser) throws IOException, IllegalAccessException { + FieldInfo field = fieldsByName.get(fieldName); + if (field == null) { + parser.skip(); + } else { + field.getProperty().setAccessible(true); + field.getProperty().set(instance, field.getCodec().decode(parser)); + } + } } diff --git a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java index fc1183b6..3de4af7e 100644 --- a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java +++ b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java @@ -28,62 +28,47 @@ public DynamicCodec(CodecProvider provider) { @Override public Object decode(UTF8FaunaParser parser) throws IOException { - Object value = null; switch (parser.getCurrentTokenType()) { case NULL: - break; + return null; case START_OBJECT: - value = map.decode(parser); - break; + return map.decode(parser); case START_ARRAY: - value = list.decode(parser); - break; + return list.decode(parser); case START_PAGE: - value = page.decode(parser); - break; + return page.decode(parser); case START_REF: - value = decodeRef(parser); - break; + return decodeRef(parser); case START_DOCUMENT: - value = decodeDocument(parser); - break; + return decodeDocument(parser); case MODULE: - value = parser.getValueAsModule(); - break; + return parser.getValueAsModule(); case INT: - value = parser.getValueAsInt(); - break; + return parser.getValueAsInt(); case STRING: - value = parser.getValueAsString(); - break; + return parser.getValueAsString(); case DATE: - value = parser.getValueAsLocalDate(); - break; + return parser.getValueAsLocalDate(); case TIME: - value = parser.getValueAsTime(); - break; + return parser.getValueAsTime(); case DOUBLE: - value = parser.getValueAsDouble(); - break; + return parser.getValueAsDouble(); case LONG: - value = parser.getValueAsLong(); - break; + return parser.getValueAsLong(); case TRUE: case FALSE: - value = parser.getValueAsBoolean(); - break; - default: - throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + return parser.getValueAsBoolean(); } - return value; + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } @Override - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings("unchecked") public void encode(UTF8FaunaGenerator gen, Object obj) throws IOException { // TODO: deal with Object.class loop + @SuppressWarnings("rawtypes") Codec codec = provider.get(obj.getClass()); codec.encode(gen, obj); } diff --git a/src/main/java/com/fauna/codec/codecs/InternalRef.java b/src/main/java/com/fauna/codec/codecs/InternalRef.java new file mode 100644 index 00000000..4ece59d5 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/InternalRef.java @@ -0,0 +1,93 @@ +package com.fauna.codec.codecs; + +import com.fauna.client.ExponentialBackoffStrategy; +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.NullDocumentException; +import com.fauna.serialization.UTF8FaunaParser; +import com.fauna.types.Module; + +class InternalRef { + + private final String id; + private final String name; + private final Module coll; + private final boolean exists; + private final String cause; + + public InternalRef(String id, String name, Module coll, boolean exists, String cause){ + this.id = id; + this.name = name; + this.coll = coll; + this.exists = exists; + this.cause = cause; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public Module getColl() { + return coll; + } + + public boolean getExists() { + return exists; + } + + public String getCause() { + return cause; + } + + public void throwIfNotExists() { + if (!getExists()) { + throw new NullDocumentException(getId() != null ? getId() : getName(), getColl(), getCause()); + } + } + + static class Builder { + private String id = null; + private String name = null; + private Module coll = null; + private boolean exists = true; + private String cause = null; + + InternalRef.Builder withField(String fieldName, UTF8FaunaParser parser) { + switch (fieldName) { + case "id": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.id = parser.getValueAsString(); + } + break; + case "name": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.name = parser.getValueAsString(); + } + break; + case "coll": + if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { + this.coll = parser.getValueAsModule(); + } + break; + case "exists": + if (parser.getCurrentTokenType() == FaunaTokenType.FALSE) { + this.exists = parser.getValueAsBoolean(); + } + break; + case "cause": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.cause = parser.getValueAsString(); + } + break; + } + return this; + } + + InternalRef build() { + return new InternalRef(this.id, this.name, this.coll, this.exists, this.cause); + } + } +} diff --git a/src/main/java/com/fauna/codec/codecs/MapCodec.java b/src/main/java/com/fauna/codec/codecs/MapCodec.java index 70404579..3eaaae62 100644 --- a/src/main/java/com/fauna/codec/codecs/MapCodec.java +++ b/src/main/java/com/fauna/codec/codecs/MapCodec.java @@ -20,7 +20,6 @@ public MapCodec(Codec valueCodec) { } @Override - @SuppressWarnings("unchecked") public L decode(UTF8FaunaParser parser) throws IOException { if (parser.getCurrentTokenType() != FaunaTokenType.START_OBJECT) { throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); @@ -38,8 +37,10 @@ public L decode(UTF8FaunaParser parser) throws IOException { V value = valueCodec.decode(parser); map.put(fieldName, value); } - - return (L) map; + + @SuppressWarnings("unchecked") + L typed = (L) map; + return typed; } @Override diff --git a/src/main/java/com/fauna/codec/codecs/PageCodec.java b/src/main/java/com/fauna/codec/codecs/PageCodec.java index c99c0504..582954ae 100644 --- a/src/main/java/com/fauna/codec/codecs/PageCodec.java +++ b/src/main/java/com/fauna/codec/codecs/PageCodec.java @@ -38,10 +38,9 @@ public L decode(UTF8FaunaParser parser) throws IOException { public void encode(UTF8FaunaGenerator gen, L obj) throws IOException { if (obj == null) { gen.writeNullValue(); - return; + } else { + throw new ClientException(this.unsupportedTypeMessage(obj.getClass())); } - - throw new ClientException(this.unsupportedTypeMessage(obj.getClass())); } @Override @@ -50,7 +49,6 @@ public Class getCodecClass() { } - @SuppressWarnings("unchecked") private L decodePage(UTF8FaunaParser parser, FaunaTokenType endToken) throws IOException { List data = null; String after = null; @@ -73,12 +71,15 @@ private L decodePage(UTF8FaunaParser parser, FaunaTokenType endToken) throws IOE throw new ClientException("No page data found while deserializing into Page<>"); } - return (L) new Page<>(data, after); + @SuppressWarnings("unchecked") + L res = (L) new Page<>(data, after); + return res; } - @SuppressWarnings("unchecked") private L wrapDocumentInPage(UTF8FaunaParser parser) throws IOException { E elem = this.elementCodec.decode(parser); - return (L) new Page<>(List.of(elem), null); + @SuppressWarnings("unchecked") + L res = (L) new Page<>(List.of(elem), null); + return res; } } From f06f98063f010600506d6772b06c046fe82026cc Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Tue, 13 Aug 2024 15:19:03 +0200 Subject: [PATCH 03/88] Encapsulate document handling/building --- .../com/fauna/codec/codecs/ClassCodec.java | 7 +- .../com/fauna/codec/codecs/DynamicCodec.java | 96 ++---------- .../fauna/codec/codecs/InternalDocument.java | 139 ++++++++++++++++++ .../com/fauna/codec/codecs/InternalRef.java | 93 ------------ .../fauna/codec/codecs/ClassCodecTest.java | 20 +++ .../fauna/codec/codecs/DynamicCodecTest.java | 66 +++++++++ 6 files changed, 240 insertions(+), 181 deletions(-) create mode 100644 src/main/java/com/fauna/codec/codecs/InternalDocument.java delete mode 100644 src/main/java/com/fauna/codec/codecs/InternalRef.java create mode 100644 src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java diff --git a/src/main/java/com/fauna/codec/codecs/ClassCodec.java b/src/main/java/com/fauna/codec/codecs/ClassCodec.java index 691f426e..fa64689c 100644 --- a/src/main/java/com/fauna/codec/codecs/ClassCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ClassCodec.java @@ -128,7 +128,7 @@ private Object createInstance() throws InvocationTargetException, InstantiationE private void setFields(Object instance, UTF8FaunaParser parser, FaunaTokenType endToken) throws IOException, IllegalAccessException { - InternalRef.Builder refBuilder = new InternalRef.Builder(); + InternalDocument.Builder builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != endToken) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { @@ -139,7 +139,7 @@ private void setFields(Object instance, UTF8FaunaParser parser, parser.read(); if (endToken == FaunaTokenType.END_REF) { - refBuilder = refBuilder.withField(fieldName, parser); + builder = builder.withRefField(fieldName, parser); } if (fieldName.equals(ID_FIELD)) { @@ -151,7 +151,8 @@ private void setFields(Object instance, UTF8FaunaParser parser, } } - refBuilder.build().throwIfNotExists(); + // Throws if it does not exist, otherwise no-op. + builder.build(); } private void trySetId(String fieldName, Object instance, UTF8FaunaParser parser) throws IllegalAccessException { diff --git a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java index 3de4af7e..b6010d75 100644 --- a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java +++ b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java @@ -80,11 +80,8 @@ public Class getCodecClass() { private Object decodeDocument(UTF8FaunaParser parser) throws IOException { - Map data = new HashMap<>(); - String id = null; - String name = null; - Instant ts = null; - Module coll = null; + + var builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_DOCUMENT) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { @@ -95,58 +92,27 @@ private Object decodeDocument(UTF8FaunaParser parser) parser.read(); switch (fieldName) { case "id": - id = parser.getValueAsString(); - break; case "name": - name = parser.getValueAsString(); - break; case "ts": - ts = parser.getValueAsTime(); - break; case "coll": - coll = parser.getValueAsModule(); + builder = builder.withDocField(fieldName, parser); break; default: var v = this.decode(parser); - if (v != null) data.put(fieldName, v); + if (v != null) { + builder = builder.withDataField(fieldName, v); + } break; } } - if (id != null && coll != null && ts != null) { - if (name != null) { - data.put("name", name); - } - return new Document(id, coll, ts, data); - } - - if (name != null && coll != null && ts != null) { - return new NamedDocument(name, coll, ts, data); - } - - if (id != null) { - data.put("id", id); - } - if (name != null) { - data.put("name", name); - } - if (coll != null) { - data.put("coll", coll); - } - if (ts != null) { - data.put("ts", ts); - } - return data; + return builder.build(); } private Object decodeRef(UTF8FaunaParser parser) throws IOException { - String id = null; - String name = null; - Module coll = null; - boolean exists = true; - String cause = null; - Map allProps = new HashMap<>(); + + var builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_REF) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { @@ -155,49 +121,9 @@ private Object decodeRef(UTF8FaunaParser parser) String fieldName = parser.getValueAsString(); parser.read(); - switch (fieldName) { - case "id": - id = parser.getValueAsString(); - allProps.put("id", id); - break; - case "name": - name = parser.getValueAsString(); - allProps.put("name", name); - break; - case "coll": - coll = parser.getValueAsModule(); - allProps.put("coll", coll); - break; - case "exists": - exists = parser.getValueAsBoolean(); - allProps.put("exists", exists); - break; - case "cause": - cause = parser.getValueAsString(); - allProps.put("cause", cause); - break; - default: - allProps.put(fieldName, this.decode(parser)); - break; - } - } - - if (id != null && coll != null) { - if (exists) { - return new DocumentRef(id, coll); - } - - throw new NullDocumentException(id, coll, cause); - } - - if (name != null && coll != null) { - if (exists) { - return new NamedDocumentRef(name, coll); - } - - throw new NullDocumentException(name, coll, cause); + builder = builder.withRefField(fieldName, parser); } - return allProps; + return builder.build(); } } diff --git a/src/main/java/com/fauna/codec/codecs/InternalDocument.java b/src/main/java/com/fauna/codec/codecs/InternalDocument.java new file mode 100644 index 00000000..6864660a --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/InternalDocument.java @@ -0,0 +1,139 @@ +package com.fauna.codec.codecs; + +import com.fauna.enums.FaunaTokenType; +import com.fauna.exception.NullDocumentException; +import com.fauna.serialization.UTF8FaunaParser; +import com.fauna.types.*; +import com.fauna.types.Module; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +class InternalDocument { + + static class Builder { + + private String id = null; + private String name = null; + private Module coll = null; + private Boolean exists = null; + private String cause = null; + private Instant ts = null; + private Map data = new HashMap<>(); + private boolean throwIfNotExists = true; + + InternalDocument.Builder withDataField(String key, Object value) { + data.put(key, value); + return this; + } + + InternalDocument.Builder withDocField(String fieldName, UTF8FaunaParser parser) { + switch (fieldName) { + case "id": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.id = parser.getValueAsString(); + } + break; + case "name": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.name = parser.getValueAsString(); + } + break; + case "coll": + if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { + this.coll = parser.getValueAsModule(); + } + break; + case "ts": + if (parser.getCurrentTokenType() == FaunaTokenType.TIME) { + this.ts = parser.getValueAsTime(); + } + } + return this; + } + + InternalDocument.Builder withRefField(String fieldName, UTF8FaunaParser parser) { + switch (fieldName) { + case "id": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.id = parser.getValueAsString(); + } + break; + case "name": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.name = parser.getValueAsString(); + } + break; + case "coll": + if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { + this.coll = parser.getValueAsModule(); + } + break; + case "exists": + if (parser.getCurrentTokenType() == FaunaTokenType.FALSE || parser.getCurrentTokenType() == FaunaTokenType.TRUE) { + this.exists = parser.getValueAsBoolean(); + } + break; + case "cause": + if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { + this.cause = parser.getValueAsString(); + } + break; + } + return this; + } + + Object build() { + if (exists != null && !exists && throwIfNotExists) { + throw new NullDocumentException(id != null ? id : name, coll, cause); + } + + if (id != null && coll != null && ts != null) { + if (name != null) { + data.put("name", name); + } + return new Document(id, coll, ts, data); + } + + if (id != null && coll != null) { + return new DocumentRef(id, coll); + } + + if (name != null && coll != null && ts != null) { + return new NamedDocument(name, coll, ts, data); + } + + if (name != null && coll != null) { + return new NamedDocumentRef(name, coll); + } + + // We got something we don't know how to handle, so just return it. + if (id != null) { + data.put("id", id); + } + + if (name != null) { + data.put("name", name); + } + + if (coll != null) { + data.put("coll", coll); + } + + if (ts != null) { + data.put("ts", ts); + } + + if (exists != null) { + data.put("exists", exists); + } + + if (cause != null) { + data.put("cause", cause); + } + + return data; + } + } +} diff --git a/src/main/java/com/fauna/codec/codecs/InternalRef.java b/src/main/java/com/fauna/codec/codecs/InternalRef.java deleted file mode 100644 index 4ece59d5..00000000 --- a/src/main/java/com/fauna/codec/codecs/InternalRef.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.client.ExponentialBackoffStrategy; -import com.fauna.enums.FaunaTokenType; -import com.fauna.exception.NullDocumentException; -import com.fauna.serialization.UTF8FaunaParser; -import com.fauna.types.Module; - -class InternalRef { - - private final String id; - private final String name; - private final Module coll; - private final boolean exists; - private final String cause; - - public InternalRef(String id, String name, Module coll, boolean exists, String cause){ - this.id = id; - this.name = name; - this.coll = coll; - this.exists = exists; - this.cause = cause; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public Module getColl() { - return coll; - } - - public boolean getExists() { - return exists; - } - - public String getCause() { - return cause; - } - - public void throwIfNotExists() { - if (!getExists()) { - throw new NullDocumentException(getId() != null ? getId() : getName(), getColl(), getCause()); - } - } - - static class Builder { - private String id = null; - private String name = null; - private Module coll = null; - private boolean exists = true; - private String cause = null; - - InternalRef.Builder withField(String fieldName, UTF8FaunaParser parser) { - switch (fieldName) { - case "id": - if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { - this.id = parser.getValueAsString(); - } - break; - case "name": - if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { - this.name = parser.getValueAsString(); - } - break; - case "coll": - if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { - this.coll = parser.getValueAsModule(); - } - break; - case "exists": - if (parser.getCurrentTokenType() == FaunaTokenType.FALSE) { - this.exists = parser.getValueAsBoolean(); - } - break; - case "cause": - if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { - this.cause = parser.getValueAsString(); - } - break; - } - return this; - } - - InternalRef build() { - return new InternalRef(this.id, this.name, this.coll, this.exists, this.cause); - } - } -} diff --git a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java index 8890f031..d355174d 100644 --- a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java +++ b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java @@ -1,12 +1,15 @@ package com.fauna.codec.codecs; import com.fauna.beans.ClassWithParameterizedFields; +import com.fauna.beans.PersonWithAttributes; import com.fauna.codec.*; +import com.fauna.exception.NullDocumentException; import org.junit.jupiter.api.Test; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ClassCodecTest { @@ -21,4 +24,21 @@ public void roundtrip_classWithParameterizedFields() throws IOException { var encoded = Helpers.encode(codec, decoded); assertEquals(wire, encoded); } + + @Test + public void decode_docAsClass() throws IOException { + var wire = "{\"@doc\":{\"id\":\"123\",\"coll\":\"Foo\",\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + var codec = cp.get(PersonWithAttributes.class); + PersonWithAttributes decoded = Helpers.decode(codec, wire); + assertEquals("foo", decoded.getFirstName()); + assertEquals("bar", decoded.getLastName()); + } + + @Test + public void decode_docAsClassThrowsIfNotExists() throws IOException { + var wire = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + var codec = cp.get(PersonWithAttributes.class); + var ex = assertThrows(NullDocumentException.class, () -> Helpers.decode(codec, wire)); + assertEquals("Document 123 in collection Foo is null: not found", ex.getMessage()); + } } diff --git a/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java b/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java new file mode 100644 index 00000000..7b3d0518 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java @@ -0,0 +1,66 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import com.fauna.exception.NullDocumentException; +import com.fauna.types.*; +import com.fauna.types.Module; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DynamicCodecTest { + + CodecRegistry cr = new DefaultCodecRegistry(); + CodecProvider cp = new DefaultCodecProvider(cr); + + @Test + public void decode_doc() throws IOException { + var wire = "{\"@doc\":{\"id\":\"123\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + var codec = cp.get(Object.class); + Document decoded = (Document) Helpers.decode(codec, wire); + assertEquals("123", decoded.getId()); + assertEquals(new Module("Foo"), decoded.getCollection()); + assertEquals("foo", decoded.get("first_name")); + assertEquals("bar", decoded.get("last_name")); + } + + @Test + public void decode_docRef() throws IOException { + var wire = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + var codec = cp.get(Object.class); + DocumentRef decoded = (DocumentRef) Helpers.decode(codec, wire); + assertEquals("123", decoded.getId()); + assertEquals(new Module("Foo"), decoded.getCollection()); + } + + @Test + public void decode_namedDoc() throws IOException { + var wire = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + var codec = cp.get(Object.class); + NamedDocument decoded = (NamedDocument) Helpers.decode(codec, wire); + assertEquals("Boogles", decoded.getName()); + assertEquals(new Module("Foo"), decoded.getCollection()); + assertEquals("foo", decoded.get("first_name")); + assertEquals("bar", decoded.get("last_name")); + } + + @Test + public void decode_namedDocRef() throws IOException { + var wire = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + var codec = cp.get(Object.class); + NamedDocumentRef decoded = (NamedDocumentRef) Helpers.decode(codec, wire); + assertEquals("Boogles", decoded.getName()); + assertEquals(new Module("Foo"), decoded.getCollection()); + } + + @Test + public void decode_docThrowsIfNotExists() throws IOException { + var wire = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + var codec = cp.get(Object.class); + var ex = assertThrows(NullDocumentException.class, () -> Helpers.decode(codec, wire)); + assertEquals("Document 123 in collection Foo is null: not found", ex.getMessage()); + } +} From 544e7683f6c6c0dead86341af2957dc9ec2cbdeb Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Thu, 15 Aug 2024 17:02:02 +0200 Subject: [PATCH 04/88] Add a number of new codecs plus fixes --- .../com/fauna/codec/DefaultCodecProvider.java | 18 ++- .../com/fauna/codec/DefaultCodecRegistry.java | 37 ++++- .../com/fauna/codec/codecs/BaseCodec.java | 4 +- .../com/fauna/codec/codecs/BoolCodec.java | 39 +++++ .../fauna/codec/codecs/ByteArrayCodec.java | 39 +++++ .../com/fauna/codec/codecs/ByteCodec.java | 38 +++++ .../com/fauna/codec/codecs/CharCodec.java | 38 +++++ .../com/fauna/codec/codecs/ClassCodec.java | 15 +- .../com/fauna/codec/codecs/DoubleCodec.java | 40 +++++ .../com/fauna/codec/codecs/DynamicCodec.java | 8 +- .../com/fauna/codec/codecs/FloatCodec.java | 40 +++++ .../fauna/codec/codecs/InternalDocument.java | 2 +- .../com/fauna/codec/codecs/ListCodec.java | 26 ++-- .../com/fauna/codec/codecs/LongCodec.java | 39 +++++ .../java/com/fauna/codec/codecs/MapCodec.java | 36 +++-- .../com/fauna/codec/codecs/OptionalCodec.java | 46 ++++++ .../com/fauna/codec/codecs/PageCodec.java | 2 + .../com/fauna/codec/codecs/ShortCodec.java | 38 +++++ .../com/fauna/codec/codecs/StringCodec.java | 2 + .../java/com/fauna/enums/FaunaTokenType.java | 1 + .../serialization/UTF8FaunaGenerator.java | 23 ++- .../fauna/serialization/UTF8FaunaParser.java | 46 +++--- .../java/com/fauna/types/BaseDocument.java | 2 +- src/main/java/com/fauna/types/Document.java | 24 ++- .../java/com/fauna/types/DocumentRef.java | 23 ++- .../java/com/fauna/types/NamedDocument.java | 22 +++ .../com/fauna/types/NamedDocumentRef.java | 21 +++ src/main/java/com/fauna/types/Page.java | 20 +++ .../beans/ClassWithParameterizedFields.java | 21 +++ .../fauna/beans/ClassWithRefTagCollision.java | 33 +++++ .../com/fauna/beans/PersonWithAttributes.java | 21 +++ .../fauna/codec/codecs/ClassCodecTest.java | 44 ------ .../com/fauna/codec/codecs/CodecTests.java | 138 ++++++++++++++++++ .../fauna/codec/codecs/DynamicCodecTest.java | 66 --------- .../java/com/fauna/codec/codecs/Fixtures.java | 93 ++++++++++++ .../com/fauna/codec/codecs/IntCodecTest.java | 23 --- .../com/fauna/codec/codecs/ListCodecTest.java | 26 ---- .../com/fauna/codec/codecs/MapCodecTest.java | 27 ---- .../fauna/codec/codecs/StringCodecTest.java | 24 --- .../serialization/UTF8FaunaParserTest.java | 26 +++- 40 files changed, 938 insertions(+), 293 deletions(-) create mode 100644 src/main/java/com/fauna/codec/codecs/BoolCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/ByteCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/CharCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/DoubleCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/FloatCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/LongCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/OptionalCodec.java create mode 100644 src/main/java/com/fauna/codec/codecs/ShortCodec.java create mode 100644 src/test/java/com/fauna/beans/ClassWithRefTagCollision.java delete mode 100644 src/test/java/com/fauna/codec/codecs/ClassCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/CodecTests.java delete mode 100644 src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java create mode 100644 src/test/java/com/fauna/codec/codecs/Fixtures.java delete mode 100644 src/test/java/com/fauna/codec/codecs/IntCodecTest.java delete mode 100644 src/test/java/com/fauna/codec/codecs/ListCodecTest.java delete mode 100644 src/test/java/com/fauna/codec/codecs/MapCodecTest.java delete mode 100644 src/test/java/com/fauna/codec/codecs/StringCodecTest.java diff --git a/src/main/java/com/fauna/codec/DefaultCodecProvider.java b/src/main/java/com/fauna/codec/DefaultCodecProvider.java index 197ee7f4..7bf1505a 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecProvider.java +++ b/src/main/java/com/fauna/codec/DefaultCodecProvider.java @@ -1,20 +1,20 @@ package com.fauna.codec; import com.fauna.codec.codecs.*; +import com.fauna.types.Page; import java.util.List; import java.util.Map; +import java.util.Optional; public class DefaultCodecProvider implements CodecProvider { - private final CodecRegistry registry; + public static final CodecProvider SINGLETON = new DefaultCodecProvider(DefaultCodecRegistry.SINGLETON); + public DefaultCodecProvider(CodecRegistry registry) { var dynamic = new DynamicCodec(this); - registry.put(CodecRegistryKey.from(String.class), StringCodec.singleton); - registry.put(CodecRegistryKey.from(Integer.class), IntCodec.singleton); - registry.put(CodecRegistryKey.from(int.class), IntCodec.singleton); registry.put(CodecRegistryKey.from(Object.class), dynamic); this.registry = registry; } @@ -50,6 +50,16 @@ private Codec generate(Class clazz, Class typeArg) { return (Codec) new MapCodec>(valueCodec); } + if (clazz == Optional.class) { + Codec valueCodec = this.get(ta, null); + return (Codec) new OptionalCodec>(valueCodec); + } + + if (clazz == Page.class) { + Codec valueCodec = this.get(ta, null); + return (Codec) new PageCodec>(valueCodec); + } + return new ClassCodec<>(clazz, this); } } diff --git a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java index ef0c26f6..24455c54 100644 --- a/src/main/java/com/fauna/codec/DefaultCodecRegistry.java +++ b/src/main/java/com/fauna/codec/DefaultCodecRegistry.java @@ -1,11 +1,44 @@ package com.fauna.codec; +import com.fauna.codec.codecs.*; + import java.util.concurrent.ConcurrentHashMap; public class DefaultCodecRegistry implements CodecRegistry { - private static final ConcurrentHashMap> codecs = new ConcurrentHashMap<>(); - public DefaultCodecRegistry() {} + public static final CodecRegistry SINGLETON = new DefaultCodecRegistry(); + private final ConcurrentHashMap> codecs; + + public DefaultCodecRegistry() { + codecs = new ConcurrentHashMap<>(); + codecs.put(CodecRegistryKey.from(String.class), StringCodec.singleton); + + codecs.put(CodecRegistryKey.from(byte[].class), ByteArrayCodec.singleton); + + codecs.put(CodecRegistryKey.from(boolean.class), BoolCodec.singleton); + codecs.put(CodecRegistryKey.from(Boolean.class), BoolCodec.singleton); + + codecs.put(CodecRegistryKey.from(char.class), CharCodec.singleton); + codecs.put(CodecRegistryKey.from(Character.class), CharCodec.singleton); + + codecs.put(CodecRegistryKey.from(byte.class), ByteCodec.singleton); + codecs.put(CodecRegistryKey.from(Byte.class), ByteCodec.singleton); + + codecs.put(CodecRegistryKey.from(short.class), ShortCodec.singleton); + codecs.put(CodecRegistryKey.from(Short.class), ShortCodec.singleton); + + codecs.put(CodecRegistryKey.from(Integer.class), IntCodec.singleton); + codecs.put(CodecRegistryKey.from(int.class), IntCodec.singleton); + + codecs.put(CodecRegistryKey.from(Long.class), LongCodec.singleton); + codecs.put(CodecRegistryKey.from(long.class), LongCodec.singleton); + + codecs.put(CodecRegistryKey.from(Float.class), FloatCodec.singleton); + codecs.put(CodecRegistryKey.from(float.class), FloatCodec.singleton); + + codecs.put(CodecRegistryKey.from(Double.class), DoubleCodec.singleton); + codecs.put(CodecRegistryKey.from(double.class), DoubleCodec.singleton); + } @Override public Codec get(CodecRegistryKey key) { diff --git a/src/main/java/com/fauna/codec/codecs/BaseCodec.java b/src/main/java/com/fauna/codec/codecs/BaseCodec.java index 9eba2e41..5a4210b7 100644 --- a/src/main/java/com/fauna/codec/codecs/BaseCodec.java +++ b/src/main/java/com/fauna/codec/codecs/BaseCodec.java @@ -11,8 +11,8 @@ public abstract class BaseCodec implements Codec { - protected Set TAGS = new HashSet<>(Arrays.asList( - "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", "@doc", "@set", "@object" + public static Set TAGS = new HashSet<>(Arrays.asList( + "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", "@doc", "@set", "@object", "@bytes" )); protected String unexpectedTokenExceptionMessage(FaunaTokenType token) { diff --git a/src/main/java/com/fauna/codec/codecs/BoolCodec.java b/src/main/java/com/fauna/codec/codecs/BoolCodec.java new file mode 100644 index 00000000..9cbfa37f --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/BoolCodec.java @@ -0,0 +1,39 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class BoolCodec extends BaseCodec { + + public static final BoolCodec singleton = new BoolCodec(); + + @Override + public Boolean decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case TRUE: + case FALSE: + return parser.getValueAsBoolean(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Boolean obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeBooleanValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Boolean.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java b/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java new file mode 100644 index 00000000..57b296c5 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java @@ -0,0 +1,39 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class ByteArrayCodec extends BaseCodec { + + public static final ByteArrayCodec singleton = new ByteArrayCodec(); + + @Override + public byte[] decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case BYTES: + return parser.getValueAsByteArray(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, byte[] obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + return; + } + + gen.writeBytesValue(obj); + } + + @Override + public Class getCodecClass() { + return byte[].class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/ByteCodec.java b/src/main/java/com/fauna/codec/codecs/ByteCodec.java new file mode 100644 index 00000000..1d764893 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/ByteCodec.java @@ -0,0 +1,38 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class ByteCodec extends BaseCodec { + + public static final ByteCodec singleton = new ByteCodec(); + + @Override + public Byte decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + return parser.getValueAsByte(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Byte obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeIntValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Byte.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/CharCodec.java b/src/main/java/com/fauna/codec/codecs/CharCodec.java new file mode 100644 index 00000000..d12137a4 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/CharCodec.java @@ -0,0 +1,38 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class CharCodec extends BaseCodec { + + public static final CharCodec singleton = new CharCodec(); + + @Override + public Character decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + return parser.getValueAsCharacter(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Character obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeCharValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Character.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/ClassCodec.java b/src/main/java/com/fauna/codec/codecs/ClassCodec.java index fa64689c..70e49123 100644 --- a/src/main/java/com/fauna/codec/codecs/ClassCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ClassCodec.java @@ -25,17 +25,14 @@ public class ClassCodec extends BaseCodec { private static final String ID_FIELD = "id"; private static final String NAME_FIELD = "name"; - private static final String COLL_FIELD = "coll"; - private static final String EXISTS_FIELD = "exists"; - private static final String CAUSE_FIELD = "cause"; - private final Type type; + private final Class type; private final List fields; private final Map fieldsByName; private final boolean shouldEscapeObject; - public ClassCodec(Type ty, CodecProvider provider) { + public ClassCodec(Class ty, CodecProvider provider) { this.type = ty; List fieldsList = new ArrayList<>(); @@ -76,10 +73,8 @@ public T decode(UTF8FaunaParser parser) { @SuppressWarnings("unchecked") T typed = (T) instance; return typed; - } catch (IOException exc) { - throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } catch (IllegalAccessException | ClassNotFoundException | InvocationTargetException | InstantiationException | - NoSuchMethodException e) { + NoSuchMethodException | IOException e) { throw new RuntimeException(e); } } @@ -115,8 +110,8 @@ public void encode(UTF8FaunaGenerator gen, T obj) throws IOException { } @Override - public Class getCodecClass() { - return null; + public Class getCodecClass() { + return this.type; } private Object createInstance() throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException { diff --git a/src/main/java/com/fauna/codec/codecs/DoubleCodec.java b/src/main/java/com/fauna/codec/codecs/DoubleCodec.java new file mode 100644 index 00000000..eeca2d23 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/DoubleCodec.java @@ -0,0 +1,40 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class DoubleCodec extends BaseCodec { + + public static final DoubleCodec singleton = new DoubleCodec(); + + @Override + public Double decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + case LONG: + case DOUBLE: + return parser.getValueAsDouble(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Double obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeDoubleValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Double.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java index b6010d75..77ebfa4b 100644 --- a/src/main/java/com/fauna/codec/codecs/DynamicCodec.java +++ b/src/main/java/com/fauna/codec/codecs/DynamicCodec.java @@ -4,14 +4,10 @@ import com.fauna.codec.CodecProvider; import com.fauna.enums.FaunaTokenType; import com.fauna.exception.ClientException; -import com.fauna.exception.NullDocumentException; import com.fauna.serialization.*; import com.fauna.types.*; -import com.fauna.types.Module; import java.io.IOException; -import java.time.Instant; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -74,8 +70,8 @@ public void encode(UTF8FaunaGenerator gen, Object obj) throws IOException { } @Override - public Class getCodecClass() { - return null; + public Class getCodecClass() { + return Object.class; } private Object decodeDocument(UTF8FaunaParser parser) diff --git a/src/main/java/com/fauna/codec/codecs/FloatCodec.java b/src/main/java/com/fauna/codec/codecs/FloatCodec.java new file mode 100644 index 00000000..ff9f55f4 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/FloatCodec.java @@ -0,0 +1,40 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class FloatCodec extends BaseCodec { + + public static final FloatCodec singleton = new FloatCodec(); + + @Override + public Float decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + case LONG: + case DOUBLE: + return parser.getValueAsFloat(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Float obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeDoubleValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Float.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/InternalDocument.java b/src/main/java/com/fauna/codec/codecs/InternalDocument.java index 6864660a..a36a45c0 100644 --- a/src/main/java/com/fauna/codec/codecs/InternalDocument.java +++ b/src/main/java/com/fauna/codec/codecs/InternalDocument.java @@ -20,7 +20,7 @@ static class Builder { private Boolean exists = null; private String cause = null; private Instant ts = null; - private Map data = new HashMap<>(); + private final Map data = new HashMap<>(); private boolean throwIfNotExists = true; InternalDocument.Builder withDataField(String key, Object value) { diff --git a/src/main/java/com/fauna/codec/codecs/ListCodec.java b/src/main/java/com/fauna/codec/codecs/ListCodec.java index eba3ada8..511e652a 100644 --- a/src/main/java/com/fauna/codec/codecs/ListCodec.java +++ b/src/main/java/com/fauna/codec/codecs/ListCodec.java @@ -20,18 +20,22 @@ public ListCodec(Codec elementCodec) { @Override public L decode(UTF8FaunaParser parser) throws IOException { - if (parser.getCurrentTokenType() != FaunaTokenType.START_ARRAY) { - throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case START_ARRAY: + List list = new ArrayList<>(); + + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_ARRAY) { + E value = elementCodec.decode(parser); + list.add(value); + } + @SuppressWarnings("unchecked") + var typed = (L) list; + return typed; + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } - - List list = new ArrayList<>(); - - while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_ARRAY) { - E value = elementCodec.decode(parser); - list.add(value); - } - - return (L) list; } @Override diff --git a/src/main/java/com/fauna/codec/codecs/LongCodec.java b/src/main/java/com/fauna/codec/codecs/LongCodec.java new file mode 100644 index 00000000..81de2b5f --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/LongCodec.java @@ -0,0 +1,39 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class LongCodec extends BaseCodec { + + public static final LongCodec singleton = new LongCodec(); + + @Override + public Long decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + case LONG: + return parser.getValueAsLong(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Long obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeLongValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Long.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/MapCodec.java b/src/main/java/com/fauna/codec/codecs/MapCodec.java index 3eaaae62..92c3ae57 100644 --- a/src/main/java/com/fauna/codec/codecs/MapCodec.java +++ b/src/main/java/com/fauna/codec/codecs/MapCodec.java @@ -7,6 +7,7 @@ import com.fauna.serialization.UTF8FaunaParser; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,26 +22,29 @@ public MapCodec(Codec valueCodec) { @Override public L decode(UTF8FaunaParser parser) throws IOException { - if (parser.getCurrentTokenType() != FaunaTokenType.START_OBJECT) { - throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); - } + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case START_OBJECT: + Map map = new HashMap<>(); - Map map = new HashMap<>(); + while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_OBJECT) { + if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { + throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } - while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_OBJECT) { - if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { - throw new ClientException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); - } + String fieldName = parser.getValueAsString(); + parser.read(); + V value = valueCodec.decode(parser); + map.put(fieldName, value); + } - String fieldName = parser.getValueAsString(); - parser.read(); - V value = valueCodec.decode(parser); - map.put(fieldName, value); + @SuppressWarnings("unchecked") + L typed = (L) map; + return typed; + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } - - @SuppressWarnings("unchecked") - L typed = (L) map; - return typed; } @Override diff --git a/src/main/java/com/fauna/codec/codecs/OptionalCodec.java b/src/main/java/com/fauna/codec/codecs/OptionalCodec.java new file mode 100644 index 00000000..6b90e443 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/OptionalCodec.java @@ -0,0 +1,46 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.Codec; +import com.fauna.enums.FaunaTokenType; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; +import java.util.Optional; + +public class OptionalCodec> extends BaseCodec { + + private final Codec valueCodec; + + public OptionalCodec(Codec valueCodec) { + this.valueCodec = valueCodec; + } + + @Override + public L decode(UTF8FaunaParser parser) throws IOException { + if (parser.getCurrentTokenType() == FaunaTokenType.NULL) { + @SuppressWarnings("unchecked") + L res = (L) Optional.empty(); + return res; + } + + @SuppressWarnings("unchecked") + L res = (L) Optional.of(valueCodec.decode(parser)); + return res; + } + + @Override + public void encode(UTF8FaunaGenerator gen, L obj) throws IOException { + if (obj == null || obj.isEmpty()) { + gen.writeNullValue(); + return; + } + + valueCodec.encode(gen, obj.get()); + } + + @Override + public Class getCodecClass() { + return Optional.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/PageCodec.java b/src/main/java/com/fauna/codec/codecs/PageCodec.java index 582954ae..e3b3bd68 100644 --- a/src/main/java/com/fauna/codec/codecs/PageCodec.java +++ b/src/main/java/com/fauna/codec/codecs/PageCodec.java @@ -23,6 +23,8 @@ public PageCodec(Codec elementCodec) { @Override public L decode(UTF8FaunaParser parser) throws IOException { switch (parser.getCurrentTokenType()) { + case NULL: + return null; case START_PAGE: return decodePage(parser, FaunaTokenType.END_PAGE); case START_OBJECT: diff --git a/src/main/java/com/fauna/codec/codecs/ShortCodec.java b/src/main/java/com/fauna/codec/codecs/ShortCodec.java new file mode 100644 index 00000000..880bbc91 --- /dev/null +++ b/src/main/java/com/fauna/codec/codecs/ShortCodec.java @@ -0,0 +1,38 @@ +package com.fauna.codec.codecs; + +import com.fauna.exception.ClientException; +import com.fauna.serialization.UTF8FaunaGenerator; +import com.fauna.serialization.UTF8FaunaParser; + +import java.io.IOException; + +public class ShortCodec extends BaseCodec { + + public static final ShortCodec singleton = new ShortCodec(); + + @Override + public Short decode(UTF8FaunaParser parser) throws IOException { + switch (parser.getCurrentTokenType()) { + case NULL: + return null; + case INT: + return parser.getValueAsShort(); + default: + throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); + } + } + + @Override + public void encode(UTF8FaunaGenerator gen, Short obj) throws IOException { + if (obj == null) { + gen.writeNullValue(); + } else { + gen.writeIntValue(obj); + } + } + + @Override + public Class getCodecClass() { + return Short.class; + } +} diff --git a/src/main/java/com/fauna/codec/codecs/StringCodec.java b/src/main/java/com/fauna/codec/codecs/StringCodec.java index 3eebf59f..cdd43d11 100644 --- a/src/main/java/com/fauna/codec/codecs/StringCodec.java +++ b/src/main/java/com/fauna/codec/codecs/StringCodec.java @@ -17,6 +17,8 @@ public String decode(UTF8FaunaParser parser) throws IOException { return null; case STRING: return parser.getValueAsString(); + case BYTES: + return parser.getTaggedValueAsString(); default: throw new ClientException(this.unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } diff --git a/src/main/java/com/fauna/enums/FaunaTokenType.java b/src/main/java/com/fauna/enums/FaunaTokenType.java index 1ee20bcc..930a0dcb 100644 --- a/src/main/java/com/fauna/enums/FaunaTokenType.java +++ b/src/main/java/com/fauna/enums/FaunaTokenType.java @@ -26,6 +26,7 @@ public enum FaunaTokenType { FIELD_NAME, STRING, + BYTES, INT, LONG, diff --git a/src/main/java/com/fauna/serialization/UTF8FaunaGenerator.java b/src/main/java/com/fauna/serialization/UTF8FaunaGenerator.java index 62122ab9..81303102 100644 --- a/src/main/java/com/fauna/serialization/UTF8FaunaGenerator.java +++ b/src/main/java/com/fauna/serialization/UTF8FaunaGenerator.java @@ -271,6 +271,17 @@ public void writeDoubleValue(double value) throws IOException { writeTaggedValue("@double", Double.toString(value)); } + /** + * Writes a float value as a tagged element (@double). + * + * @param value The float value to write as a double. + * @throws IOException If an I/O error occurs. + */ + public void writeDoubleValue(float value) throws IOException { + writeTaggedValue("@double", Float.toString(value)); + } + + /** * Writes an integer value as a tagged element. * @@ -343,7 +354,7 @@ public void writeBooleanValue(boolean value) throws IOException { } public void writeCharValue(Character value) throws IOException { - jsonGenerator.writeString(String.valueOf(value)); + writeIntValue(value); } /** @@ -365,6 +376,16 @@ public void writeModuleValue(Module value) throws IOException { writeTaggedValue("@mod", value.getName()); } + /** + * Writes a byte array encoded as a base64 string as a tagged element. + * + * @param value The byte array to write. + * @throws IOException If an I/O error occurs. + */ + public void writeBytesValue(byte[] value) throws IOException { + writeTaggedValue("@bytes", Base64.getEncoder().encodeToString(value)); + } + @Override public void close() throws IOException { jsonGenerator.close(); diff --git a/src/main/java/com/fauna/serialization/UTF8FaunaParser.java b/src/main/java/com/fauna/serialization/UTF8FaunaParser.java index 9a01d406..4689b6b9 100644 --- a/src/main/java/com/fauna/serialization/UTF8FaunaParser.java +++ b/src/main/java/com/fauna/serialization/UTF8FaunaParser.java @@ -1,16 +1,5 @@ package com.fauna.serialization; -import static com.fauna.enums.FaunaTokenType.DATE; -import static com.fauna.enums.FaunaTokenType.DOUBLE; -import static com.fauna.enums.FaunaTokenType.END_ARRAY; -import static com.fauna.enums.FaunaTokenType.END_DOCUMENT; -import static com.fauna.enums.FaunaTokenType.END_OBJECT; -import static com.fauna.enums.FaunaTokenType.END_PAGE; -import static com.fauna.enums.FaunaTokenType.END_REF; -import static com.fauna.enums.FaunaTokenType.LONG; -import static com.fauna.enums.FaunaTokenType.NONE; -import static com.fauna.enums.FaunaTokenType.TIME; - import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; @@ -24,11 +13,9 @@ import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeParseException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; -import java.util.Stack; +import java.util.*; + +import static com.fauna.enums.FaunaTokenType.*; /** * Represents a reader that provides fast, non-cached, forward-only access to serialized data. @@ -45,6 +32,7 @@ public class UTF8FaunaParser { private static final String MOD_TAG = "@mod"; private static final String SET_TAG = "@set"; private static final String OBJECT_TAG = "@object"; + private static final String BYTES_TAG = "@bytes"; private final JsonParser jsonParser; private final Stack tokenStack = new Stack<>(); @@ -175,6 +163,9 @@ private void handleStartObject() throws IOException { switch (jsonParser.currentToken()) { case FIELD_NAME: switch (jsonParser.getText()) { + case BYTES_TAG: + handleTaggedString(FaunaTokenType.BYTES); + break; case INT_TAG: handleTaggedString(FaunaTokenType.INT); break; @@ -291,7 +282,7 @@ private void validateTaggedTypes(FaunaTokenType... types) { } public Character getValueAsCharacter() { - validateTaggedType(FaunaTokenType.INT); + validateTaggedType(INT); return Character.valueOf((char) Integer.parseInt(taggedTokenValue)); } @@ -303,8 +294,17 @@ public String getValueAsString() { } } + public String getTaggedValueAsString() { + return taggedTokenValue; + } + + public byte[] getValueAsByteArray() { + validateTaggedTypes(BYTES); + return Base64.getDecoder().decode(taggedTokenValue.getBytes()); + } + public Byte getValueAsByte() { - validateTaggedType(FaunaTokenType.INT); + validateTaggedType(INT); try { return Byte.parseByte(taggedTokenValue); } catch (NumberFormatException e) { @@ -313,7 +313,7 @@ public Byte getValueAsByte() { } public Short getValueAsShort() { - validateTaggedType(FaunaTokenType.INT); + validateTaggedType(INT); try { return Short.parseShort(taggedTokenValue); } catch (NumberFormatException e) { @@ -323,7 +323,7 @@ public Short getValueAsShort() { } public Integer getValueAsInt() { - validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); + validateTaggedTypes(INT, FaunaTokenType.LONG); try { return Integer.parseInt(taggedTokenValue); } catch (NumberFormatException e) { @@ -358,7 +358,7 @@ public Instant getValueAsTime() { } public Float getValueAsFloat() { - validateTaggedType(DOUBLE); + validateTaggedTypes(INT, LONG, DOUBLE); try { return Float.parseFloat(taggedTokenValue); } catch (NumberFormatException e) { @@ -367,7 +367,7 @@ public Float getValueAsFloat() { } public Double getValueAsDouble() { - validateTaggedType(DOUBLE); + validateTaggedTypes(INT, LONG, DOUBLE); try { return Double.parseDouble(taggedTokenValue); } catch (NumberFormatException e) { @@ -376,7 +376,7 @@ public Double getValueAsDouble() { } public Long getValueAsLong() { - validateTaggedType(LONG); + validateTaggedTypes(INT, LONG); try { return Long.parseLong(taggedTokenValue); } catch (NumberFormatException e) { diff --git a/src/main/java/com/fauna/types/BaseDocument.java b/src/main/java/com/fauna/types/BaseDocument.java index 161d2b9f..0fd56ed4 100644 --- a/src/main/java/com/fauna/types/BaseDocument.java +++ b/src/main/java/com/fauna/types/BaseDocument.java @@ -11,7 +11,7 @@ */ public class BaseDocument implements Iterable { - private final Hashtable data = new Hashtable<>(); + protected final Hashtable data = new Hashtable<>(); private final Instant ts; private final Module collection; diff --git a/src/main/java/com/fauna/types/Document.java b/src/main/java/com/fauna/types/Document.java index ecc1aeb4..df5facdf 100644 --- a/src/main/java/com/fauna/types/Document.java +++ b/src/main/java/com/fauna/types/Document.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.Map; +import java.util.Objects; /** * Represents a document. @@ -44,4 +45,25 @@ public Document(String id, Module coll, Instant ts, Map data) { public String getId() { return id; } -} \ No newline at end of file + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + Document c = (Document) o; + + return id.equals(c.id) + && getTs().equals(c.getTs()) + && getCollection().equals(c.getCollection()) + && data.equals(c.data); + } + + @Override + public int hashCode() { + return Objects.hash(id, getTs(), getCollection(), data); + } +} diff --git a/src/main/java/com/fauna/types/DocumentRef.java b/src/main/java/com/fauna/types/DocumentRef.java index 46eb662e..f52b484d 100644 --- a/src/main/java/com/fauna/types/DocumentRef.java +++ b/src/main/java/com/fauna/types/DocumentRef.java @@ -1,6 +1,8 @@ package com.fauna.types; +import java.util.Objects; + /** * Represents a document ref. */ @@ -55,4 +57,23 @@ public Module getCollection() { public void setCollection(Module collection) { this.collection = collection; } -} \ No newline at end of file + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + DocumentRef c = (DocumentRef) o; + + return id.equals(c.id) + && getCollection().equals(c.getCollection()); + } + + @Override + public int hashCode() { + return Objects.hash(id, getCollection()); + } +} diff --git a/src/main/java/com/fauna/types/NamedDocument.java b/src/main/java/com/fauna/types/NamedDocument.java index bc93fbc6..8397cba8 100644 --- a/src/main/java/com/fauna/types/NamedDocument.java +++ b/src/main/java/com/fauna/types/NamedDocument.java @@ -2,6 +2,7 @@ import java.time.Instant; import java.util.Map; +import java.util.Objects; /** * Represents a document that has a "name" instead of an "id". For example, a Role document is @@ -48,4 +49,25 @@ public NamedDocument(String name, Module coll, Instant ts, Map d public String getName() { return name; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + NamedDocument c = (NamedDocument) o; + + return name.equals(c.name) + && getTs().equals(c.getTs()) + && getCollection().equals(c.getCollection()) + && data.equals(c.data); + } + + @Override + public int hashCode() { + return Objects.hash(name, getTs(), getCollection(), data); + } } \ No newline at end of file diff --git a/src/main/java/com/fauna/types/NamedDocumentRef.java b/src/main/java/com/fauna/types/NamedDocumentRef.java index eb3e9377..ece15cda 100644 --- a/src/main/java/com/fauna/types/NamedDocumentRef.java +++ b/src/main/java/com/fauna/types/NamedDocumentRef.java @@ -1,5 +1,7 @@ package com.fauna.types; +import java.util.Objects; + /** * Represents a document ref that has a "name" instead of an "id". For example, a Role document * reference is represented as a NamedDocumentRef. @@ -55,4 +57,23 @@ public Module getCollection() { public void setCollection(Module collection) { this.collection = collection; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + NamedDocumentRef c = (NamedDocumentRef) o; + + return name.equals(c.name) + && getCollection().equals(c.getCollection()); + } + + @Override + public int hashCode() { + return Objects.hash(name, getCollection()); + } } diff --git a/src/main/java/com/fauna/types/Page.java b/src/main/java/com/fauna/types/Page.java index d2783b0a..cca12694 100644 --- a/src/main/java/com/fauna/types/Page.java +++ b/src/main/java/com/fauna/types/Page.java @@ -1,6 +1,7 @@ package com.fauna.types; import java.util.List; +import java.util.Objects; /** * Represents a page in a dataset for pagination. @@ -23,4 +24,23 @@ public List data() { public String after() { return after; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + Page c = (Page) o; + + return Objects.equals(after,c.after) + && data.equals(c.data); + } + + @Override + public int hashCode() { + return Objects.hash(after, data); + } } \ No newline at end of file diff --git a/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java index f6cbec09..a4d53f7b 100644 --- a/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java +++ b/src/test/java/com/fauna/beans/ClassWithParameterizedFields.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import java.util.Objects; @FaunaObject public class ClassWithParameterizedFields { @@ -17,4 +18,24 @@ public class ClassWithParameterizedFields { @FaunaField(name = "a_map", typeArgument = Integer.class) public Map map = Map.of("key1", 42); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + ClassWithParameterizedFields c = (ClassWithParameterizedFields) o; + + return list.equals(c.list) + && map.equals(c.map) + && firstName.equals(c.firstName); + } + + @Override + public int hashCode() { + return Objects.hash(list, map, firstName); + } } \ No newline at end of file diff --git a/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java b/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java new file mode 100644 index 00000000..9bf80e81 --- /dev/null +++ b/src/test/java/com/fauna/beans/ClassWithRefTagCollision.java @@ -0,0 +1,33 @@ +package com.fauna.beans; + + +import com.fauna.annotation.FaunaField; +import com.fauna.annotation.FaunaObject; + +import java.util.Objects; + + +@FaunaObject +public class ClassWithRefTagCollision { + + @FaunaField(name = "@ref") + public String field; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + ClassWithRefTagCollision c = (ClassWithRefTagCollision) o; + + return field.equals(c.field); + } + + @Override + public int hashCode() { + return Objects.hash(field); + } +} diff --git a/src/test/java/com/fauna/beans/PersonWithAttributes.java b/src/test/java/com/fauna/beans/PersonWithAttributes.java index 7f824732..ee4ec6cb 100644 --- a/src/test/java/com/fauna/beans/PersonWithAttributes.java +++ b/src/test/java/com/fauna/beans/PersonWithAttributes.java @@ -3,6 +3,8 @@ import com.fauna.annotation.FaunaField; import com.fauna.annotation.FaunaObject; +import java.util.Objects; + @FaunaObject public class PersonWithAttributes { @@ -37,4 +39,23 @@ public Integer getAge() { return age; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null) return false; + + if (getClass() != o.getClass()) return false; + + PersonWithAttributes c = (PersonWithAttributes) o; + + return firstName.equals(c.firstName) + && lastName.equals(c.lastName) + && Objects.equals(age, c.age); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName, age); + } } diff --git a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java b/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java deleted file mode 100644 index d355174d..00000000 --- a/src/test/java/com/fauna/codec/codecs/ClassCodecTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.beans.ClassWithParameterizedFields; -import com.fauna.beans.PersonWithAttributes; -import com.fauna.codec.*; -import com.fauna.exception.NullDocumentException; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class ClassCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - public void roundtrip_classWithParameterizedFields() throws IOException { - var wire = "{\"first_name\":\"foo\",\"a_list\":[\"item1\"],\"a_map\":{\"key1\":{\"@int\":\"42\"}}}"; - var codec = cp.get(ClassWithParameterizedFields.class); - ClassWithParameterizedFields decoded = Helpers.decode(codec, wire); - var encoded = Helpers.encode(codec, decoded); - assertEquals(wire, encoded); - } - - @Test - public void decode_docAsClass() throws IOException { - var wire = "{\"@doc\":{\"id\":\"123\",\"coll\":\"Foo\",\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; - var codec = cp.get(PersonWithAttributes.class); - PersonWithAttributes decoded = Helpers.decode(codec, wire); - assertEquals("foo", decoded.getFirstName()); - assertEquals("bar", decoded.getLastName()); - } - - @Test - public void decode_docAsClassThrowsIfNotExists() throws IOException { - var wire = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - var codec = cp.get(PersonWithAttributes.class); - var ex = assertThrows(NullDocumentException.class, () -> Helpers.decode(codec, wire)); - assertEquals("Document 123 in collection Foo is null: not found", ex.getMessage()); - } -} diff --git a/src/test/java/com/fauna/codec/codecs/CodecTests.java b/src/test/java/com/fauna/codec/codecs/CodecTests.java new file mode 100644 index 00000000..ccaf3678 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/CodecTests.java @@ -0,0 +1,138 @@ +package com.fauna.codec.codecs; + +import com.fauna.codec.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.Set; +import java.util.stream.Stream; + +import static com.fauna.codec.codecs.Fixtures.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CodecTests { + + public enum TestType { + RoundTrip, + Decode, + Encode + } + + private static Stream testArgs() { + return Stream.of( + // StringCodec + Arguments.of(TestType.RoundTrip, STRING_CODEC, STRING_WIRE, null, null), + Arguments.of(TestType.RoundTrip, STRING_CODEC, NULL_WIRE, null, null), + Arguments.of(TestType.Decode, STRING_CODEC, BYTES_WIRE, BASE64_STRING, null), + + // ByteCodec + Arguments.of(TestType.RoundTrip, BYTE_CODEC, MAX_BYTE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, BYTE_CODEC, MIN_BYTE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, BYTE_CODEC, NULL_WIRE, null, null), + + // ShortCodec + Arguments.of(TestType.RoundTrip, SHORT_CODEC, MAX_SHORT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, SHORT_CODEC, MIN_SHORT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, SHORT_CODEC, NULL_WIRE, null, null), + + // IntCodec + Arguments.of(TestType.RoundTrip, INT_CODEC, MAX_INT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, INT_CODEC, MIN_INT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, INT_CODEC, NULL_WIRE, null, null), + + // LongCodec + Arguments.of(TestType.RoundTrip, LONG_CODEC, MAX_LONG_WIRE, null, null), + Arguments.of(TestType.RoundTrip, LONG_CODEC, MIN_LONG_WIRE, null, null), + Arguments.of(TestType.RoundTrip, LONG_CODEC, NULL_WIRE, null, null), + + // FloatCodec + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, MAX_FLOAT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, MIN_FLOAT_WIRE, null, null), + Arguments.of(TestType.RoundTrip, FLOAT_CODEC, NULL_WIRE, null, null), + + // DoubleCodec + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, MAX_DOUBLE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, MIN_DOUBLE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, DOUBLE_CODEC, NULL_WIRE, null, null), + + // CharCodec + Arguments.of(TestType.RoundTrip, LONG_CODEC, MAX_LONG_WIRE, null, null), + Arguments.of(TestType.RoundTrip, CHAR_CODEC, NULL_WIRE, null, null), + + // BoolCodec + Arguments.of(TestType.RoundTrip, BOOL_CODEC, TRUE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, BOOL_CODEC, FALSE_WIRE, null, null), + Arguments.of(TestType.RoundTrip, BOOL_CODEC, NULL_WIRE, null, null), + + // ByteArrayCodec + Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, BYTES_WIRE, null, null), + Arguments.of(TestType.RoundTrip, BYTE_ARRAY_CODEC, NULL_WIRE, null, null), + + // ListCodec + Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, ARRAY_WIRE, null, null), + Arguments.of(TestType.RoundTrip, LIST_INT_CODEC, NULL_WIRE, null, null), + + // MapCodec + Arguments.of(TestType.RoundTrip, MAP_INT_CODEC, OBJECT_WIRE, null, null), + + // ClassCodec + Arguments.of(TestType.RoundTrip, CLASS_WITH_PARAMETERIZED_FIELDS_CODEC, CLASS_WITH_PARAMETERIZED_FIELDS_WIRE, null, null), + Arguments.of(TestType.RoundTrip, CLASS_WITH_REF_TAG_COLLISION_CODEC, ESCAPED_OBJECT_WITH_WIRE("@ref"), null, null), + Arguments.of(TestType.Decode, PERSON_WITH_ATTRIBUTES_CODEC, DOCUMENT_WIRE, PERSON_WITH_ATTRIBUTES, null), + Arguments.of(TestType.Decode, PERSON_WITH_ATTRIBUTES_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION), + + // OptionalCodec + Arguments.of(TestType.RoundTrip, OPTIONAL_INT_CODEC, NULL_WIRE, null, null), + Arguments.of(TestType.RoundTrip, OPTIONAL_STRING_CODEC, STRING_WIRE, null, null), + + // PageCodec + Arguments.of(TestType.RoundTrip, PAGE_CODEC, NULL_WIRE, null, null), + Arguments.of(TestType.Decode, PAGE_CODEC, PAGE_WIRE, PAGE_DOCUMENT, null), + Arguments.of(TestType.Decode, PAGE_CODEC, DOCUMENT_WIRE, PAGE_DOCUMENT, null), + + // DynamicCodec + Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_WIRE, DOCUMENT, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, DOCUMENT_REF_WIRE, DOCUMENT_REF, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, NAMED_DOCUMENT_WIRE, NAMED_DOCUMENT, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, NAMED_DOCUMENT_REF_WIRE, NAMED_DOCUMENT_REF, null), + Arguments.of(TestType.Decode, DYNAMIC_CODEC, NULL_DOC_WIRE, null, NULL_DOC_EXCEPTION) + + ); + } + + @ParameterizedTest(name = "{index} {0}:{1}:{2}:{3}:{4}") + @MethodSource("testArgs") + public void all_codecs(TestType testType, Codec codec, String wire, Object obj, E exception) throws IOException { + switch (testType) { + case RoundTrip: + var decodeRoundTrip = Helpers.decode(codec, wire); + var encodeRoundTrip = Helpers.encode(codec, decodeRoundTrip); + assertEquals(wire, encodeRoundTrip); + break; + case Decode: + if (exception != null) { + var ex = assertThrows(exception.getClass(), () -> { + Helpers.decode(codec, wire); + }); + + assertEquals(exception.getMessage(), ex.getMessage()); + } else { + var decoded = Helpers.decode(codec, wire); + assertEquals(obj, decoded); + } + } + } + + private static Set tags() { + return BaseCodec.TAGS; + } + + @ParameterizedTest + @MethodSource("tags") + public void map_escapeOnReservedKey(String tag) throws IOException { + all_codecs(TestType.RoundTrip, MAP_STRING_CODEC, ESCAPED_OBJECT_WITH_WIRE(tag), null, null); + } +} diff --git a/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java b/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java deleted file mode 100644 index 7b3d0518..00000000 --- a/src/test/java/com/fauna/codec/codecs/DynamicCodecTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.*; -import com.fauna.exception.NullDocumentException; -import com.fauna.types.*; -import com.fauna.types.Module; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class DynamicCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - public void decode_doc() throws IOException { - var wire = "{\"@doc\":{\"id\":\"123\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; - var codec = cp.get(Object.class); - Document decoded = (Document) Helpers.decode(codec, wire); - assertEquals("123", decoded.getId()); - assertEquals(new Module("Foo"), decoded.getCollection()); - assertEquals("foo", decoded.get("first_name")); - assertEquals("bar", decoded.get("last_name")); - } - - @Test - public void decode_docRef() throws IOException { - var wire = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; - var codec = cp.get(Object.class); - DocumentRef decoded = (DocumentRef) Helpers.decode(codec, wire); - assertEquals("123", decoded.getId()); - assertEquals(new Module("Foo"), decoded.getCollection()); - } - - @Test - public void decode_namedDoc() throws IOException { - var wire = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; - var codec = cp.get(Object.class); - NamedDocument decoded = (NamedDocument) Helpers.decode(codec, wire); - assertEquals("Boogles", decoded.getName()); - assertEquals(new Module("Foo"), decoded.getCollection()); - assertEquals("foo", decoded.get("first_name")); - assertEquals("bar", decoded.get("last_name")); - } - - @Test - public void decode_namedDocRef() throws IOException { - var wire = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; - var codec = cp.get(Object.class); - NamedDocumentRef decoded = (NamedDocumentRef) Helpers.decode(codec, wire); - assertEquals("Boogles", decoded.getName()); - assertEquals(new Module("Foo"), decoded.getCollection()); - } - - @Test - public void decode_docThrowsIfNotExists() throws IOException { - var wire = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; - var codec = cp.get(Object.class); - var ex = assertThrows(NullDocumentException.class, () -> Helpers.decode(codec, wire)); - assertEquals("Document 123 in collection Foo is null: not found", ex.getMessage()); - } -} diff --git a/src/test/java/com/fauna/codec/codecs/Fixtures.java b/src/test/java/com/fauna/codec/codecs/Fixtures.java new file mode 100644 index 00000000..3a2e5801 --- /dev/null +++ b/src/test/java/com/fauna/codec/codecs/Fixtures.java @@ -0,0 +1,93 @@ +package com.fauna.codec.codecs; + +import com.fauna.beans.ClassWithParameterizedFields; +import com.fauna.beans.ClassWithRefTagCollision; +import com.fauna.beans.PersonWithAttributes; +import com.fauna.codec.Codec; +import com.fauna.codec.DefaultCodecProvider; +import com.fauna.exception.NullDocumentException; +import com.fauna.types.*; +import com.fauna.types.Module; + +import java.time.Instant; +import java.util.*; + +@SuppressWarnings({"rawtypes","unchecked"}) +public class Fixtures { + + // Wire Strings + public static final String NULL_WIRE = "null"; + public static final String STRING_WIRE = "\"Fauna\""; + public static final String TRUE_WIRE = "true"; + public static final String FALSE_WIRE = "false"; + public static final String BYTES_WIRE = "{\"@bytes\":\"RmF1bmE=\"}"; + public static final String BASE64_STRING = "RmF1bmE="; // Fauna + public static final String MAX_BYTE_WIRE = String.format("{\"@int\":\"%s\"}", Byte.MAX_VALUE); + public static final String MIN_BYTE_WIRE = String.format("{\"@int\":\"%s\"}", Byte.MIN_VALUE); + public static final String MAX_SHORT_WIRE = String.format("{\"@int\":\"%s\"}", Short.MAX_VALUE); + public static final String MIN_SHORT_WIRE = String.format("{\"@int\":\"%s\"}", Short.MIN_VALUE); + public static final String MAX_INT_WIRE = String.format("{\"@int\":\"%s\"}", Integer.MAX_VALUE); + public static final String MIN_INT_WIRE = String.format("{\"@int\":\"%s\"}", Integer.MIN_VALUE); + public static final String MAX_LONG_WIRE = String.format("{\"@long\":\"%s\"}", Long.MAX_VALUE); + public static final String MIN_LONG_WIRE = String.format("{\"@long\":\"%s\"}", Long.MIN_VALUE); + public static final String MAX_FLOAT_WIRE = String.format("{\"@double\":\"%s\"}", Float.MAX_VALUE); + public static final String MIN_FLOAT_WIRE = String.format("{\"@double\":\"%s\"}", Float.MIN_VALUE); + public static final String MAX_DOUBLE_WIRE = String.format("{\"@double\":\"%s\"}", Double.MAX_VALUE); + public static final String MIN_DOUBLE_WIRE = String.format("{\"@double\":\"%s\"}", Double.MIN_VALUE); + + public static final String OBJECT_WIRE = "{\"key1\":{\"@int\":\"42\"}}"; + public static String ESCAPED_OBJECT_WITH_WIRE(String tag) { + return String.format("{\"@object\":{\"%s\":\"not\"}}", tag); + } + public static final String ARRAY_WIRE = "[{\"@int\":\"42\"}]"; + public static final String CLASS_WITH_PARAMETERIZED_FIELDS_WIRE = "{\"first_name\":\"foo\",\"a_list\":[\"item1\"],\"a_map\":{\"key1\":{\"@int\":\"42\"}}}"; + public static final String DOCUMENT_WIRE = "{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}"; + public static final String DOCUMENT_REF_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final String NAMED_DOCUMENT_WIRE = "{\"@doc\":{\"name\":\"Boogles\",\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"coll\":{\"@mod\":\"Foo\"},\"first_name\":\"foo\",\"last_name\":\"bar\"}}"; + public static final String NAMED_DOCUMENT_REF_WIRE = "{\"@ref\":{\"name\":\"Boogles\",\"coll\":{\"@mod\":\"Foo\"}}}"; + public static final String NULL_DOC_WIRE = "{\"@ref\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"exists\":false,\"cause\":\"not found\"}}"; + public static final String PAGE_WIRE = "{\"@set\":{\"data\":[{\"@doc\":{\"id\":\"123\",\"coll\":{\"@mod\":\"Foo\"},\"ts\":{\"@time\":\"2023-12-15T01:01:01.0010010Z\"},\"first_name\":\"foo\",\"last_name\":\"bar\",\"age\":{\"@int\":\"42\"}}}],\"after\": null}}"; + + + // Objects + public static final PersonWithAttributes PERSON_WITH_ATTRIBUTES = new PersonWithAttributes("foo","bar",42); + public static final Page PAGE_DOCUMENT = new Page<>(List.of(PERSON_WITH_ATTRIBUTES),null); + public static final DocumentRef DOCUMENT_REF = new DocumentRef("123", new Module("Foo")); + public static final Document DOCUMENT = new Document( + "123", + new Module("Foo"), + Instant.parse("2023-12-15T01:01:01.0010010Z"), + Map.of("first_name","foo", "last_name", "bar","age",42) + ); + public static final NamedDocumentRef NAMED_DOCUMENT_REF = new NamedDocumentRef("Boogles", new Module("Foo")); + public static final NamedDocument NAMED_DOCUMENT = new NamedDocument( + "Boogles", + new Module("Foo"), + Instant.parse("2023-12-15T01:01:01.0010010Z"), + Map.of("first_name","foo", "last_name", "bar") + ); + public static final NullDocumentException NULL_DOC_EXCEPTION = new NullDocumentException("123", new Module("Foo"), "not found"); + + // Codecs + public static final Codec DYNAMIC_CODEC = DefaultCodecProvider.SINGLETON.get(Object.class); + public static final Codec STRING_CODEC = DefaultCodecProvider.SINGLETON.get(String.class); + public static final Codec CHAR_CODEC = DefaultCodecProvider.SINGLETON.get(Character.class); + public static final Codec BOOL_CODEC = DefaultCodecProvider.SINGLETON.get(Boolean.class); + public static final Codec BYTE_ARRAY_CODEC = DefaultCodecProvider.SINGLETON.get(byte[].class); + public static final Codec BYTE_CODEC = DefaultCodecProvider.SINGLETON.get(byte.class); + public static final Codec SHORT_CODEC = DefaultCodecProvider.SINGLETON.get(short.class); + public static final Codec INT_CODEC = DefaultCodecProvider.SINGLETON.get(int.class); + public static final Codec LONG_CODEC = DefaultCodecProvider.SINGLETON.get(long.class); + public static final Codec FLOAT_CODEC = DefaultCodecProvider.SINGLETON.get(float.class); + public static final Codec DOUBLE_CODEC = DefaultCodecProvider.SINGLETON.get(double.class); + public static final Codec> OPTIONAL_INT_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Optional.class, int.class); + public static final Codec> OPTIONAL_STRING_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Optional.class, String.class); + public static final Codec> PAGE_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Page.class, PersonWithAttributes.class); + public static final Codec> LIST_INT_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(List.class, int.class); + public static final Codec> MAP_INT_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Map.class, Integer.class); + public static final Codec> MAP_STRING_CODEC = (Codec>) (Codec) DefaultCodecProvider.SINGLETON.get(Map.class, String.class); + public static final Codec CLASS_WITH_REF_TAG_COLLISION_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithRefTagCollision.class); + public static final Codec CLASS_WITH_PARAMETERIZED_FIELDS_CODEC = DefaultCodecProvider.SINGLETON.get(ClassWithParameterizedFields.class); + public static final Codec PERSON_WITH_ATTRIBUTES_CODEC = DefaultCodecProvider.SINGLETON.get(PersonWithAttributes.class); + +} diff --git a/src/test/java/com/fauna/codec/codecs/IntCodecTest.java b/src/test/java/com/fauna/codec/codecs/IntCodecTest.java deleted file mode 100644 index 4bc2ffed..00000000 --- a/src/test/java/com/fauna/codec/codecs/IntCodecTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.*; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class IntCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - public void roundtrip_int() throws IOException { - var wire = "{\"@int\":\"42\"}"; - var codec = cp.get(int.class); - int decoded = Helpers.decode(codec, wire); - var encoded = Helpers.encode(codec, decoded); - assertEquals(wire, encoded); - } -} diff --git a/src/test/java/com/fauna/codec/codecs/ListCodecTest.java b/src/test/java/com/fauna/codec/codecs/ListCodecTest.java deleted file mode 100644 index 6203fc46..00000000 --- a/src/test/java/com/fauna/codec/codecs/ListCodecTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.*; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -public class ListCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - @SuppressWarnings("unchecked") - public void roundtrip_listOfIntegers() throws IOException { - var wire = "[{\"@int\":\"42\"}]"; - var codec = cp.get(List.class, Integer.class); - List decoded = Helpers.decode(codec, wire); - var encoded = Helpers.encode(codec, decoded); - assertEquals(wire, encoded); - } -} diff --git a/src/test/java/com/fauna/codec/codecs/MapCodecTest.java b/src/test/java/com/fauna/codec/codecs/MapCodecTest.java deleted file mode 100644 index 8578a9f2..00000000 --- a/src/test/java/com/fauna/codec/codecs/MapCodecTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.*; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -public class MapCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - @SuppressWarnings("unchecked") - public void roundtrip_mapOfIntegers() throws IOException { - var wire = "{\"key1\":{\"@int\":\"42\"}}"; - var codec = cp.get(Map.class, Integer.class); - Map decoded = Helpers.decode(codec, wire); - var encoded = Helpers.encode(codec, decoded); - assertEquals(wire, encoded); - } -} diff --git a/src/test/java/com/fauna/codec/codecs/StringCodecTest.java b/src/test/java/com/fauna/codec/codecs/StringCodecTest.java deleted file mode 100644 index 950a96c4..00000000 --- a/src/test/java/com/fauna/codec/codecs/StringCodecTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fauna.codec.codecs; - -import com.fauna.codec.*; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class StringCodecTest { - - CodecRegistry cr = new DefaultCodecRegistry(); - CodecProvider cp = new DefaultCodecProvider(cr); - - @Test - public void roundtrip_string() throws IOException { - var wire = "\"disco\""; - var codec = cp.get(String.class); - String decoded = Helpers.decode(codec, wire); - var encoded = Helpers.encode(codec, decoded); - assertEquals(wire, encoded); - } -} diff --git a/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java b/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java index 92abd7fa..ba700407 100644 --- a/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java +++ b/src/test/java/com/fauna/serialization/UTF8FaunaParserTest.java @@ -1,9 +1,6 @@ package com.fauna.serialization; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import static org.junit.jupiter.api.Assertions.assertThrows; import com.fauna.enums.FaunaTokenType; @@ -15,8 +12,11 @@ import java.time.Instant; import java.time.LocalDate; import java.util.AbstractMap; +import java.util.Arrays; import java.util.List; import java.util.Map; + +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -34,6 +34,19 @@ public void testGetValueAsString() throws IOException { assertReader(reader, expectedTokens); } + @Test + public void testGetValueAsByteArray() throws IOException { + String s = "{\"@bytes\": \"RmF1bmE=\"}"; + InputStream inputStream = new ByteArrayInputStream(s.getBytes()); + UTF8FaunaParser reader = new UTF8FaunaParser(inputStream); + + List> expectedTokens = List.of( + Map.entry(FaunaTokenType.BYTES, "Fauna".getBytes()) + ); + + assertReader(reader, expectedTokens); + } + @Test public void testGetValueAsInt() throws IOException { String s = "{\"@int\": \"123\"}"; @@ -588,6 +601,11 @@ private static void assertReader(UTF8FaunaParser reader, case STRING: assertEquals(entry.getValue(), reader.getValueAsString()); break; + case BYTES: + var ar1 = (byte[])entry.getValue(); + var ar2 = reader.getValueAsByteArray(); + if (!Arrays.equals(ar1, ar2)) Assertions.fail(String.format("expected: %s , received: %s", Arrays.toString(ar1), Arrays.toString(ar2))); + break; case INT: assertEquals(entry.getValue(), reader.getValueAsInt()); break; From e8d2546a8f5a8c94db93bab4bf04a6e5cf589432 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Thu, 15 Aug 2024 17:15:51 +0200 Subject: [PATCH 05/88] Add more test info --- .github/workflows/gradle-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-test.yml b/.github/workflows/gradle-test.yml index 8b52cf9c..e9d1ac5e 100644 --- a/.github/workflows/gradle-test.yml +++ b/.github/workflows/gradle-test.yml @@ -24,7 +24,7 @@ jobs: distribution: corretto - name: Run Gradle test - run: ./gradlew test + run: ./gradlew test -i - name: Run Gradle javadoc run: ./gradlew javadoc From 01b03c16bd0673e4d2109cf99e1344d6f7678587 Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Thu, 15 Aug 2024 17:18:39 +0200 Subject: [PATCH 06/88] Remove broken test --- .../com/fauna/serialization/RoundTripTest.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/test/java/com/fauna/serialization/RoundTripTest.java b/src/test/java/com/fauna/serialization/RoundTripTest.java index 5b5d1d35..2091038b 100644 --- a/src/test/java/com/fauna/serialization/RoundTripTest.java +++ b/src/test/java/com/fauna/serialization/RoundTripTest.java @@ -158,23 +158,6 @@ public void testList() throws IOException { assertEquals(serialized, Serializer.serialize(deserialized)); } - - - // @Disabled("Byte array deserialization not supported yet.") - @Test - public void testByteArray() throws IOException { - byte[] var = new byte[]{-128, 0, 127}; - String serialized = Serializer.serialize(var); - assertEquals("{\"@bytes\":\"gAB/\"}", serialized); - // IDeserializer deserializer = Deserializer.generate(ctx, Byte[].class); - Object deserialized = Deserializer.DYNAMIC.deserialize(new UTF8FaunaParser(serialized)); - // ser -> deser round trip - assertEquals(Map.of("@bytes", "gAB/"), deserialized); - // assertEquals(var, deserialized); TODO: Should get a byte[] or Byte[] not {"@bytes": "gAB\"} back. - // deser -> ser round trip - assertEquals(serialized, Serializer.serialize(deserialized)); - } - @Disabled("Object array deserialization not supported yet.") @Test public void testObjectArray() throws IOException { From 33517687801781a1a8ecbad630bb74882d664cbd Mon Sep 17 00:00:00 2001 From: Lucas Pedroza Date: Thu, 15 Aug 2024 18:52:46 +0200 Subject: [PATCH 07/88] Fix CodecRegistryKey equality --- src/main/java/com/fauna/codec/CodecRegistryKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fauna/codec/CodecRegistryKey.java b/src/main/java/com/fauna/codec/CodecRegistryKey.java index 89d56ed5..836d62ae 100644 --- a/src/main/java/com/fauna/codec/CodecRegistryKey.java +++ b/src/main/java/com/fauna/codec/CodecRegistryKey.java @@ -25,7 +25,7 @@ public boolean equals(Object other) { return true; } else if (other instanceof CodecRegistryKey) { CodecRegistryKey otherCRK = (CodecRegistryKey) other; - return this.base == otherCRK.base && this.typeArg == otherCRK.typeArg; + return Objects.equals(base, otherCRK.base) && Objects.equals(typeArg, otherCRK.typeArg); } else { return false; } From 481e7ae490580d7def9e74333d1af25e2904bfc4 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Fri, 16 Aug 2024 08:49:04 -0400 Subject: [PATCH 08/88] [DOCS-3263] Add GTM tags to JVM driver API index --- README.md | 11 ++++++----- concourse/scripts/body_gtm.dat | 1 + concourse/scripts/head_gtm.dat | 1 + concourse/scripts/publish-docs.sh | 14 ++++++++------ 4 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 concourse/scripts/body_gtm.dat create mode 100644 concourse/scripts/head_gtm.dat diff --git a/README.md b/README.md index 97906b39..11c0b141 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,10 @@ versions of FQL. To query your databases with earlier API versions, use the - Java 11 or later -## Javadocs +## API reference -API reference documentation is available in the [Javadocs](https://fauna.github.io/fauna-jvm/latest/). +API reference documentation for the driver is available at +https://fauna.github.io/fauna-jvm/. The docs are generated using Javadoc. ## Installation @@ -275,7 +276,7 @@ import com.fauna.client.PageIterator; public class App { public static void main(String[] args) { FaunaClient client = Fauna.client(); - + // `paginate()` will make an async request to Fauna. PageIterator iter1 = client.paginate(fql("Product.all()"), Product.class); @@ -285,9 +286,9 @@ public class App { List pageData = page.data(); // Do something with your data. } - + PageIterator iter2 = client.paginate(fql("Product.all()"), Product.class); - + // Use the `flatten()` on PageIterator to iterate over every item in a set. Iterator productIter = iter2.flatten(); List products = new ArrayList<>(); diff --git a/concourse/scripts/body_gtm.dat b/concourse/scripts/body_gtm.dat new file mode 100644 index 00000000..fddff324 --- /dev/null +++ b/concourse/scripts/body_gtm.dat @@ -0,0 +1 @@ +