diff --git a/build.gradle b/build.gradle index 1c1bc38c..9ae21bba 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,9 @@ project(':faunaJava') { dependencies { implementation project(':common') + implementation "com.google.code.gson:gson:${gsonVersion}" + + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" testImplementation "org.mockito:mockito-core:${mockitoVersion}" diff --git a/faunaJava/src/main/java/com/fauna/encoding/DateWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/DateWrapper.java new file mode 100644 index 00000000..cbada2e6 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/DateWrapper.java @@ -0,0 +1,40 @@ +package com.fauna.encoding; + +import com.google.gson.annotations.SerializedName; + +import java.time.LocalDate; + +/** + * A wrapper class that formats {@link LocalDate} objects for serialization with GSON. + * The {@code DateWrapper} class provides a convenient way to serialize Java {@link LocalDate} + * instances into a JSON format expected by Fauna, where date values are represented as strings + * with a specific annotation to be recognized by the database's query language. + */ +class DateWrapper { + + /** + * The {@link LocalDate} value represented as a string. + * It is annotated with {@code @SerializedName} to indicate the corresponding JSON key + * when serialized with GSON. + */ + @SerializedName("@date") + private final String value; + + /** + * Constructs a new {@code DateWrapper} instance using the provided {@link LocalDate} value. + * The local date is converted to its ISO-8601 string representation. + * + * @param value The local date value to be wrapped. It should not be {@code null}. + * The date is formatted as an ISO-8601 string (such as "2007-12-03"). + * If {@code null} is passed, it will result in a {@link NullPointerException}. + * @throws NullPointerException If the {@code value} parameter is {@code null}. + */ + public DateWrapper(LocalDate value) { + if (value == null) { + throw new NullPointerException("The LocalDate value for DateWrapper cannot be null."); + } + + this.value = value.toString(); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/DocumentReferenceWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/DocumentReferenceWrapper.java new file mode 100644 index 00000000..08bd800b --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/DocumentReferenceWrapper.java @@ -0,0 +1,46 @@ +package com.fauna.encoding; + +import com.fauna.query.model.DocumentReference; +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; +import java.util.Map; + +/** + * Wraps a Fauna document reference for serialization with GSON. + * This class is a utility to convert {@link DocumentReference} instances into the JSON format + * used by Fauna to denote references, with specific structure dictated by the database's + * requirements for references. + */ +class DocumentReferenceWrapper { + + /** + * A map representing the structure of a Fauna reference. + * The map is serialized into a JSON object with the key "@ref" to conform with + * the JSON format that Fauna expects for document references. + */ + @SerializedName("@ref") + private final Map ref; + + /** + * Constructs a new {@code DocumentReferenceWrapper} with the specified {@link DocumentReference}. + * It initializes an internal map structure to hold and represent the reference in the way + * Fauna can interpret. + * + * @param documentReference The document reference to wrap. This object must contain the ID + * and the collection module information to construct a valid reference. + * It should not be {@code null}. + * @throws NullPointerException If {@code documentReference} or any required property of it is {@code null}. + */ + public DocumentReferenceWrapper(DocumentReference documentReference) { + if (documentReference == null) { + throw new NullPointerException("DocumentReference cannot be null."); + } + + // Initialize the reference map with the document's ID and collection information. + this.ref = new HashMap<>(); + this.ref.put("id", documentReference.getId()); + this.ref.put("coll", new ModuleWrapper(documentReference.getColl())); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/DoubleWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/DoubleWrapper.java new file mode 100644 index 00000000..4ece5f20 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/DoubleWrapper.java @@ -0,0 +1,38 @@ +package com.fauna.encoding; + +import com.google.gson.annotations.SerializedName; + +/** + * A wrapper class that formats {@code Double} values for serialization with GSON. + * The {@code DoubleWrapper} class encapsulates a double value to be represented as a string + * for compatibility with the JSON format expected by Fauna, which requires specific typing + * of numbers as doubles using an annotation. + */ +class DoubleWrapper { + + /** + * The {@code Double} value represented as a string, annotated for JSON serialization. + * The annotation {@code @SerializedName("@double")} specifies the key name to use when + * this attribute is serialized to JSON, conforming to the expected Fauna JSON format. + */ + @SerializedName("@double") + private final String value; + + /** + * Constructs a new {@code DoubleWrapper} instance from a {@code Double} value. + * This constructor converts the {@code Double} value into its string representation. + * + * @param value The {@code Double} value to be wrapped, which should not be {@code null}. + * Conversion to string is done using the {@code Double.toString()} method. + * If {@code null} is passed, it will result in a {@link NullPointerException}. + * @throws NullPointerException If {@code value} is {@code null}, to prevent the creation + * of an invalid {@code DoubleWrapper} instance. + */ + public DoubleWrapper(Double value) { + if (value == null) { + throw new NullPointerException("The Double value for DoubleWrapper cannot be null."); + } + this.value = value.toString(); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/FaunaEncoder.java b/faunaJava/src/main/java/com/fauna/encoding/FaunaEncoder.java new file mode 100644 index 00000000..c361eaa5 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/FaunaEncoder.java @@ -0,0 +1,232 @@ +package com.fauna.encoding; + +import com.fauna.exception.TypeError; +import com.fauna.query.builder.Fragment; +import com.fauna.query.builder.LiteralFragment; +import com.fauna.query.builder.Query; +import com.fauna.query.builder.ValueFragment; +import com.fauna.query.model.Document; +import com.fauna.query.model.DocumentReference; +import com.fauna.query.model.Module; +import com.fauna.query.model.NamedDocument; +import com.fauna.query.model.NamedDocumentReference; +import com.fauna.query.model.NullDocument; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for encoding various Java types into a representation suitable for Fauna queries. + * The encoder translates Java objects to their Fauna tag equivalents, ensuring type fidelity when storing or querying the database. + *

+ * Supported type conversions are as follows: + *

+ *

+ * This class ensures that data types are encoded properly to maintain the integrity of the data when interacting with the Fauna service. + */ +public class FaunaEncoder { + + private static final String INT_TAG = "@int"; + private static final String LONG_TAG = "@long"; + private static final String DOUBLE_TAG = "@double"; + private static final String DATE_TAG = "@date"; + private static final String TIME_TAG = "@time"; + private static final String REF_TAG = "@ref"; + private static final String MOD_TAG = "@mod"; + private static final String OBJECT_TAG = "@object"; + private static final String DOC_TAG = "@doc"; + + private static final Set RESERVED_TAGS = Set.of( + INT_TAG, LONG_TAG, DOUBLE_TAG, DATE_TAG, TIME_TAG, MOD_TAG, DOC_TAG, REF_TAG, OBJECT_TAG + ); + private static final Gson gson = new GsonBuilder().serializeNulls().create(); + + private FaunaEncoder() { + } + + /** + * Encodes an object into its corresponding Fauna representation. + * This method dispatches the object to the appropriate encoder method based on its type. + * + * @param value The object to encode. + * @return A string containing the JSON encoded representation of the value. + * @throws TypeError if the object type is not supported by the encoder. + */ + public static String encode(Object value) { + return gson.toJson(wrapValue(value)); + } + + /** + * Wraps a value in its Fauna representation. + * This method handles encoding of basic types, special Fauna types, and structures like lists and maps. + * + * @param value The value to wrap. + * @return The encoded representation of the value. + */ + private static Object wrapValue(Object value) { + // This method decides how to wrap the value based on its type + if (value instanceof String || value == null || value instanceof Boolean) { + return value; + } + if (value instanceof Integer) { + return new IntWrapper((Integer) value); + } + if (value instanceof Long) { + return new LongWrapper((Long) value); + } + if (value instanceof Double) { + return new DoubleWrapper((Double) value); + } + if (value instanceof LocalDateTime) { + return new TimeWrapper((LocalDateTime) value); + } + if (value instanceof LocalDate) { + return new DateWrapper((LocalDate) value); + } + if (value instanceof DocumentReference) { + return new DocumentReferenceWrapper((DocumentReference) value); + } + if (value instanceof NamedDocumentReference) { + return new NamedDocumentReferenceWrapper((NamedDocumentReference) value); + } + if (value instanceof Module) { + return new ModuleWrapper((Module) value); + } + if (value instanceof NullDocument) { + return new NullDocumentWrapper((NullDocument) value); + } + if (value instanceof Document) { + return new DocumentReferenceWrapper( + new DocumentReference(((Document) value).getColl(), ((Document) value).getId()) + ); + } + if (value instanceof NamedDocument) { + return new NamedDocumentReferenceWrapper( + new NamedDocumentReference(((NamedDocument) value).getColl(), ((NamedDocument) value).getName()) + ); + } + if (value instanceof Query) { + return encodeQuery((Query) value); + } + if (value instanceof Map) { + return encodeMap((Map) value); + } + if (value instanceof List) { + return encodeList((List) value); + } + throw new TypeError("Unsupported type: " + value.getClass().getName()); + } + + /** + * Encodes a list of objects for Fauna. + * Iterates over the list and encodes each item using the wrapValue method. + * + * @param list The list of objects to encode. + * @return A List containing encoded representations of the original list's items. + */ + private static List encodeList(List list) { + List encodedList = new ArrayList<>(list.size()); + for (Object item : list) { + encodedList.add(wrapValue(item)); + } + return encodedList; + } + + /** + * Encodes a map for Fauna. + * Checks for reserved keys and encodes each entry using the wrapValue method. + * If reserved keys are present, the map is nested under an "@object" tag. + * + * @param map The map to encode. + * @return A Map containing the encoded representation of the original map. + * @throws IllegalArgumentException if the map contains keys that are not strings. + */ + private static Map encodeMap(Map map) { + Map encodedMap = new HashMap<>(); + boolean hasReservedKeys = false; // Flag to track if any reserved keys are present + + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + if (!(key instanceof String keyStr)) { + throw new IllegalArgumentException("Map keys must be strings to encode as Fauna object"); + } + if (RESERVED_TAGS.contains(keyStr)) { + hasReservedKeys = true; // Set flag if reserved key is found + encodedMap.put(keyStr, wrapValue(entry.getValue())); // Directly add to encodedMap for now + } else { + encodedMap.put(keyStr, wrapValue(entry.getValue())); + } + } + + // If reserved keys were present, nest all keys under OBJECT_TAG + if (hasReservedKeys) { + Map objectMap = new HashMap<>(); + objectMap.put(OBJECT_TAG, encodedMap); // Create a nested map for reserved keys + return objectMap; // Return the nested map + } + + return encodedMap; + } + + /** + * Encodes a Query object into a representation suitable for Fauna. + * + * @param value the Query object to encode. + * @return a Map representation of the query for Fauna. + */ + private static Map> encodeQuery(Query value) { + return Collections.singletonMap("fql", + value.getFragments().stream() + .map(FaunaEncoder::encodeFragment) + .collect(Collectors.toList())); + } + + /** + * Encodes a Fragment object. + * + * @param value the Fragment to encode. + * @return an Object representation of the fragment. + * @throws IllegalArgumentException if the fragment type is unknown. + */ + private static Object encodeFragment(Fragment value) { + if (value instanceof LiteralFragment) { + return ((LiteralFragment) value).get(); + } + if (value instanceof ValueFragment) { + Object fragmentValue = value.get(); + if (fragmentValue instanceof Query) { + return encodeQuery((Query) fragmentValue); + } + return Collections.singletonMap("value", wrapValue(fragmentValue)); + } + throw new IllegalArgumentException("Unknown fragment type: " + value.getClass()); + + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/IntWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/IntWrapper.java new file mode 100644 index 00000000..6a7f6bc7 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/IntWrapper.java @@ -0,0 +1,37 @@ +package com.fauna.encoding; + +import com.google.gson.annotations.SerializedName; + +/** + * A wrapper class for serializing {@link Integer} values as strings with GSON, using a specified + * serialization key that matches the Fauna convention for integers. This class ensures that + * integer values are correctly formatted as JSON strings in the expected format when interacting + * with Fauna, which uses special typing annotations for numbers. + */ +class IntWrapper { + + /** + * The {@link Integer} value represented as a string for JSON serialization. The field is + * marked with {@link SerializedName} to indicate to GSON the key name "@int" under which + * the serialized value should be placed in the JSON object. + */ + @SerializedName("@int") + private final String value; + + /** + * Constructs a new {@code IntWrapper} instance from an {@link Integer} value. The integer + * is converted to its String representation, as required for serialization. + * + * @param value The integer value to be converted to a string and wrapped. Must not be {@code null} + * to prevent the creation of an invalid {@code IntWrapper} instance. If {@code null}, + * a {@link NullPointerException} is thrown. + * @throws NullPointerException if the input value is {@code null}. + */ + public IntWrapper(Integer value) { + if (value == null) { + throw new NullPointerException("The Integer value for IntWrapper cannot be null."); + } + this.value = value.toString(); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/LongWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/LongWrapper.java new file mode 100644 index 00000000..2e96ac3b --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/LongWrapper.java @@ -0,0 +1,38 @@ +package com.fauna.encoding; + +import com.google.gson.annotations.SerializedName; + +/** + * A wrapper class for serializing {@link Long} values as strings with GSON, conforming to the + * expected JSON serialization format for Fauna. When Fauna expects a number to be explicitly + * typed as a long, this class ensures that {@link Long} values are serialized with the correct + * type annotation. + */ +class LongWrapper { + + /** + * The {@link Long} value represented as a string for JSON serialization. It is annotated with + * {@link SerializedName} to specify the "@long" key that Fauna uses to recognize the type of + * the serialized number. + */ + @SerializedName("@long") + private final String value; + + /** + * Constructs a new {@code LongWrapper} with the given {@link Long} value by converting it to + * its string representation, as Fauna expects long values to be transmitted as strings in + * JSON objects. + * + * @param value The {@link Long} value to be wrapped, which should not be {@code null} to + * ensure proper serialization. If {@code null} is provided, the constructor will + * throw a {@link NullPointerException}. + * @throws NullPointerException If the input value is {@code null}. + */ + public LongWrapper(Long value) { + if (value == null) { + throw new NullPointerException("The Long value for LongWrapper cannot be null."); + } + this.value = value.toString(); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/ModuleWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/ModuleWrapper.java new file mode 100644 index 00000000..84f54eb4 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/ModuleWrapper.java @@ -0,0 +1,35 @@ +package com.fauna.encoding; + +import com.fauna.query.model.Module; +import com.google.gson.annotations.SerializedName; + +/** + * Wraps a module object for JSON serialization with GSON. This class specifically handles + * the serialization of {@link Module} instances to conform with the JSON structure expected by + * Fauna. The serialization process will output a JSON object with a single key "@mod" that + * maps to the name of the module. + */ +class ModuleWrapper { + + /** + * The name of the module represented as a string. The {@link SerializedName} annotation + * specifies the key "@mod" under which this value will be placed in the serialized JSON object. + */ + @SerializedName("@mod") + private final String name; + + /** + * Constructs a {@code ModuleWrapper} with the provided {@link Module} object. + * It extracts the name of the module and prepares it for serialization with the "@mod" key. + * + * @param module The {@link Module} instance to wrap. The module name is extracted and stored. + * Must not be {@code null} to avoid a {@link NullPointerException}. + * @throws NullPointerException If the {@code module} parameter is {@code null}. + */ + ModuleWrapper(Module module) { + if (module == null) { + throw new NullPointerException("Module cannot be null for ModuleWrapper."); + } + this.name = module.getName(); + } +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/NamedDocumentReferenceWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/NamedDocumentReferenceWrapper.java new file mode 100644 index 00000000..cad37ffd --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/NamedDocumentReferenceWrapper.java @@ -0,0 +1,46 @@ +package com.fauna.encoding; + +import com.fauna.query.model.NamedDocumentReference; +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; +import java.util.Map; + +/** + * A wrapper class that prepares a {@link NamedDocumentReference} for JSON serialization with GSON, + * formatting it according to Fauna's reference object structure. This class specifically handles + * the inclusion of the document's name and its collection module within a serialized reference map. + */ +class NamedDocumentReferenceWrapper { + + /** + * A map that holds the structure of a named document reference as expected by Fauna when serialized. + * The serialized name and collection information are encapsulated within this map with the "@ref" + * key annotation provided by the {@link SerializedName} annotation. + */ + @SerializedName("@ref") + private final Map reference; + + /** + * Constructs a new {@code NamedDocumentReferenceWrapper} with the provided {@link NamedDocumentReference}. + * This constructor initializes a map with "name" and "coll" keys to represent the document reference + * as required by Fauna's JSON format for named references. + * + * @param namedDocumentReference The named document reference to wrap, not to be {@code null}. + * It should contain both the name of the document and the collection + * module reference. + * @throws NullPointerException If the input {@code namedDocumentReference} or any of its required + * properties are {@code null}. + */ + public NamedDocumentReferenceWrapper(NamedDocumentReference namedDocumentReference) { + if (namedDocumentReference == null) { + throw new NullPointerException("NamedDocumentReference cannot be null."); + } + // Initialize the map to be used for serialization with appropriate keys and values. + Map details = new HashMap<>(); + details.put("name", namedDocumentReference.getName()); + details.put("coll", new ModuleWrapper(namedDocumentReference.getColl())); + this.reference = details; + } +} + diff --git a/faunaJava/src/main/java/com/fauna/encoding/NullDocumentWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/NullDocumentWrapper.java new file mode 100644 index 00000000..61ef3692 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/NullDocumentWrapper.java @@ -0,0 +1,56 @@ +package com.fauna.encoding; + +import com.fauna.query.model.BaseReference; +import com.fauna.query.model.DocumentReference; +import com.fauna.query.model.NamedDocumentReference; +import com.fauna.query.model.NullDocument; +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; +import java.util.Map; + +/** + * A wrapper class for serializing {@code NullDocument} instances in the structure expected by Fauna. + * This class adapts the {@code NullDocument} to a JSON-serializable format by converting its reference + * information into a map structure. The resulting JSON object uses the "@ref" key to denote the reference + * type according to Fauna's schema. + */ +class NullDocumentWrapper { + + /** + * A map representing the reference details of a {@code NullDocument}. + * The map is structured with keys that Fauna understands for representing document references. + */ + @SerializedName("@ref") + private final Map refMap; + + /** + * Constructs a new {@code NullDocumentWrapper} with the specified {@code NullDocument}. + * Depending on the subtype of the {@code BaseReference} held by the {@code NullDocument}, + * it prepares a map with either "id" and "coll" keys for a {@code DocumentReference} or "name" + * and "coll" for a {@code NamedDocumentReference}. + * + * @param nullDoc The {@code NullDocument} to wrap. Its reference should be a valid {@code BaseReference} + * instance, either {@code DocumentReference} or {@code NamedDocumentReference}. + * The reference is used to populate the map with appropriate details. + */ + public NullDocumentWrapper(NullDocument nullDoc) { + this.refMap = new HashMap<>(); + BaseReference ref = nullDoc.getRef(); + + // Check the type of reference and populate the map accordingly. + if (ref instanceof DocumentReference docRef) { + Map collMap = new HashMap<>(); + collMap.put("@mod", docRef.getColl().getName()); + refMap.put("id", docRef.getId()); + refMap.put("coll", collMap); + } else if (ref instanceof NamedDocumentReference namedDocRef) { + Map collMap = new HashMap<>(); + collMap.put("@mod", namedDocRef.getColl().getName()); + refMap.put("name", namedDocRef.getName()); + refMap.put("coll", collMap); + } + + } + +} diff --git a/faunaJava/src/main/java/com/fauna/encoding/TimeWrapper.java b/faunaJava/src/main/java/com/fauna/encoding/TimeWrapper.java new file mode 100644 index 00000000..e9bb8943 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/encoding/TimeWrapper.java @@ -0,0 +1,53 @@ +package com.fauna.encoding; + +import com.google.gson.annotations.SerializedName; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +/** + * A wrapper class for serializing {@link LocalDateTime} instances as UTC strings with GSON, + * annotated to match the JSON serialization format required by Fauna for time values. + * This class formats the date and time with microsecond precision, appending a 'Z' to + * indicate UTC. + */ +class TimeWrapper { + + /** + * The formatter to use for converting {@code LocalDateTime} instances to strings. + * This formatter will produce times in the format "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", + * which is compliant with the ISO-8601 standard for representation of dates and times. + */ + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"); + + /** + * The {@link LocalDateTime} value formatted as a string. The {@link SerializedName} annotation + * specifies the key "@time" under which this value will be placed in the serialized JSON object. + */ + @SerializedName("@time") + private final String value; + + /** + * Constructs a new {@code TimeWrapper} instance from a {@link LocalDateTime} value. + * The local date and time is converted to UTC and formatted to a string according to the + * pattern defined by {@code formatter}. + * + * @param value The local date and time value to be converted to a string and wrapped. + * The value must be non-null and will be converted to UTC time zone. + * @throws NullPointerException If the {@code value} is {@code null}. + */ + public TimeWrapper(LocalDateTime value) { + if (value == null) { + throw new NullPointerException("The LocalDateTime value for TimeWrapper cannot be null."); + } + + // Convert LocalDateTime to OffsetDateTime with UTC as the time zone. + OffsetDateTime offsetDateTime = value.atOffset(ZoneOffset.UTC); + // Format the OffsetDateTime to the string representation using the custom formatter. + this.value = offsetDateTime.format(formatter); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/exception/TypeError.java b/faunaJava/src/main/java/com/fauna/exception/TypeError.java new file mode 100644 index 00000000..cf443a8b --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/exception/TypeError.java @@ -0,0 +1,9 @@ +package com.fauna.exception; + +public class TypeError extends RuntimeException{ + + public TypeError(String message) { + super(message); + } + +} diff --git a/faunaJava/src/main/java/com/fauna/query/builder/Fragment.java b/faunaJava/src/main/java/com/fauna/query/builder/Fragment.java index 30331a41..6a4b55d0 100644 --- a/faunaJava/src/main/java/com/fauna/query/builder/Fragment.java +++ b/faunaJava/src/main/java/com/fauna/query/builder/Fragment.java @@ -3,7 +3,7 @@ /** * An abstract class serving as a base for different types of query fragments. */ -abstract class Fragment { +public abstract class Fragment { /** * Retrieves the value represented by this fragment. diff --git a/faunaJava/src/main/java/com/fauna/query/builder/LiteralFragment.java b/faunaJava/src/main/java/com/fauna/query/builder/LiteralFragment.java index 0f54ea61..371eda4a 100644 --- a/faunaJava/src/main/java/com/fauna/query/builder/LiteralFragment.java +++ b/faunaJava/src/main/java/com/fauna/query/builder/LiteralFragment.java @@ -6,7 +6,7 @@ * Represents a literal fragment of a Fauna query. * This class encapsulates a fixed string that does not contain any variables. */ -class LiteralFragment extends Fragment { +public class LiteralFragment extends Fragment { private final String value; diff --git a/faunaJava/src/main/java/com/fauna/query/builder/Query.java b/faunaJava/src/main/java/com/fauna/query/builder/Query.java index b99a4c8f..3c070415 100644 --- a/faunaJava/src/main/java/com/fauna/query/builder/Query.java +++ b/faunaJava/src/main/java/com/fauna/query/builder/Query.java @@ -66,7 +66,7 @@ public static Query fql(String query, Map args) throws IllegalAr * * @return a list of Fragments. */ - List getFragments() { + public List getFragments() { return fragments; } diff --git a/faunaJava/src/main/java/com/fauna/query/builder/ValueFragment.java b/faunaJava/src/main/java/com/fauna/query/builder/ValueFragment.java index d0d875b4..8942ff94 100644 --- a/faunaJava/src/main/java/com/fauna/query/builder/ValueFragment.java +++ b/faunaJava/src/main/java/com/fauna/query/builder/ValueFragment.java @@ -6,7 +6,7 @@ * Represents a value fragment of a Fauna query. * This class encapsulates a value that can be a variable in the query. */ -class ValueFragment extends Fragment { +public class ValueFragment extends Fragment { private final Object value; diff --git a/faunaJava/src/main/java/com/fauna/query/model/BaseDocument.java b/faunaJava/src/main/java/com/fauna/query/model/BaseDocument.java new file mode 100644 index 00000000..4406c41a --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/BaseDocument.java @@ -0,0 +1,63 @@ +package com.fauna.query.model; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +/** + * Base class for documents, providing a read-only view of data as a map. + * This class implements the Iterable interface, allowing iteration over the document's entries. + */ +public class BaseDocument implements Iterable> { + + private final Map store; + + /** + * Constructs a BaseDocument with the given map data. + * + * @param data A map containing the initial data for this document. + */ + protected BaseDocument(Map data) { + // Creates an immutable copy of the data map + this.store = Map.copyOf(data); + } + + /** + * Retrieves the value to which the specified key is mapped. + * + * @param key The key whose associated value is to be returned. + * @return The value to which the specified key is mapped, or null if this map contains no mapping for the key. + */ + public Object get(String key) { + return store.get(key); + } + + /** + * Returns the number of key-value mappings in this document. + * + * @return The number of key-value mappings in this document. + */ + public int size() { + return store.size(); + } + + @Override + public Iterator> iterator() { + return store.entrySet().iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BaseDocument entries = (BaseDocument) o; + + return Objects.equals(store, entries.store); + } + + @Override + public int hashCode() { + return store != null ? store.hashCode() : 0; + } +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/BaseReference.java b/faunaJava/src/main/java/com/fauna/query/model/BaseReference.java new file mode 100644 index 00000000..40efb0c3 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/BaseReference.java @@ -0,0 +1,41 @@ +package com.fauna.query.model; + +/** + * Abstract base class for reference models in Fauna query representation. + * It holds a reference to a Module object representing a collection in the database. + */ +public abstract class BaseReference { + + /** + * Module object representing a collection + */ + protected Module coll; + + /** + * Constructs a BaseReference with a collection name which is converted to a Module. + * + * @param coll A string representation of the collection name. + */ + public BaseReference(String coll) { + this.coll = new Module(coll); + } + + /** + * Constructs a BaseReference with a given Module object. + * + * @param coll A Module object representing the collection. + */ + public BaseReference(Module coll) { + this.coll = coll; + } + + /** + * Gets the collection Module object. + * + * @return The Module object representing the collection. + */ + public Module getColl() { + return coll; + } + +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/Document.java b/faunaJava/src/main/java/com/fauna/query/model/Document.java new file mode 100644 index 00000000..39876e71 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/Document.java @@ -0,0 +1,59 @@ +package com.fauna.query.model; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a specific document in the Fauna database with a unique identifier and timestamp. + * This class is immutable and extends BaseDocument. + */ +public class Document extends BaseDocument { + + private final String id; + private final LocalDateTime ts; + private final Module coll; + + /** + * Constructs a new Document with the specified identifier, timestamp, collection reference, and data. + * + * @param id The unique identifier for this document. + * @param ts The timestamp of the document. + * @param coll The collection to which this document belongs. + * @param data The data map containing the document's properties. + */ + public Document(String id, LocalDateTime ts, Module coll, Map data) { + super(data); + this.id = Objects.requireNonNull(id, "Document id cannot be null"); + this.ts = Objects.requireNonNull(ts, "Document timestamp cannot be null"); + this.coll = Objects.requireNonNull(coll, "Document collection reference cannot be null"); + } + + /** + * Retrieves the unique identifier for this document. + * + * @return The unique identifier for this document. + */ + public String getId() { + return id; + } + + /** + * Retrieves the timestamp of when this document was last updated or created. + * + * @return The timestamp of this document. + */ + public LocalDateTime getTs() { + return ts; + } + + /** + * Retrieves the collection reference in which this document is stored. + * + * @return The collection reference as a Module. + */ + public Module getColl() { + return coll; + } + +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/DocumentReference.java b/faunaJava/src/main/java/com/fauna/query/model/DocumentReference.java new file mode 100644 index 00000000..18e3713c --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/DocumentReference.java @@ -0,0 +1,76 @@ +package com.fauna.query.model; + +import java.util.Objects; + +/** + * Class representing a reference to a Document in Fauna. + * It extends BaseReference by adding an identifier for the specific document. + */ +public class DocumentReference extends BaseReference { + + /** + * Document identifier + */ + private final String id; + + /** + * Constructs a DocumentReference with a collection name and document ID. + * + * @param coll A string representing the collection name. + * @param id The document's identifier. + */ + public DocumentReference(String coll, String id) { + super(coll); + this.id = id; + } + + /** + * Constructs a DocumentReference with a Module and document ID. + * + * @param coll A Module object representing the collection. + * @param id The document's identifier. + */ + public DocumentReference(Module coll, String id) { + super(coll); + this.id = id; + } + + /** + * Gets the document ID. + * + * @return The string representing the document's identifier. + */ + public String getId() { + return id; + } + + /** + * Factory method to create a DocumentReference from a formatted string. + * + * @param ref A string of the format "CollectionName:ID". + * @return A new DocumentReference parsed from the string. + * @throws IllegalArgumentException If the format of the reference string is incorrect. + */ + public static DocumentReference fromString(String ref) { + String[] parts = ref.split(":"); + if (parts.length != 2) { + throw new IllegalArgumentException("Expects string of format :"); + } + return new DocumentReference(parts[0], parts[1]); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DocumentReference that = (DocumentReference) o; + + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/Module.java b/faunaJava/src/main/java/com/fauna/query/model/Module.java new file mode 100644 index 00000000..b918978e --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/Module.java @@ -0,0 +1,51 @@ +package com.fauna.query.model; + +import java.util.Objects; + +/** + * Represents a Module in the Fauna database system. A module may represent various + * constructs within Fauna such as collections, mathematical modules, or user-defined collections. + * The Module class provides a structure to interact with these different elements through + * Fauna query language (FQL) by encapsulating the module's name. + */ +public class Module { + + /** + * The name of the module. + */ + private final String name; + + /** + * Constructs a new Module with the specified name. + * The name parameter represents the identifier used within FQL queries. + * + * @param name The name of the module to be created, not null. + */ + public Module(String name) { + this.name = name; + } + + /** + * Retrieves the name of the module. + * + * @return The name of the module. + */ + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Module module = (Module) o; + + return Objects.equals(name, module.name); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/NamedDocument.java b/faunaJava/src/main/java/com/fauna/query/model/NamedDocument.java new file mode 100644 index 00000000..bc3eb940 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/NamedDocument.java @@ -0,0 +1,76 @@ +package com.fauna.query.model; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a named document in the Fauna database, such as definitions of collections, indexes, and roles. + * This class is immutable and extends BaseDocument. + */ +public class NamedDocument extends BaseDocument { + + private final String name; + private final LocalDateTime ts; + private final Module coll; + + /** + * Constructs a new NamedDocument with the specified name, timestamp, collection reference, and data. + * @param name The name of this document. + * @param ts The timestamp of the document. + * @param coll The collection to which this named document belongs. + * @param data The data map containing the document's properties. + */ + public NamedDocument(String name, LocalDateTime ts, Module coll, Map data) { + super(data); + this.name = Objects.requireNonNull(name, "NamedDocument name cannot be null"); + this.ts = Objects.requireNonNull(ts, "NamedDocument timestamp cannot be null"); + this.coll = Objects.requireNonNull(coll, "NamedDocument collection reference cannot be null"); + } + + /** + * Retrieves the name of this named document, which could represent a collection, index, role, etc. + * @return The name of the named document. + */ + public String getName() { + return name; + } + + /** + * Retrieves the timestamp of when this named document was last updated or created. + * @return The timestamp of the named document. + */ + public LocalDateTime getTs() { + return ts; + } + + /** + * Retrieves the collection reference in which this named document is stored. + * @return The collection reference as a Module. + */ + public Module getColl() { + return coll; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + NamedDocument entries = (NamedDocument) o; + + if (!Objects.equals(name, entries.name)) return false; + if (!Objects.equals(ts, entries.ts)) return false; + return Objects.equals(coll, entries.coll); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (ts != null ? ts.hashCode() : 0); + result = 31 * result + (coll != null ? coll.hashCode() : 0); + return result; + } +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/NamedDocumentReference.java b/faunaJava/src/main/java/com/fauna/query/model/NamedDocumentReference.java new file mode 100644 index 00000000..3f356ed2 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/NamedDocumentReference.java @@ -0,0 +1,61 @@ +package com.fauna.query.model; + +import java.util.Objects; + +/** + * Class representing a reference to a named document in Fauna. + * It extends BaseReference by adding a name for the specific document. + */ +public class NamedDocumentReference extends BaseReference { + + /** + * Named document identifier. + */ + private final String name; + + /** + * Constructs a NamedDocumentReference with a collection name and document name. + * + * @param coll A string representing the collection name. + * @param name The named document's identifier. + */ + public NamedDocumentReference(String coll, String name) { + super(coll); + this.name = name; + } + + /** + * Constructs a NamedDocumentReference with a Module and document name. + * + * @param coll A Module object representing the collection. + * @param name The named document's identifier. + */ + public NamedDocumentReference(Module coll, String name) { + super(coll); + this.name = name; + } + + /** + * Gets the named document identifier. + * + * @return The string representing the named document's identifier. + */ + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NamedDocumentReference that = (NamedDocumentReference) o; + + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } +} diff --git a/faunaJava/src/main/java/com/fauna/query/model/NullDocument.java b/faunaJava/src/main/java/com/fauna/query/model/NullDocument.java new file mode 100644 index 00000000..b67e4c07 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/model/NullDocument.java @@ -0,0 +1,71 @@ +package com.fauna.query.model; + +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a null or non-existent document within the Fauna database. + * This class provides a way to encapsulate a reference to a document that + * might not be present, along with an optional cause for its absence. + */ +public class NullDocument { + + /** + * Reference to the potentially null document. + */ + private final BaseReference ref; + + /** + * Reason for the document's absence, if any + */ + private final String cause; + + /** + * Constructs a NullDocument with a given reference and an optional cause. + * + * @param ref A reference to the document that is null. This can be an instance of + * DocumentReference or NamedDocumentReference. + * @param cause An optional string describing the cause of the document's null status. + */ + public NullDocument(BaseReference ref, String cause) { + this.ref = ref; + this.cause = cause; + } + + /** + * Retrieves the reference to the null document. + * + * @return The reference to the null document. + */ + public BaseReference getRef() { + return ref; + } + + /** + * Retrieves an Optional containing the cause for the document's absence. + * The Optional is empty if there is no cause provided. + * + * @return An Optional containing the cause of absence, if available. + */ + public Optional getCause() { + return Optional.ofNullable(cause); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NullDocument that = (NullDocument) o; + + if (!Objects.equals(ref, that.ref)) return false; + return Objects.equals(cause, that.cause); + } + + @Override + public int hashCode() { + int result = ref != null ? ref.hashCode() : 0; + result = 31 * result + (cause != null ? cause.hashCode() : 0); + return result; + } +} diff --git a/faunaJava/src/test/java/com/fauna/encoding/FaunaEncoderTest.java b/faunaJava/src/test/java/com/fauna/encoding/FaunaEncoderTest.java new file mode 100644 index 00000000..7ad69c44 --- /dev/null +++ b/faunaJava/src/test/java/com/fauna/encoding/FaunaEncoderTest.java @@ -0,0 +1,928 @@ +package com.fauna.encoding; + +import com.fauna.query.builder.Query; +import com.fauna.query.model.Document; +import com.fauna.query.model.DocumentReference; +import com.fauna.query.model.Module; +import com.fauna.query.model.NamedDocument; +import com.fauna.query.model.NamedDocumentReference; +import com.fauna.query.model.NullDocument; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import static com.fauna.query.builder.Query.fql; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FaunaEncoderTest { + Map testMap; + + { + testMap = new HashMap<>(); + testMap.put("int", 10); + testMap.put("double", 10.0); + testMap.put("long", 2147483649L); + testMap.put("string", "foo"); + testMap.put("true", true); + testMap.put("false", false); + testMap.put("none", null); + testMap.put("date", LocalDate.of(2023, 2, 28)); + testMap.put("time", LocalDateTime.of(2023, 2, 28, 10, 10, 10, 10000)); + + } + + private Gson gson; + + @BeforeEach + public void setUp() { + gson = new GsonBuilder().serializeNulls().create(); + } + + + @Test + void testEncodeString() { + String test = "hello"; + JsonElement expectedJson = JsonParser.parseString(test); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeTrue() { + Boolean test = true; + JsonElement expectedJson = JsonParser.parseString(String.valueOf(test)); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeFalse() { + Boolean test = false; + JsonElement expectedJson = JsonParser.parseString(String.valueOf(test)); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@int\":\"10\"}"); + int test = 10; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMax32BitSignedInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@int\":\"2147483647\"}"); + int test = 2147483647; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMin32BitSignedInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@int\":\"-2147483648\"}"); + int test = -2147483648; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMax32BitSignedIntPlusOne() { + JsonElement expectedJson = JsonParser.parseString("{\"@long\":\"2147483648\"}"); + long test = 2147483648L; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMin32BitSignedIntMinusOne() { + JsonElement expectedJson = JsonParser.parseString("{\"@long\":\"-2147483649\"}"); + long test = -2147483649L; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMax64BitSignedInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@long\":\"9223372036854775807\"}"); + long test = 9223372036854775807L; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeMin64BitSignedInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@long\":\"-9223372036854775808\"}"); + long test = -9223372036854775808L; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + + } + + + @Test + void testEncodeNegativeDouble() { + JsonElement expectedJson = JsonParser.parseString("{\"@double\":\"-100.0\"}"); + double test = -100.0; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodePositiveDouble() { + JsonElement expectedJson = JsonParser.parseString("{\"@double\":\"9.999999999999\"}"); + double test = 9.999999999999; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeNone() { + JsonElement expectedJson = JsonParser.parseString("{\"foo\":null}"); + Map test = new HashMap<>(); + test.put("foo", null); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeLocalDateTime() { + JsonElement expectedJson = JsonParser.parseString("{\"@time\":\"2023-02-28T10:10:10.000001Z\"}"); + LocalDateTime testDateTime = LocalDateTime.of(2023, 2, 28, 10, 10, 10, 1000); + + String encoded = FaunaEncoder.encode(testDateTime); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeLocalDate() { + JsonElement expectedJson = JsonParser.parseString("{\"@date\":\"2023-03-17\"}"); + LocalDate testDate = LocalDate.parse("2023-03-17"); + + String encoded = FaunaEncoder.encode(testDate); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeDocumentReference() { + JsonElement expectedJson = JsonParser.parseString("{'@ref': {'coll': {'@mod': 'Col'}, 'id': \"123\"}}"); + DocumentReference docRef = new DocumentReference(new Module("Col"), "123"); + + String encoded = FaunaEncoder.encode(docRef); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeNullDocument() { + JsonElement expectedJson = JsonParser.parseString("{\"@ref\": {\"id\": \"456\", \"coll\": {\"@mod\": \"NDCol\"}}}"); + NullDocument nullDoc = new NullDocument(new DocumentReference(new Module("NDCol"), "456"), "not found"); + + String encoded = FaunaEncoder.encode(nullDoc); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeNamedNullDocument() { + JsonElement expectedJson = JsonParser.parseString("{\"@ref\": {\"name\": \"Party\", \"coll\": {\"@mod\": \"Collection\"}}}"); + NullDocument nullDoc = new NullDocument(new NamedDocumentReference("Collection", "Party"), "not found"); + + String encoded = FaunaEncoder.encode(nullDoc); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeNamedDocumentReference() { + JsonElement expectedJson = JsonParser.parseString("{\"@ref\": {\"name\": \"Hi\", \"coll\": {\"@mod\": \"Col\"}}}"); + NamedDocumentReference docRef = new NamedDocumentReference("Col", "Hi"); + + String encoded = FaunaEncoder.encode(docRef); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeDocument() { + JsonElement expectedJson = JsonParser.parseString("{\"@ref\": {\"id\": \"123\", \"coll\": {\"@mod\": \"Dogs\"}}}"); + + LocalDateTime fixedDatetime = LocalDateTime.now(); + Module dogsModule = new Module("Dogs"); + Map data = new HashMap<>(); + data.put("name", "Scout"); + Document testDoc = new Document("123", fixedDatetime, dogsModule, data); + + String encoded = FaunaEncoder.encode(testDoc); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeNamedDocuments() { + JsonElement expectedJson = JsonParser.parseString("{\"@ref\": {\"name\": \"DogSchema\", \"coll\": {\"@mod\": \"Dogs\"}}}"); + LocalDateTime fixedDatetime = LocalDateTime.now(); + NamedDocument testNamedDoc = new NamedDocument("DogSchema", fixedDatetime, new Module("Dogs"), new HashMap<>()); + + String encoded = FaunaEncoder.encode(testNamedDoc); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeModule() { + JsonElement expectedJson = JsonParser.parseString("{\"@mod\": \"Math\"}"); + Module testModule = new Module("Math"); + + String encoded = FaunaEncoder.encode(testModule); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeList() { + JsonElement expectedJson = JsonParser.parseString("[{\"@date\":\"2023-02-28\"}, \"foo\", {\"@double\":\"10.0\"}, true, false, null, {\"@time\":\"2023-02-28T10:10:10.000010Z\"}, {\"@int\":\"10\"}, {\"@long\":\"2147483649\"}]"); + Object[] testValues = testMap.values().toArray(); + + String encoded = FaunaEncoder.encode(Arrays.asList(testValues)); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeMap() { + JsonElement expectedJson = JsonParser.parseString("{\"date\":{\"@date\":\"2023-02-28\"},\"string\":\"foo\",\"double\":{\"@double\":\"10.0\"},\"true\":true,\"false\":false,\"none\":null,\"time\":{\"@time\":\"2023-02-28T10:10:10.000010Z\"},\"int\":{\"@int\":\"10\"},\"long\":{\"@long\":\"2147483649\"}}"); + + String encoded = FaunaEncoder.encode(testMap); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeIntConflictWithIntType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@int\": {\"@int\": \"10\"}}}"); + + Map test = Map.of("@int", 10); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeIntConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@int\": \"bar\"}}"); + + Map test = Map.of("@int", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + + } + + @Test + void testEncodeLongConflictWithLongType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@long\": {\"@long\": \"2147483649\"}}}"); + + Map test = Map.of("@long", 2147483649L); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeLongConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@long\": \"bar\"}}"); + + Map test = Map.of("@long", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeDoubleConflictWithDoubleType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@double\": {\"@double\": \"10.2\"}}}"); + + Map test = Map.of("@double", 10.2); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + + } + + @Test + public void testEncodeDoubleConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@double\": \"bar\"}}"); + + Map test = Map.of("@double", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + + } + + @Test + public void testEncodeDateConflictWithDateType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@date\": {\"@date\": \"2023-02-28\"}}}"); + + Map test = Map.of("@date", LocalDate.of(2023, 2, 28)); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeDateConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@date\": \"bar\"}}"); + + Map test = Map.of("@date", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeTimeConflictWithDateTimeType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@time\": {\"@time\": \"2023-02-28T10:10:10.000001Z\"}}}"); + LocalDateTime dateTime = LocalDateTime.of(2023, 2, 28, 10, 10, 10, 1000); + + Map test = Map.of("@time", dateTime); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeTimeConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@time\": \"bar\"}}"); + + Map test = Map.of("@time", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeRefConflictWithRefType() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"@object\": {\n" + + " \"@ref\": {\n" + + " \"@ref\": {\n" + + " \"id\": \"123\",\n" + + " \"coll\": {\n" + + " \"@mod\": \"Col\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }"); + + Map test = Map.of("@ref", DocumentReference.fromString("Col:123")); + ; + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeDocConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@doc\": \"bar\"}}"); + + Map test = Map.of("@doc", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeModConflictWithModType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@mod\": {\"@mod\": \"Math\"}}}"); + + Map test = Map.of("@mod", new Module("Math")); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeModConflictWithOtherType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@mod\": \"bar\"}}"); + + Map test = Map.of("@mod", "bar"); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeObjectConflictsWithType() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@object\": {\"@int\": \"10\"}}}"); + + Map test = Map.of("@object", 10); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeObjectConflictsWithInt() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@object\": {\"@object\": {\"@int\": \"bar\"}}}}"); + + Map> test = Map.of("@object", Map.of("@int", "bar")); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeObjectConflictsWithObject() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@object\": {\"@object\": {\"@object\": \"bar\"}}}}"); + + Map> test = Map.of("@object", Map.of("@object", "bar")); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeMultipleKeysInConflict_NonConflicting() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@int\": \"foo\", \"tree\": \"birch\"}}"); + + Map test = Map.of( + "@int", "foo", + "tree", "birch" + ); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeMultipleKeysInConflict_Conflicting() { + JsonElement expectedJson = JsonParser.parseString("{\"@object\": {\"@int\": \"foo\", \"@double\": \"birch\"}}"); + + Map test = Map.of( + "@int", "foo", + "@double", "birch" + ); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeNestedConflict() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"@object\": {\n" + + " \"@int\": {\n" + + " \"@object\": {\n" + + " \"@date\": {\n" + + " \"@object\": {\n" + + " \"@time\": {\n" + + " \"@object\": {\n" + + " \"@long\": {\n" + + " \"@int\": \"10\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }"); + + // Construct nested map structure with nested reserved keywords + Map innermostMap = new HashMap<>(); + innermostMap.put("@long", 10); // This should be wrapped as "@long": "@int": "10" + Map timeMap = new HashMap<>(); + timeMap.put("@time", innermostMap); // This should be wrapped into another @object due to @time conflict + Map dateMap = new HashMap<>(); + dateMap.put("@date", timeMap); // Again, wrapped due to @date conflict + Map intMap = new HashMap<>(); + intMap.put("@int", dateMap); // Top-level conflict, wrap this one too + + String encoded = FaunaEncoder.encode(intMap); + JsonElement actualJson = JsonParser.parseString(encoded); + + + assertEquals(expectedJson, actualJson); + } + + @Test + void testEncodeNonConflictingAtPrefix() { + JsonElement expectedJson = JsonParser.parseString("{\"@foo\": {\"@int\": \"10\"}}"); + Map test = new HashMap<>(); + test.put("@foo", 10); + + String encoded = FaunaEncoder.encode(test); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeComplexObject() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"bugs_coll\": {\n" + + " \"@mod\": \"Bugs\"\n" + + " },\n" + + " \"bug\": {\n" + + " \"@ref\": {\n" + + " \"id\": \"123\",\n" + + " \"coll\": {\n" + + " \"@mod\": \"Bugs\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"name\": \"fir\",\n" + + " \"age\": {\n" + + " \"@int\": \"200\"\n" + + " },\n" + + " \"birthdate\": {\n" + + " \"@date\": \"1823-02-08\"\n" + + " },\n" + + " \"molecules\": {\n" + + " \"@long\": \"999999999999999999\"\n" + + " },\n" + + " \"circumference\": {\n" + + " \"@double\": \"3.82\"\n" + + " },\n" + + " \"created_at\": {\n" + + " \"@time\": \"2003-02-08T13:28:12.555000Z\"\n" + + " },\n" + + " \"extras\": {\n" + + " \"nest\": {\n" + + " \"@object\": {\n" + + " \"@object\": {\n" + + " \"egg\": {\n" + + " \"fertilized\": false\n" + + " }\n" + + " },\n" + + " \"num_sticks\": {\n" + + " \"@int\": \"58\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"measurements\": [\n" + + " {\n" + + " \"id\": {\n" + + " \"@int\": \"1\"\n" + + " },\n" + + " \"employee\": {\n" + + " \"@int\": \"3\"\n" + + " },\n" + + " \"time\": {\n" + + " \"@time\": \"2013-02-08T12:00:05.123000Z\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"id\": {\n" + + " \"@int\": \"2\"\n" + + " },\n" + + " \"employee\": {\n" + + " \"@int\": \"5\"\n" + + " },\n" + + " \"time\": {\n" + + " \"@time\": \"2023-02-08T14:22:01.000001Z\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"); + + // Construct the complex object structure + Map complexObject = new HashMap<>(); + complexObject.put("bugs_coll", new Module("Bugs")); + complexObject.put("bug", DocumentReference.fromString("Bugs:123")); + complexObject.put("name", "fir"); + complexObject.put("age", 200); + complexObject.put("birthdate", LocalDate.of(1823, 2, 8)); + complexObject.put("molecules", 999999999999999999L); + complexObject.put("circumference", 3.82); + complexObject.put("created_at", LocalDateTime.of(2003, 2, 8, 13, 28, 12, 555000000)); + + Map nestMap = new HashMap<>(); + nestMap.put("num_sticks", 58); + nestMap.put("@object", new HashMap() {{ + put("egg", new HashMap() {{ + put("fertilized", false); + }}); + }}); + + complexObject.put("extras", new HashMap() {{ + put("nest", nestMap); + }}); + + List> measurements = Arrays.asList( + new HashMap<>() {{ + put("id", 1); + put("employee", 3); + put("time", LocalDateTime.of(2013, 2, 8, 12, 0, 5, 123000000)); + }}, + new HashMap<>() {{ + put("id", 2); + put("employee", 5); + put("time", LocalDateTime.of(2023, 2, 8, 14, 22, 1, 1000)); + }} + ); + complexObject.put("measurements", measurements); + + String encoded = FaunaEncoder.encode(complexObject); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + + } + + @Test + public void testEncodeLargeList() { + List testList = Collections.nCopies(10000, 10); + + JsonArray expectedJsonArray = new JsonArray(); + testList.forEach(value -> { + JsonObject intObject = new JsonObject(); + intObject.addProperty("@int", value.toString()); + expectedJsonArray.add(intObject); + }); + + String encoded = FaunaEncoder.encode(testList); + JsonArray parsedEncodedArray = new Gson().fromJson(encoded, JsonArray.class); + + assertEquals(expectedJsonArray, parsedEncodedArray); + } + + @Test + public void testEncodeLargeDict() { + Map testMap = new HashMap<>(); + IntStream.rangeClosed(1, 9999).forEach(i -> testMap.put("k" + i, i)); + + // Expected JSON structure + JsonObject expectedJsonObject = new JsonObject(); + testMap.forEach((key, value) -> { + JsonObject intObject = new JsonObject(); + intObject.addProperty("@int", value.toString()); + expectedJsonObject.add(key, intObject); + }); + + String encoded = FaunaEncoder.encode(testMap); + JsonObject parsedEncodedObject = new Gson().fromJson(encoded, JsonObject.class); + + assertEquals(expectedJsonObject, parsedEncodedObject); + } + + @Test + public void testEncodeDeepNestingInDict() { + Map testMap = new HashMap<>(); + testMap.put("k1", "v"); + + Map currentMap = testMap; + for (int i = 2; i < 300; i++) { + Map newMap = new HashMap<>(); + newMap.put("k" + i, "v"); + currentMap.put("k" + i, newMap); + currentMap = newMap; // Move the pointer to the new nested map + } + + String encoded = FaunaEncoder.encode(testMap); + + JsonElement parsedEncoded = gson.fromJson(encoded, JsonElement.class); + + // Recursively build the expected JSON structure + JsonObject expectedOuterObject = new JsonObject(); + JsonObject currentObject = expectedOuterObject; + for (int i = 2; i < 300; i++) { + JsonObject newObject = new JsonObject(); + newObject.addProperty("k" + i, "v"); + currentObject.add("k" + i, newObject); + currentObject = newObject; // Move the pointer to the new nested object + } + expectedOuterObject.addProperty("k1", "v"); // Add the initial pair + + assertEquals(expectedOuterObject, parsedEncoded); + } + + @Test + public void testEncodePureStringQuery() { + JsonElement expectedJson = JsonParser.parseString("{\"fql\": [\"let x = 11\"]}"); + Query query = fql("let x = 11", Map.of()); + + String encoded = FaunaEncoder.encode(query); + JsonElement actualJson = JsonParser.parseString(encoded); + + Assertions.assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodePureStringQueryWithBraces() { + JsonElement expectedJson = JsonParser.parseString("{\"fql\": [\"let x = { y: 11 }\"]}"); + Query query = fql("let x = { y: 11 }", Map.of()); + + String encoded = FaunaEncoder.encode(query); + JsonElement actualJson = JsonParser.parseString(encoded); + + Assertions.assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeQueryBuilderWithFaunaStringInterpolation() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"fql\": [\n" + + " \"let age = \",\n" + + " {\n" + + " \"value\": {\n" + + " \"@int\": \"5\"\n" + + " }\n" + + " },\n" + + " \"\\n\\\"Alice is #{age} years old.\\\"\"\n" + + " ]\n" + + " }"); + + Map args = new HashMap<>(); + args.put("n1", 5); + Query query = Query.fql("let age = ${n1}\n\"Alice is #{age} years old.\"", args); + + String encoded = FaunaEncoder.encode(query); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeQueryBuilderWithValue() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"fql\": [\n" + + " \"let x = \", {\n" + + " 'value': {\n" + + " 'name': 'Dino',\n" + + " 'age': {\n" + + " '@int': '0'\n" + + " },\n" + + " 'birthdate': {\n" + + " '@date': '2023-02-24'\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }"); + + Map user = new HashMap<>(); + user.put("name", "Dino"); + user.put("age", 0); + user.put("birthdate", LocalDate.of(2023, 2, 24)); + Map args = new HashMap<>(); + args.put("my_var", user); + Query query = Query.fql("let x = ${my_var}", args); + + String encoded = FaunaEncoder.encode(query); + JsonElement actualJson = JsonParser.parseString(encoded); + + + assertEquals(expectedJson, actualJson); + } + + @Test + public void testEncodeQueryBuilderSubQueries() { + JsonElement expectedJson = JsonParser.parseString("{\n" + + " \"fql\": [{\n" + + " \"fql\": [\n" + + " \"let x = \", {\n" + + " 'value': {\n" + + " 'name': 'Dino',\n" + + " 'age': {\n" + + " '@int': '0'\n" + + " },\n" + + " 'birthdate': {\n" + + " '@date': '2023-02-24'\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " }, \"\\nx { .name }\"]\n" + + " }"); + + Map user = new HashMap<>(); + user.put("name", "Dino"); + user.put("age", 0); + user.put("birthdate", LocalDate.of(2023, 2, 24)); + Map innerArgs = new HashMap<>(); + innerArgs.put("my_var", user); + Query innerQuery = Query.fql("let x = ${my_var}", innerArgs); + + Map outerArgs = new HashMap<>(); + outerArgs.put("inner", innerQuery); + Query outerQuery = Query.fql("${inner}\nx { .name }", outerArgs); + + String encoded = FaunaEncoder.encode(outerQuery); + JsonElement actualJson = JsonParser.parseString(encoded); + + assertEquals(expectedJson, actualJson); + } + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c42a4e49..b2f5bfa3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,6 @@ version=0.1.0 +gsonVersion=2.10.1 + junitVersion=5.10.0 mockitoVersion=5.6.0 \ No newline at end of file