Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I18n for validation error messages #2878

Merged
merged 15 commits into from Jul 22, 2022
2 changes: 1 addition & 1 deletion src/main/java/graphql/ExecutionInput.java
Expand Up @@ -225,7 +225,7 @@ public static class Builder {
//
private DataLoaderRegistry dataLoaderRegistry = DataLoaderDispatcherInstrumentationState.EMPTY_DATALOADER_REGISTRY;
private CacheControl cacheControl = CacheControl.newCacheControl();
private Locale locale;
private Locale locale = Locale.getDefault();
private ExecutionId executionId;

public Builder query(String query) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/graphql/GraphQL.java
Expand Up @@ -623,7 +623,7 @@ private List<ValidationError> validate(ExecutionInput executionInput, Document d
validationCtx.onDispatched(cf);

Predicate<Class<?>> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true);
List<ValidationError> validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate);
List<ValidationError> validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, executionInput.getLocale());

validationCtx.onCompleted(validationErrors, null);
cf.complete(validationErrors);
Expand Down
35 changes: 32 additions & 3 deletions src/main/java/graphql/ParseAndValidate.java
Expand Up @@ -9,6 +9,7 @@
import graphql.validation.Validator;

import java.util.List;
import java.util.Locale;
import java.util.function.Predicate;

/**
Expand Down Expand Up @@ -40,7 +41,7 @@ public class ParseAndValidate {
public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchema, ExecutionInput executionInput) {
ParseAndValidateResult result = parse(executionInput);
if (!result.isFailure()) {
List<ValidationError> errors = validate(graphQLSchema, result.getDocument());
List<ValidationError> errors = validate(graphQLSchema, result.getDocument(), executionInput.getLocale());
return result.transform(builder -> builder.validationErrors(errors));
}
return result;
Expand Down Expand Up @@ -71,11 +72,24 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) {
*
* @param graphQLSchema the graphql schema to validate against
* @param parsedDocument the previously parsed document
* @param locale the current locale
*
* @return a result object that indicates how this operation went
*/
public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Document parsedDocument, Locale locale) {
return validate(graphQLSchema, parsedDocument, ruleClass -> true, locale);
}

/**
* This can be called to validate a parsed graphql query, with the JVM default locale.
*
* @param graphQLSchema the graphql schema to validate against
* @param parsedDocument the previously parsed document
*
* @return a result object that indicates how this operation went
*/
public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Document parsedDocument) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. Leave the old and use Locale.getDefault() in it and delegate to the old method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right, I'll change this

return validate(graphQLSchema, parsedDocument, ruleClass -> true);
return validate(graphQLSchema, parsedDocument, ruleClass -> true, Locale.getDefault());
}

/**
Expand All @@ -84,11 +98,26 @@ public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Docume
* @param graphQLSchema the graphql schema to validate against
* @param parsedDocument the previously parsed document
* @param rulePredicate this predicate is used to decide what validation rules will be applied
* @param locale the current locale
*
* @return a result object that indicates how this operation went
*/
public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate<Class<?>> rulePredicate, Locale locale) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same breaking change- leave the old, use Locale.getDefault() and win

Validator validator = new Validator();
return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, locale);
}

/**
* This can be called to validate a parsed graphql query, with the JVM default locale.
*
* @param graphQLSchema the graphql schema to validate against
* @param parsedDocument the previously parsed document
* @param rulePredicate this predicate is used to decide what validation rules will be applied
*
* @return a result object that indicates how this operation went
*/
public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate<Class<?>> rulePredicate) {
Validator validator = new Validator();
return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate);
return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, Locale.getDefault());
}
}
70 changes: 70 additions & 0 deletions src/main/java/graphql/i18n/I18n.java
@@ -0,0 +1,70 @@
package graphql.i18n;

import graphql.Internal;
import graphql.VisibleForTesting;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import static graphql.Assert.assertNotNull;
import static graphql.Assert.assertShouldNeverHappen;

@Internal
public class I18n {

/**
* This enum is a type safe way to control what resource bundle to load from
*/
public enum BundleType {
Validation,
Execution,
General;

private final String baseName;

BundleType() {
this.baseName = "i18n." + this.name();
}
}

private final ResourceBundle resourceBundle;

@VisibleForTesting
protected I18n(BundleType bundleType, Locale locale) {
assertNotNull(bundleType);
assertNotNull(locale);
this.resourceBundle = ResourceBundle.getBundle(bundleType.baseName, locale);
}

public ResourceBundle getResourceBundle() {
return resourceBundle;
}

public static I18n i18n(BundleType bundleType, Locale locale) {
return new I18n(bundleType, locale);
}


/**
* Creates an I18N message using the key and arguments
*
* @param msgKey the key in the underlying message bundle
* @param msgArgs the message arguments
*
* @return the formatted I18N message
*/
@SuppressWarnings("UnnecessaryLocalVariable")
public String msg(String msgKey, Object... msgArgs) {
String msgPattern = null;
try {
msgPattern = resourceBundle.getString(msgKey);
} catch (MissingResourceException e) {
assertShouldNeverHappen("There must be a resource bundle key called %s", msgKey);
}

String formattedMsg = new MessageFormat(msgPattern).format(msgArgs);
return formattedMsg;
}
}
42 changes: 42 additions & 0 deletions src/main/java/graphql/i18n/I18nMsg.java
@@ -0,0 +1,42 @@
package graphql.i18n;

import java.util.ArrayList;
import java.util.List;

import static java.util.Arrays.asList;

/**
* A class that represents the intention to create a I18n message
*/
public class I18nMsg {
private final String msgKey;
private final List<Object> msgArguments;

public I18nMsg(String msgKey, List<Object> msgArguments) {
this.msgKey = msgKey;
this.msgArguments = msgArguments;
}

public I18nMsg(String msgKey, Object... msgArguments) {
this.msgKey = msgKey;
this.msgArguments = asList(msgArguments);
}

public String getMsgKey() {
return msgKey;
}

public Object[] getMsgArguments() {
return msgArguments.toArray();
}

public I18nMsg addArgumentAt(int index, Object argument) {
List<Object> newArgs = new ArrayList<>(this.msgArguments);
newArgs.add(index, argument);
return new I18nMsg(this.msgKey, newArgs);
}

public String toI18n(I18n i18n) {
return i18n.msg(msgKey, msgArguments);
}
}
2 changes: 1 addition & 1 deletion src/main/java/graphql/nextgen/GraphQL.java
Expand Up @@ -265,7 +265,7 @@ private List<ValidationError> validate(ExecutionInput executionInput, Document d
validationCtx.onDispatched(cf);

Predicate<Class<?>> validationRulePredicate = executionInput.getGraphQLContext().getOrDefault(ParseAndValidate.INTERNAL_VALIDATION_PREDICATE_HINT, r -> true);
List<ValidationError> validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate);
List<ValidationError> validationErrors = ParseAndValidate.validate(graphQLSchema, document, validationRulePredicate, executionInput.getLocale());

validationCtx.onCompleted(validationErrors, null);
cf.complete(validationErrors);
Expand Down
49 changes: 43 additions & 6 deletions src/main/java/graphql/validation/AbstractRule.java
Expand Up @@ -2,6 +2,7 @@


import graphql.Internal;
import graphql.i18n.I18nMsg;
import graphql.language.Argument;
import graphql.language.Directive;
import graphql.language.Document;
Expand All @@ -22,21 +23,20 @@
import java.util.List;

import static graphql.validation.ValidationError.newValidationError;
import static java.lang.System.arraycopy;

@Internal
public class AbstractRule {

private final ValidationContext validationContext;
private final ValidationErrorCollector validationErrorCollector;


private final ValidationUtil validationUtil;
private boolean visitFragmentSpreads;

private ValidationUtil validationUtil = new ValidationUtil();

public AbstractRule(ValidationContext validationContext, ValidationErrorCollector validationErrorCollector) {
this.validationContext = validationContext;
this.validationErrorCollector = validationErrorCollector;
this.validationUtil = new ValidationUtil();
}

public boolean isVisitFragmentSpreads() {
Expand All @@ -47,7 +47,6 @@ public void setVisitFragmentSpreads(boolean visitFragmentSpreads) {
this.visitFragmentSpreads = visitFragmentSpreads;
}


public ValidationUtil getValidationUtil() {
return validationUtil;
}
Expand Down Expand Up @@ -78,7 +77,6 @@ public List<ValidationError> getErrors() {
return validationErrorCollector.getErrors();
}


public ValidationContext getValidationContext() {
return validationContext;
}
Expand All @@ -91,6 +89,45 @@ protected List<String> getQueryPath() {
return validationContext.getQueryPath();
}

/**
* Creates an I18n message using the {@link graphql.i18n.I18nMsg}
*
* @param validationErrorType the type of validation failure
* @param i18nMsg the i18n message object
*
* @return the formatted I18n message
*/
public String i18n(ValidationErrorType validationErrorType, I18nMsg i18nMsg) {
return i18n(validationErrorType, i18nMsg.getMsgKey(), i18nMsg.getMsgArguments());
}

/**
* Creates an I18N message using the key and arguments
*
* @param validationErrorType the type of validation failure
* @param msgKey the key in the underlying message bundle
* @param msgArgs the message arguments
*
* @return the formatted I18N message
*/
public String i18n(ValidationErrorType validationErrorType, String msgKey, Object... msgArgs) {
Object[] params = new Object[msgArgs.length + 1];
params[0] = mkTypeAndPath(validationErrorType);
arraycopy(msgArgs, 0, params, 1, msgArgs.length);

return validationContext.i18n(msgKey, params);
}

private String mkTypeAndPath(ValidationErrorType validationErrorType) {
List<String> queryPath = getQueryPath();
StringBuilder sb = new StringBuilder();
sb.append(validationErrorType);
if (queryPath != null) {
sb.append("@[").append(String.join("/", queryPath)).append("]");
}
return sb.toString();
}

public void checkDocument(Document document) {

}
Expand Down
29 changes: 18 additions & 11 deletions src/main/java/graphql/validation/ArgumentValidationUtil.java
Expand Up @@ -2,6 +2,7 @@

import graphql.GraphQLError;
import graphql.Internal;
import graphql.i18n.I18nMsg;
import graphql.language.Argument;
import graphql.language.ObjectField;
import graphql.language.Value;
Expand All @@ -20,7 +21,7 @@ public class ArgumentValidationUtil extends ValidationUtil {

private final List<String> argumentNames = new ArrayList<>();
private Value<?> argumentValue;
private String errorMessage;
private String errMsgKey;
private final List<Object> arguments = new ArrayList<>();
private Map<String, Object> errorExtensions;

Expand All @@ -33,13 +34,17 @@ public ArgumentValidationUtil(Argument argument) {

@Override
protected void handleNullError(Value<?> value, GraphQLType type) {
errorMessage = "must not be null";
errMsgKey = "ArgumentValidationUtil.handleNullError";
argumentValue = value;
}

@Override
protected void handleScalarError(Value<?> value, GraphQLScalarType type, GraphQLError invalid) {
errorMessage = "is not a valid '%s' - %s";
if (invalid.getMessage() == null) {
errMsgKey = "ArgumentValidationUtil.handleScalarError";
} else {
errMsgKey = "ArgumentValidationUtil.handleScalarErrorCustomMessage";
}
arguments.add(type.getName());
arguments.add(invalid.getMessage());
argumentValue = value;
Expand All @@ -48,26 +53,30 @@ protected void handleScalarError(Value<?> value, GraphQLScalarType type, GraphQL

@Override
protected void handleEnumError(Value<?> value, GraphQLEnumType type, GraphQLError invalid) {
errorMessage = "is not a valid '%s' - %s";
if (invalid.getMessage() == null) {
errMsgKey = "ArgumentValidationUtil.handleEnumError";
} else {
errMsgKey = "ArgumentValidationUtil.handleEnumErrorCustomMessage";
}
arguments.add(type.getName());
arguments.add(invalid.getMessage());
argumentValue = value;
}

@Override
protected void handleNotObjectError(Value<?> value, GraphQLInputObjectType type) {
errorMessage = "must be an object type";
errMsgKey = "ArgumentValidationUtil.handleNotObjectError";
}

@Override
protected void handleMissingFieldsError(Value<?> value, GraphQLInputObjectType type, Set<String> missingFields) {
errorMessage = "is missing required fields '%s'";
errMsgKey = "ArgumentValidationUtil.handleMissingFieldsError";
arguments.add(missingFields);
}

@Override
protected void handleExtraFieldError(Value<?> value, GraphQLInputObjectType type, ObjectField objectField) {
errorMessage = "contains a field not in '%s': '%s'";
errMsgKey = "ArgumentValidationUtil.handleExtraFieldError";
arguments.add(type.getName());
arguments.add(objectField.getName());
}
Expand All @@ -82,7 +91,7 @@ protected void handleFieldNotValidError(Value<?> value, GraphQLType type, int in
argumentNames.add(0, String.format("[%s]", index));
}

public String getMessage() {
public I18nMsg getMsgAndArgs() {
StringBuilder argument = new StringBuilder(argumentName);
for (String name : argumentNames) {
if (name.startsWith("[")) {
Expand All @@ -94,9 +103,7 @@ public String getMessage() {
arguments.add(0, argument.toString());
arguments.add(1, argumentValue);

String message = "argument '%s' with value '%s'" + " " + errorMessage;

return String.format(message, arguments.toArray());
return new I18nMsg(errMsgKey, arguments);
}

public Map<String, Object> getErrorExtensions() {
Expand Down