Skip to content

Commit

Permalink
BT-4366 Encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanAlfer committed Nov 9, 2023
1 parent fae4f08 commit f639c8c
Show file tree
Hide file tree
Showing 26 changed files with 2,072 additions and 4 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
40 changes: 40 additions & 0 deletions faunaJava/src/main/java/com/fauna/encoding/DateWrapper.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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()));
}

}
38 changes: 38 additions & 0 deletions faunaJava/src/main/java/com/fauna/encoding/DoubleWrapper.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
239 changes: 239 additions & 0 deletions faunaJava/src/main/java/com/fauna/encoding/FaunaEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
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.
* <p>
* Supported type conversions are as follows:
* <ul>
* <li>{@code Map} objects to {@code @object} for Fauna.</li>
* <li>{@code List} objects to arrays for Fauna.</li>
* <li>{@code String} objects to strings for Fauna.</li>
* <li>{@code Integer} values (32-bit signed) to {@code @int} for Fauna.</li>
* <li>{@code Long} values (64-bit signed) to {@code @long} for Fauna.</li>
* <li>{@code Double} values to {@code @double} for Fauna.</li>
* <li>{@code LocalDateTime} values to {@code @time} for Fauna.</li>
* <li>{@code LocalDate} values to {@code @date} for Fauna.</li>
* <li>{@code Boolean} values {@code true} and {@code false} are preserved as is for Fauna.</li>
* <li>{@code null} values are preserved as {@code None} for Fauna.</li>
* <li>{@code Document} instances to {@code @ref} for Fauna.</li>
* <li>{@code DocumentReference} instances to {@code @ref} for Fauna.</li>
* <li>{@code Module} instances to {@code @mod} for Fauna.</li>
* <li>{@code Query} instances to {@code fql} for Fauna.</li>
* <li>{@code ValueFragment} instances to {@code value} for Fauna.</li>
* <li>And literal string representations of queries to {@code string} for Fauna.</li>
* </ul>
* <p>
* 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<String> 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) {
if (value instanceof List) {
return gson.toJson(encodeList((List<?>) value));
}
if (value instanceof Map) {
return gson.toJson(encodeMap((Map<?, ?>) 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<Object> encodeList(List<?> list) {
List<Object> 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<String, Object> encodeMap(Map<?, ?> map) {
Map<String, Object> 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<String, Object> 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<String, List<Object>> 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());

}

}
Loading

0 comments on commit f639c8c

Please sign in to comment.