Skip to content

Commit

Permalink
Added validation of Validator setup
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasstamann committed Nov 11, 2023
1 parent 37cba46 commit a5a8e22
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package io.toolisticon.fluapigen.integrationtest;

import io.toolisticon.fluapigen.api.FluentApi;
import io.toolisticon.fluapigen.api.FluentApiBackingBean;
import io.toolisticon.fluapigen.api.FluentApiBackingBeanField;
import io.toolisticon.fluapigen.api.FluentApiBackingBeanMapping;
import io.toolisticon.fluapigen.api.FluentApiCommand;
import io.toolisticon.fluapigen.api.FluentApiImplicitValue;
import io.toolisticon.fluapigen.api.FluentApiInlineBackingBeanMapping;
import io.toolisticon.fluapigen.api.FluentApiInterface;
import io.toolisticon.fluapigen.api.FluentApiParentBackingBeanMapping;
import io.toolisticon.fluapigen.api.FluentApiRoot;
import io.toolisticon.fluapigen.api.MappingAction;
import io.toolisticon.fluapigen.api.TargetBackingBean;
import io.toolisticon.fluapigen.validation.api.Matches;
import io.toolisticon.fluapigen.validation.api.NotNull;

import java.io.File;
import java.util.List;

@FluentApi("EmailFluentApiStarter")
public class EmailFluentApi {

// ---------------------------------------------------
// -- Backing Beans
// ---------------------------------------------------

/**
* The root backing bean.
*/
@FluentApiBackingBean
public interface EmailBB {

List<RecipientBB> recipients();

String subject();

String body();

List<AttachmentBB> attachments();
}

/**
* Backing bean for storing recipients
*/
@FluentApiBackingBean
public interface RecipientBB{
RecipientKind recipientKind();

String emailAddress();
}

public enum RecipientKind {
TO,
CC,
BCC;
}

/**
* Backing Bean for attachments
*/
@FluentApiBackingBean
public interface AttachmentBB {
@FluentApiBackingBeanField()
File attachment();

String attachmentName();
}

// ---------------------------------------------------
// -- Commands
// ---------------------------------------------------

@FluentApiCommand
public static class SendEmailCommand {
public static EmailBB sendEmail(EmailBB emailBB) {
// Implementation for sending the email
return emailBB;
}
}

// ---------------------------------------------------
// -- Fluent Api
// ---------------------------------------------------


@FluentApiRoot
@FluentApiInterface(EmailBB.class)
public interface EmailStartInterface {

@FluentApiInlineBackingBeanMapping("recipients")
@FluentApiImplicitValue(value="TO", id="recipientKind", target = TargetBackingBean.INLINE)
AddRecipientsOrSetSubject to (
@FluentApiBackingBeanMapping(value = "emailAddress", target = TargetBackingBean.INLINE)
@NotNull @Matches(".*[@].*") String emailAddress);

@FluentApiInlineBackingBeanMapping("recipients")
@FluentApiImplicitValue(value="CC", id="recipientKind", target = TargetBackingBean.INLINE)
AddRecipientsOrSetSubject cc (@FluentApiBackingBeanMapping(value = "emailAddress", target = TargetBackingBean.INLINE)
@NotNull @Matches(".*[@].*") String emailAddress);
@FluentApiInlineBackingBeanMapping("recipients")
@FluentApiImplicitValue(value="BCC", id="recipientKind", target = TargetBackingBean.INLINE)
AddRecipientsOrSetSubject bcc (@FluentApiBackingBeanMapping(value = "emailAddress", target = TargetBackingBean.INLINE)
@NotNull @Matches(".*[@].*") String emailAddress);
}

@FluentApiInterface(EmailBB.class)
public interface AddRecipientsOrSetSubject {
EmailStartInterface and();

AddBodyInterface withSubject (@FluentApiBackingBeanMapping(value="subject") @NotNull String subject);

}

@FluentApiInterface(EmailBB.class)
public interface AddBodyInterface {

AddAttachmentOrCloseCommandInterface withBody(@FluentApiBackingBeanMapping(value="subject") @NotNull String body) ;

}

@FluentApiInterface(EmailBB.class)
public interface AddAttachmentOrCloseCommandInterface {

AddAttachmentInterface addAttachment();


@FluentApiCommand(SendEmailCommand.class)
EmailBB sendEmail();

}


@FluentApiInterface(AttachmentBB.class)
public interface AddAttachmentFileInterface {

@FluentApiParentBackingBeanMapping(value = "attachments", action = MappingAction.ADD)
AddAttachmentOrCloseCommandInterface fromFile(@FluentApiBackingBeanMapping(value="attachment") File file);
}

@FluentApiInterface(AttachmentBB.class)
public interface AddAttachmentInterface extends AddAttachmentFileInterface{

AddAttachmentFileInterface withCustomName (@FluentApiBackingBeanMapping(value="attachmentName") String attachmentName);

}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.toolisticon.fluapigen.integrationtest;

import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;

import java.io.File;

public class EmailFluentApiTest {

@Test
public void test() {
EmailFluentApi.EmailBB emailBB = EmailFluentApiStarter.cc("[email protected]")
.and().to("[email protected]")
.and().cc("[email protected]")
.withSubject("Test")
.withBody("LOREM IPSUM")
.addAttachment().withCustomName("itWorks.png").fromFile(new File("abc.png"))
.sendEmail();

MatcherAssert.assertThat(emailBB, Matchers.notNullValue());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public boolean hasValidators() {
}

public List<ModelValidator> getValidators() {
return this.parameterElement.getAnnotations().stream().filter(e -> e.asElement().hasAnnotation(FluentApiValidator.class)).map(ModelValidator::new).collect(Collectors.toList());
return this.parameterElement.getAnnotations().stream().filter(e -> e.asElement().hasAnnotation(FluentApiValidator.class)).map(e -> new ModelValidator(this.parameterElement, e)).collect(Collectors.toList());
}

public String getParameterName() {
Expand Down Expand Up @@ -207,7 +207,14 @@ private void validateConverter(ModelBackingBeanField backBeanField) {
@DeclareCompilerMessage(code = "003", enumValueName = "BB_MAPPING_COULDNT_BE_RESOLVED", message = "Field ${0} doesn't exist in mapped backing bean ${1}. It must be one of: [${2}]", processorClass = FluentApiProcessor.class)
public boolean validate() {

boolean result = true;
// check validators
for (ModelValidator validator : this.getValidators()) {
if (!validator.validate()) {
return false;
}
}


// fluentApiBackingBeanMapping must not be null -> missing annotation
if (fluentApiBackingBeanMapping == null) {
parameterElement.compilerMessage().asError().write(
Expand Down Expand Up @@ -238,7 +245,7 @@ public boolean validate() {
return false;
}

return result;
return true;
}

}
Original file line number Diff line number Diff line change
@@ -1,48 +1,88 @@
package io.toolisticon.fluapigen.processor;

import io.toolisticon.aptk.common.ToolingProvider;
import io.toolisticon.aptk.compilermessage.api.DeclareCompilerMessage;
import io.toolisticon.aptk.tools.TypeMirrorWrapper;
import io.toolisticon.aptk.tools.corematcher.AptkCoreMatchers;
import io.toolisticon.aptk.tools.wrapper.AnnotationMirrorWrapper;
import io.toolisticon.aptk.tools.wrapper.AnnotationValueWrapper;
import io.toolisticon.aptk.tools.wrapper.ExecutableElementWrapper;
import io.toolisticon.aptk.tools.wrapper.TypeElementWrapper;
import io.toolisticon.aptk.tools.wrapper.VariableElementWrapper;

import javax.lang.model.element.ExecutableElement;
import java.util.List;
import javax.lang.model.element.Modifier;
import java.util.Arrays;
import java.util.stream.Collectors;

public class ModelValidator {


private final VariableElementWrapper annotatedElement;
private final AnnotationMirrorWrapper validatorAnnotation;

public ModelValidator(AnnotationMirrorWrapper validatorAnnotation) {
public ModelValidator(VariableElementWrapper annotatedElement, AnnotationMirrorWrapper validatorAnnotation) {
this.annotatedElement = annotatedElement;
this.validatorAnnotation = validatorAnnotation;
}

FluentApiValidatorWrapper getValidatorAnnotation() {
return FluentApiValidatorWrapper.wrap(this.validatorAnnotation.asElement().unwrap());
}

// TODO NOT USED YET, MUST BE IMPLEMENTED

@DeclareCompilerMessage(code = "752", enumValueName = "ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH", message = "The configured validator ${0} seems to have a broken annotation attribute to validator constructor parameter mapping. Attributes names ${1} doesn't exist. Please fix validator implementation or don't use it.", processorClass = FluentApiProcessor.class)
@DeclareCompilerMessage(code = "753", enumValueName = "ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING", message = "The configured validator ${0} seems to have a broken annotation attribute to validator constructor parameter mapping. Please fix validator implementation or don't use it.", processorClass = FluentApiProcessor.class)
@DeclareCompilerMessage(code = "754", enumValueName = "ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR", message = "The configured validator ${0} doesn't have a noarg constructor and therefore seems to be broken. Please fix validator implementation or don't use it.", processorClass = FluentApiProcessor.class)

public boolean validate() {
// must check if parameter types are assignable
FluentApiValidatorWrapper validatorMetaAnnotation = getValidatorAnnotation();
TypeMirrorWrapper validatorImplType = validatorMetaAnnotation.valueAsTypeMirrorWrapper();
String[] parameterNames = validatorMetaAnnotation.parameterNames();
String[] attributeNamesToConstructorParameterMapping = validatorMetaAnnotation.attributeNamesToConstructorParameterMapping();

if (attributeNamesToConstructorParameterMapping.length > 0) {

// First check if annotation attribute Names are correct
String[] invalidNames = Arrays.stream(attributeNamesToConstructorParameterMapping).filter(e -> !this.validatorAnnotation.getAttribute(e).isPresent()).toArray(String[]::new);
if (invalidNames.length > 0) {
this.annotatedElement.compilerMessage(this.validatorAnnotation.unwrap()).asError().write(FluentApiProcessorCompilerMessages.ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH, this.validatorAnnotation.asElement().getSimpleName(), invalidNames);
}


// loop over constructors and find if one is matching
outer:
for (ExecutableElementWrapper constructor : validatorImplType.getTypeElement().get().getConstructors(Modifier.PUBLIC)) {

if (constructor.getParameters().size() != attributeNamesToConstructorParameterMapping.length) {
continue;
}

int i = 0;
for (String attributeName : attributeNamesToConstructorParameterMapping) {

TypeMirrorWrapper attribute = this.validatorAnnotation.getAttributeTypeMirror(attributeName).get();

if (parameterNames.length > 0) {
if (!attribute.isAssignableTo(constructor.getParameters().get(i).asType())) {
continue outer;
}

} else if (!validatorImplType.getTypeElement().isPresent()) {
// couldn't get type element
// next
i = i + 1;
}

// if this is reached, the we have found a matching constructor
return true;
}

annotatedElement.compilerMessage(validatorAnnotation.unwrap()).asError().write(FluentApiProcessorCompilerMessages.ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR, validatorImplType.getSimpleName());
return false;
} else {
// must have a noarg constructor or just the default
TypeElementWrapper validatorImplTypeElement = validatorImplType.getTypeElement().get();
boolean hasNoargConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).applyFilter(AptkCoreMatchers.HAS_NO_PARAMETERS).getResult().size() == 1;
boolean hasJustDefaultConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).hasSize(0);

if (! ( hasNoargConstructor || hasJustDefaultConstructor)) {
//ToolingProvider.getTooling().getMessager().
//this.validatorAnnotation.
if (!(hasNoargConstructor || hasJustDefaultConstructor)) {
annotatedElement.compilerMessage(validatorAnnotation.unwrap()).asError().write(FluentApiProcessorCompilerMessages.ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR, validatorImplTypeElement.getSimpleName());
}
}

Expand All @@ -55,7 +95,7 @@ public String validatorExpression() {
stringBuilder.append("new ").append(getValidatorAnnotation().valueAsFqn()).append("(");

boolean isFirst = true;
for (String parameterName : getValidatorAnnotation().parameterNames()) {
for (String attributeName : getValidatorAnnotation().attributeNamesToConstructorParameterMapping()) {

// add separator
if (!isFirst) {
Expand All @@ -64,34 +104,41 @@ public String validatorExpression() {
isFirst = false;
}

AnnotationValueWrapper attribute = validatorAnnotation.getAttributeWithDefault(parameterName);

if (attribute.isString()) {
stringBuilder.append("\"").append(attribute.getStringValue()).append("\"");
} else if (attribute.isClass()) {
stringBuilder.append(attribute.getClassValue().getQualifiedName()).append(".class");
} else if (attribute.isInteger()) {
stringBuilder.append(attribute.getIntegerValue());
} else if (attribute.isLong()) {
stringBuilder.append(attribute.getLongValue()).append("L");
} else if (attribute.isBoolean()) {
stringBuilder.append(attribute.getBooleanValue());
} else if (attribute.isFloat()) {
stringBuilder.append(attribute.getFloatValue()).append("f");
} else if (attribute.isDouble()) {
stringBuilder.append(attribute.getFloatValue());
} else if (attribute.isEnum()) {
stringBuilder.append(TypeElementWrapper.toTypeElement(attribute.getEnumValue().getEnclosingElement().get()).getQualifiedName());
stringBuilder.append(".");
stringBuilder.append(attribute.getEnumValue().getSimpleName());
}
stringBuilder.append(getValidatorExpressionAttributeValueStringRepresentation(validatorAnnotation.getAttributeWithDefault(attributeName), validatorAnnotation.getAttributeTypeMirror(attributeName).get()));

}

stringBuilder.append(")");
return stringBuilder.toString();
}


String getValidatorExpressionAttributeValueStringRepresentation(AnnotationValueWrapper annotationValueWrapper, TypeMirrorWrapper annotationAttributeTypeMirror) {

if (annotationValueWrapper.isArray()) {
return annotationValueWrapper.getArrayValue().stream().map(e -> getValidatorExpressionAttributeValueStringRepresentation(e, annotationAttributeTypeMirror.getWrappedComponentType())).collect(Collectors.joining(", ", "new " + annotationAttributeTypeMirror.getWrappedComponentType().getQualifiedName() + "[]{", "}"));
} else if (annotationValueWrapper.isString()) {
return "\"" + annotationValueWrapper.getStringValue() + "\"";
} else if (annotationValueWrapper.isClass()) {
return annotationValueWrapper.getClassValue().getQualifiedName() + ".class";
} else if (annotationValueWrapper.isInteger()) {
return annotationValueWrapper.getIntegerValue().toString();
} else if (annotationValueWrapper.isLong()) {
return annotationValueWrapper.getLongValue() + "L";
} else if (annotationValueWrapper.isBoolean()) {
return annotationValueWrapper.getBooleanValue().toString();
} else if (annotationValueWrapper.isFloat()) {
return annotationValueWrapper.getFloatValue() + "f";
} else if (annotationValueWrapper.isDouble()) {
return annotationValueWrapper.getDoubleValue().toString();
} else if (annotationValueWrapper.isEnum()) {
return TypeElementWrapper.toTypeElement(annotationValueWrapper.getEnumValue().getEnclosingElement().get()).getQualifiedName() + "." + annotationValueWrapper.getEnumValue().getSimpleName();
} else {
throw new IllegalStateException("Got unsupported annotation attribute type : USUALLY THIS CANNOT HAPPEN.");
}

}

public String getValidatorSummary() {
return validatorAnnotation.getStringRepresentation();
}
Expand Down
Loading

0 comments on commit a5a8e22

Please sign in to comment.