Skip to content

Add Accept Language header to validation #234

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

Merged
merged 2 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions http-api/src/main/java/io/avaje/http/api/Valid.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@
import java.lang.annotation.Target;

/**
* Add {@code @Valid} annotation on a controller/method/BeanParam that we want bean validation to
* be included for. When we do this controller methods that take a request payload will then have
* the request bean (populated by JSON payload or form parameters) validated before it is passed
* to the controller method.
* <p>
* When trying to validate a {@code @BeanParam} bean, this will need to be placed on the BeanParam type.
* <p>
* When using this annotation we need to provide an implementation of {@link Validator} to use.
* <p>
* Alternatively we can use the Jakarta {@code @Valid} along with a Jakarta validator implementation.
* Add {@code @Valid} annotation on a controller/method/BeanParam that we want bean validation to be
* included for. When we do this controller methods that take a request payload will then have the
* request bean (populated by JSON payload or form parameters) validated before it is passed to the
* controller method.
*
* <p>When trying to validate a {@code @BeanParam} bean, this will need to be placed on the
* BeanParam type.
*
* <p>When using this annotation we need to provide an implementation of {@link Validator} to use.
*
* <p>Alternatively we can use the Jakarta {@code @Valid} along with a Jakarta validator
* implementation.
*/
@Retention(SOURCE)
@Target({METHOD, TYPE, PARAMETER})
public @interface Valid {

/** Validation groups to use */
Class<?>[] groups() default {};
}
25 changes: 18 additions & 7 deletions http-api/src/main/java/io/avaje/http/api/Validator.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package io.avaje.http.api;

/**
* Validator for form beans or request beans.
*/
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;

/** Validator for form beans or request beans. */
public interface Validator {

/**
* Validate the bean throwing an exception if the bean fails validation.
* <p>
* Typically the exception will be handled by a specific exception handler
* returning a 422 or 400 status code and usually a map of field paths to error messages.
*
* <p>Typically the exception will be handled by a specific exception handler returning a 422 or
* 400 status code and usually a map of field paths to error messages.
*
* @param bean The bean to validate
*/
void validate(Object bean);
void validate(Object bean, String acceptLanguage, Class<?>... groups) throws ValidationException;

default Locale resolveLocale(String acceptLanguage, Collection<Locale> acceptLocales) {
if (acceptLanguage == null) {
return null;
}
final List<LanguageRange> list = LanguageRange.parse(acceptLanguage);
return Locale.lookup(list, acceptLocales);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ public void registerRoutes() {

ApiBuilder.post("/hello", ctx -> {
ctx.status(201);
HelloDto dto = ctx.bodyAsClass(HelloDto.class);
validator.validate(dto);
final HelloDto dto = ctx.bodyAsClass(HelloDto.class);
validator.validate(dto, "en-us");
ctx.json(controller.post(dto));
});

ApiBuilder.post("/hello/savebean/{foo}", ctx -> {
ctx.status(201);
String foo = ctx.pathParam("foo");
HelloDto dto = ctx.bodyAsClass(HelloDto.class);
validator.validate(dto);
final String foo = ctx.pathParam("foo");
final HelloDto dto = ctx.bodyAsClass(HelloDto.class);
validator.validate(dto, "en-us");
controller.saveBean(foo, dto, ctx);
});

Expand All @@ -86,7 +86,7 @@ public void registerRoutes() {
helloForm.url = ctx.formParam("url");
helloForm.startDate = toLocalDate(ctx.formParam("startDate"));

validator.validate(helloForm);
validator.validate(helloForm, "en-us");
controller.saveForm(helloForm);
});

Expand All @@ -107,7 +107,7 @@ public void registerRoutes() {
helloForm.url = ctx.formParam("url");
helloForm.startDate = toLocalDate(ctx.formParam("startDate"));

validator.validate(helloForm);
validator.validate(helloForm, "en-us");
ctx.json(controller.saveForm3(helloForm));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,20 @@ public boolean isBodyMethodParam() {
public String indent() {
return null;
}

// these have no meaning for client adapters
@Override
public void controllerRoles(List<String> roles, ControllerReader controller) {

}
public void controllerRoles(List<String> roles, ControllerReader controller) {}

@Override
public void methodRoles(List<String> roles, ControllerReader controller) {

}
public void methodRoles(List<String> roles, ControllerReader controller) {}

@Override
public void writeReadParameter(Append writer, ParamType paramType, String paramName) {

}
public void writeReadParameter(Append writer, ParamType paramType, String paramName) {}

@Override
public void writeReadParameter(Append writer, ParamType paramType, String paramName, String paramDefault) {
public void writeReadParameter(
Append writer, ParamType paramType, String paramName, String paramDefault) {}

}
@Override
public void writeAcceptLanguage(Append writer) {}
}
2 changes: 1 addition & 1 deletion http-generator-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<artifactId>avaje-http-generator-core</artifactId>

<properties>
<avaje.prisms.version>1.9</avaje.prisms.version>
<avaje.prisms.version>1.10</avaje.prisms.version>
</properties>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Constants {
static final String IMPORT_HTTP_API = "io.avaje.http.api.*";
static final String VALIDATOR = "io.avaje.http.api.Validator";
public static final String META_INF_COMPONENT = "META-INF/services/io.avaje.http.client.HttpClient$GeneratedComponent";
public static final String ACCEPT_LANGUAGE = "Accept-Language";


}
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,7 @@ private boolean initDocHidden() {

private boolean initHasValid() {

return findAnnotation(JavaxValidPrism::getOptionalOn).isPresent()
|| findAnnotation(JakartaValidPrism::getOptionalOn).isPresent()
|| findAnnotation(ValidPrism::getOptionalOn).isPresent();
return findAnnotation(ValidPrism::getOptionalOn).isPresent();
}

String produces() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import static io.avaje.http.generator.core.ParamType.RESPONSE_HANDLER;
import static io.avaje.http.generator.core.ProcessingContext.platform;
import static io.avaje.http.generator.core.ProcessingContext.typeElement;
import static java.util.function.Predicate.not;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;

import io.avaje.http.generator.core.openapi.MethodDocBuilder;
import io.avaje.http.generator.core.openapi.MethodParamDocBuilder;
Expand Down Expand Up @@ -39,6 +42,8 @@ public class ElementReader {
private boolean isParamMap;
private final Set<String> imports = new HashSet<>();

private final List<String> validationGroups = new ArrayList<>();

ElementReader(Element element, ParamType defaultType, boolean formMarker) {
this(element, null, Util.typeDef(element.asType()), defaultType, formMarker);
}
Expand Down Expand Up @@ -70,6 +75,11 @@ public class ElementReader {
if (!contextType) {
readAnnotations(element, defaultType);
useValidation = useValidation();
HttpValidPrism.getOptionalOn(element).map(HttpValidPrism::groups).stream()
.flatMap(List::stream)
.map(TypeMirror::toString)
.forEach(validationGroups::add);
this.imports.addAll(validationGroups);
} else {
paramType = ParamType.CONTEXT;
useValidation = false;
Expand Down Expand Up @@ -138,10 +148,7 @@ private boolean useValidation() {
return false;
}
final var elementType = typeElement(rawType);
return elementType != null
&& (ValidPrism.isPresent(elementType)
|| JavaxValidPrism.isPresent(elementType)
|| JakartaValidPrism.isPresent(elementType));
return elementType != null && ValidPrism.isPresent(elementType);
}

private void readAnnotations(Element element, ParamType defaultType) {
Expand Down Expand Up @@ -276,7 +283,14 @@ void buildApiDocumentation(MethodDocBuilder methodDoc) {
void writeValidate(Append writer) {
if (!contextType && typeHandler == null) {
if (useValidation) {
writer.append("validator.validate(%s);", varName).eol();
writer.append("validator.validate(%s, ", varName);
platform().writeAcceptLanguage(writer);

if (!validationGroups.isEmpty()) {
validationGroups.forEach(g -> writer.append(", %s", Util.shortName(g)));
}

writer.append(");").eol();
} else {
writer.append("// no validation required on %s", varName).eol();
}
Expand All @@ -290,7 +304,7 @@ void writeCtxGet(Append writer, PathSegments segments) {
// body passed as method parameter (Helidon)
return;
}
String shortType = handlerShortType();
final String shortType = handlerShortType();
writer.append("%s var %s = ", platform().indent(), varName);
if (setValue(writer, segments, shortType)) {
writer.append(";").eol();
Expand Down Expand Up @@ -320,12 +334,12 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
return false;
}
if (impliedParamType) {
var name = matrixParamName != null ? matrixParamName : varName;
PathSegments.Segment segment = segments.segment(name);
final var name = matrixParamName != null ? matrixParamName : varName;
final PathSegments.Segment segment = segments.segment(name);
if (segment != null) {
// path or matrix parameter
boolean requiredParam = segment.isRequired(varName);
String asMethod =
final boolean requiredParam = segment.isRequired(varName);
final String asMethod =
(typeHandler == null)
? null
: (requiredParam) ? typeHandler.asMethod() : typeHandler.toMethod();
Expand All @@ -341,7 +355,7 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
}
}

String asMethod = (typeHandler == null) ? null : typeHandler.toMethod();
final String asMethod = (typeHandler == null) ? null : typeHandler.toMethod();
if (asMethod != null) {
writer.append(asMethod);
}
Expand Down Expand Up @@ -380,8 +394,8 @@ private boolean setValue(Append writer, PathSegments segments, String shortType)
}

private void writeForm(Append writer, String shortType, String varName, ParamType defaultParamType) {
TypeElement formBeanType = typeElement(rawType);
BeanParamReader form = new BeanParamReader(formBeanType, varName, shortType, defaultParamType);
final TypeElement formBeanType = typeElement(rawType);
final BeanParamReader form = new BeanParamReader(formBeanType, varName, shortType, defaultParamType);
form.write(writer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,12 @@ private Javadoc buildJavadoc(ExecutableElement element) {
}

private boolean initValid() {
return findAnnotation(ValidPrism::getOptionalOn).isPresent()
|| findAnnotation(JavaxValidPrism::getOptionalOn).isPresent()
|| findAnnotation(JakartaValidPrism::getOptionalOn).isPresent()
|| superMethodHasValid();
return findAnnotation(ValidPrism::getOptionalOn).isPresent() || superMethodHasValid();
}

private boolean superMethodHasValid() {
return superMethods.stream()
.anyMatch(
e ->
findAnnotation(ValidPrism::getOptionalOn).isPresent()
|| findAnnotation(JavaxValidPrism::getOptionalOn).isPresent());
.anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public interface PlatformAdapter {
void writeReadParameter(
Append writer, ParamType paramType, String paramName, String paramDefault);

void writeAcceptLanguage(Append writer);

default void writeReadMapParameter(Append writer, ParamType paramType) {
throw new UnsupportedOperationException("Unsupported Map Parameter");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.avaje.http.generator.core;

import java.util.Optional;

import javax.lang.model.element.Element;

import io.avaje.prism.GeneratePrism;

@GeneratePrism(
value = javax.validation.Valid.class,
name = "JavaxValidPrism",
superInterfaces = ValidPrism.class)
@GeneratePrism(
value = jakarta.validation.Valid.class,
name = "JakartaValidPrism",
superInterfaces = ValidPrism.class)
@GeneratePrism(
value = io.avaje.http.api.Valid.class,
name = "HttpValidPrism",
superInterfaces = ValidPrism.class)
public interface ValidPrism {

static Optional<ValidPrism> getOptionalOn(Element e) {
return Optional.<ValidPrism>empty()
.or(() -> HttpValidPrism.getOptionalOn(e))
.or(() -> JakartaValidPrism.getOptionalOn(e))
.or(() -> JavaxValidPrism.getOptionalOn(e));
}

static boolean isPresent(Element e) {
return JakartaValidPrism.isPresent(e)
|| JavaxValidPrism.isPresent(e)
|| HttpValidPrism.isPresent(e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@
@GeneratePrism(value = io.avaje.http.api.Client.class, publicAccess = true)
@GeneratePrism(value = io.avaje.http.api.Client.Import.class, publicAccess = true)
@GeneratePrism(value = io.avaje.http.api.RequestTimeout.class, publicAccess = true)
@GeneratePrism(value = javax.validation.Valid.class, name = "JavaxValidPrism")
@GeneratePrism(value = jakarta.validation.Valid.class, name = "JakartaValidPrism")
@GeneratePrism(value = io.avaje.http.api.Valid.class)
package io.avaje.http.generator.core;

import io.avaje.prism.GeneratePrism;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;

import io.avaje.http.generator.core.Append;
import io.avaje.http.generator.core.Constants;
import io.avaje.http.generator.core.ControllerReader;
import io.avaje.http.generator.core.ParamType;
import io.avaje.http.generator.core.PlatformAdapter;
Expand Down Expand Up @@ -168,4 +169,9 @@ public void writeReadCollectionParameter(
throw new UnsupportedOperationException("Unsupported MultiValue Parameter");
}
}

@Override
public void writeAcceptLanguage(Append writer) {
writer.append("req.headers().first(\"%s\").orElse(null)", Constants.ACCEPT_LANGUAGE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;

import io.avaje.http.generator.core.Append;
import io.avaje.http.generator.core.Constants;
import io.avaje.http.generator.core.ControllerReader;
import io.avaje.http.generator.core.ParamType;
import io.avaje.http.generator.core.PlatformAdapter;
Expand Down Expand Up @@ -108,4 +109,9 @@ public void writeReadCollectionParameter(
}
writer.append("withDefault(ctx.queryParams(\"%s\"), java.util.List.of(\"%s\"))", paramName, String.join(",", paramDefault));
}

@Override
public void writeAcceptLanguage(Append writer) {
writer.append("ctx.header(\"%s\")", Constants.ACCEPT_LANGUAGE);
}
}
Loading