Skip to content

Commit

Permalink
Merge pull request #143 from fauna/implement-error-parsers
Browse files Browse the repository at this point in the history
[Implement-error-parsers] Refactor to use JsonParser instead of ObjectMapper for parsing errors.
  • Loading branch information
findgriffin authored Sep 16, 2024
2 parents 7004bf1 + 6acec51 commit ca80aef
Show file tree
Hide file tree
Showing 15 changed files with 649 additions and 61 deletions.
2 changes: 2 additions & 0 deletions src/main/java/com/fauna/constants/ResponseFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public final class ResponseFields {
public static final String ERROR_MESSAGE_FIELD_NAME = "message";
public static final String ERROR_CONSTRAINT_FAILURES_FIELD_NAME = "constraint_failures";
public static final String ERROR_ABORT_FIELD_NAME = "abort";
public static final String ERROR_NAME_FIELD_NAME = "name";
public static final String ERROR_PATHS_FIELD_NAME = "paths";

// Stream-related fields
public static final String STREAM_TYPE_FIELD_NAME = "type";
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/fauna/exception/ServiceException.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fauna.exception;

import java.util.Map;
import java.util.Optional;

import com.fauna.response.QueryFailure;
import com.fauna.response.QueryStats;
Expand Down Expand Up @@ -76,12 +77,12 @@ public QueryStats getStats() {
}

/**
* Returns the faled query's last transaction timestamp.
* Returns the failed query's last transaction timestamp.
*
* @return the transaction timestamp as a long value
*/
public long getTxnTs() {
return this.response.getLastSeenTxn();
public Optional<Long> getTxnTs() {
return Optional.ofNullable(this.response.getLastSeenTxn());
}

/**
Expand Down
158 changes: 154 additions & 4 deletions src/main/java/com/fauna/response/ConstraintFailure.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,161 @@
package com.fauna.response;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fauna.constants.ResponseFields;
import com.fauna.exception.ClientResponseException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class ConstraintFailure {
private final String message;

private final String name;

private final List<List<Object>> paths;
private final PathElement[][] paths;

public ConstraintFailure(String message, String name, List<List<Object>> paths) {
// This constructor is called by the QueryFailure constructor, which is getting deprecated.
@Deprecated
public ConstraintFailure(String message, String name, List<List<Object>> pathLists) {
this.message = message;
this.name = name;
this.paths = new PathElement[pathLists.size()][];
for (int i = 0; i < pathLists.size(); i++) {
this.paths[i] = new PathElement[pathLists.get(i).size()];
for (int j = 0; j < this.paths[i].length; j++) {
Object element = pathLists.get(i).get(j);
if (element instanceof String) {
paths[i][j] = new PathElement((String) element);
} else if (element instanceof Integer) {
paths[i][j] = new PathElement((Integer) element);
}
}
}
}

public ConstraintFailure(String message, String name, PathElement[][] paths) {
this.message = message;
this.name = name;
this.paths = paths;
}

public static class PathElement {
private String sVal = null;
private Integer iVal = null;

public PathElement(String sVal) {
this.sVal = sVal;
}

public PathElement(Integer iVal) {
this.iVal = iVal;
}

public boolean isString() {
return sVal != null;
}

/**
* Note that this parser does not advance the parser.
* @param parser
* @return
* @throws IOException
*/
public static PathElement parse(JsonParser parser) throws IOException {
if (parser.currentToken().isNumeric()) {
return new PathElement(parser.getValueAsInt());
} else {
return new PathElement(parser.getText());
}
}

@Override
public boolean equals(Object o) {
if (o instanceof PathElement) {
PathElement other = (PathElement) o;
return other.isString() == this.isString() && other.toString().equals(this.toString());
} else {
return false;
}
}

public String toString() {
return sVal == null ? String.valueOf(iVal) : sVal;
}
}


public static class Builder {
String message = null;
String name = null;
PathElement[][] paths;

public Builder message(String message) {
this.message = message;
return this;
}

public Builder name(String name) {
this.name = name;
return this;
}

public Builder paths(PathElement[][] paths) {
this.paths = paths;
return this;
}

public ConstraintFailure build() {
return new ConstraintFailure(this.message, this.name, this.paths);
}

}

public static Builder builder() {
return new Builder();
}

public static ConstraintFailure parse(JsonParser parser) throws IOException {
if (parser.currentToken() != JsonToken.START_OBJECT && parser.nextToken() != JsonToken.START_OBJECT) {
throw new ClientResponseException("Constraint failure should be a JSON object.");
}
Builder builder = ConstraintFailure.builder();
while (parser.nextToken() == JsonToken.FIELD_NAME) {
String fieldName = parser.getValueAsString();
switch (fieldName) {
case ResponseFields.ERROR_MESSAGE_FIELD_NAME:
builder.message(parser.nextTextValue());
break;
case ResponseFields.ERROR_NAME_FIELD_NAME:
builder.name(parser.nextTextValue());
break;
case ResponseFields.ERROR_PATHS_FIELD_NAME:
List<PathElement[]> paths = new ArrayList<>();
JsonToken firstPathToken = parser.nextToken();
if (firstPathToken == JsonToken.START_ARRAY) {
while (parser.nextToken() == JsonToken.START_ARRAY) {
List<PathElement> path = new ArrayList<>();
while (parser.nextToken() != JsonToken.END_ARRAY) {
path.add(PathElement.parse(parser));
}
paths.add(path.toArray(new PathElement[path.size()]));
}
builder.paths(paths.toArray(new PathElement[paths.size()][]));
} else if (firstPathToken != JsonToken.VALUE_NULL) {
throw new ClientResponseException("Constraint failure path should be array or null, got: " + firstPathToken.toString());
}
break;
}
}
return builder.build();

}

public String getMessage() {
return this.message;
}
Expand All @@ -24,8 +164,18 @@ public Optional<String> getName() {
return Optional.ofNullable(this.name);
}

public List<List<Object>> getPaths() {
return paths != null ? paths : List.of();
public Optional<PathElement[][]> getPaths() {
return Optional.ofNullable(paths);
}

public Optional<List<String>> getPathStrings() {
if (paths == null) {
return Optional.empty();
} else {
return Optional.of(Arrays.stream(paths).map(
pathElements -> Arrays.stream(pathElements).map(PathElement::toString).collect(
Collectors.joining("."))).collect(Collectors.toList()));
}
}

}
120 changes: 119 additions & 1 deletion src/main/java/com/fauna/response/ErrorInfo.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
package com.fauna.response;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fauna.codec.Codec;
import com.fauna.codec.DefaultCodecProvider;
import com.fauna.codec.UTF8FaunaParser;
import com.fauna.exception.ClientResponseException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static com.fauna.constants.ResponseFields.ERROR_ABORT_FIELD_NAME;
import static com.fauna.constants.ResponseFields.ERROR_CODE_FIELD_NAME;
import static com.fauna.constants.ResponseFields.ERROR_CONSTRAINT_FAILURES_FIELD_NAME;
import static com.fauna.constants.ResponseFields.ERROR_MESSAGE_FIELD_NAME;

/**
* This class will encapsulate all the information Fauna returns about errors including constraint failures, and
* abort data, for now it just has the code and message.
*/
public class ErrorInfo {
private final String code;
private final String message;
private final ConstraintFailure[] constraintFailures;
private final TreeNode abort;

public ErrorInfo(String code, String message) {
public ErrorInfo(String code, String message, ConstraintFailure[] constraintFailures, TreeNode abort) {
this.code = code;
this.message = message;
this.constraintFailures = constraintFailures;
this.abort = abort;
}

public String getCode() {
Expand All @@ -20,4 +43,99 @@ public String getCode() {
public String getMessage() {
return message;
}

public Optional<ConstraintFailure[]> getConstraintFailures() {
return Optional.ofNullable(this.constraintFailures);
}

public Optional<TreeNode> getAbortJson() {
return Optional.ofNullable(this.abort);
}

public <T> Optional<T> getAbort(Class<T> abortDataClass) {
return this.getAbortJson().map(tree -> {
UTF8FaunaParser parser = new UTF8FaunaParser(tree.traverse());
Codec<T> codec = DefaultCodecProvider.SINGLETON.get(abortDataClass);
parser.read();
return codec.decode(parser);
});
}



public static class Builder {
String code = null;
String message = null;
ConstraintFailure[] constraintFailures = null;
TreeNode abort = null;

public Builder code(String code) {
this.code = code;
return this;
}

public Builder message(String message) {
this.message = message;
return this;
}

public Builder abort(TreeNode abort) {
this.abort = abort;
return this;
}

public Builder constraintFailures(List<ConstraintFailure> constraintFailures) {
this.constraintFailures = constraintFailures.toArray(new ConstraintFailure[constraintFailures.size()]);
return this;
}

public ErrorInfo build() {
return new ErrorInfo(this.code, this.message, this.constraintFailures, this.abort);
}
}

public static Builder builder() {
return new Builder();
}

public static ErrorInfo parse(JsonParser parser) throws IOException {
if (parser.nextToken() != JsonToken.START_OBJECT) {
throw new ClientResponseException("Error parsing error info, got token" + parser.currentToken());
}
Builder builder = ErrorInfo.builder();

while (parser.nextToken() == JsonToken.FIELD_NAME) {
String fieldName = parser.getCurrentName();
switch (fieldName) {
case ERROR_CODE_FIELD_NAME:
builder.code(parser.nextTextValue());
break;
case ERROR_MESSAGE_FIELD_NAME:
builder.message(parser.nextTextValue());
break;
case ERROR_ABORT_FIELD_NAME:
JsonToken firstAbortToken = parser.nextToken();
builder.abort(new ObjectMapper().readTree(parser));
break;
case ERROR_CONSTRAINT_FAILURES_FIELD_NAME:
List<ConstraintFailure> failures = new ArrayList<>();
JsonToken token = parser.nextToken();
if (token == JsonToken.VALUE_NULL) {
break;
} else if (token == JsonToken.START_ARRAY) {
JsonToken nextToken = parser.nextToken();
while (nextToken == JsonToken.START_OBJECT) {
failures.add(ConstraintFailure.parse(parser));
nextToken = parser.nextToken();
}
builder.constraintFailures(failures);
break;
} else {
throw new ClientResponseException("Unexpected token in constraint failures: " + token);
}
default: throw new ClientResponseException("Unexpected token in error info: " + parser.currentToken());
}
}
return builder.build();
}
}
Loading

0 comments on commit ca80aef

Please sign in to comment.