From ddec85f1658e3fe29a347b6f02c55a4e1e0521e8 Mon Sep 17 00:00:00 2001 From: Anton Duyun Date: Thu, 13 Apr 2023 20:01:50 +0300 Subject: [PATCH] Validator support for sealed interfaces (#123) --- .../AbstractAnnotationProcessorTest.java | 57 ++-- .../tinkoff/kora/ksp/common/KspCommonUtils.kt | 4 + .../ksp/common/AbstractSymbolProcessorTest.kt | 47 ++- .../processor/ValidAnnotationProcessor.java | 214 ++------------ .../annotation/processor/ValidMeta.java | 3 +- .../processor/ValidatorGenerator.java | 271 +++++++++++++++++ .../extension/ValidKoraExtension.java | 50 +++- .../processor/ValidationExtensionTest.java | 27 ++ .../processor/ValidationSealedTypeTest.java | 99 +++++++ .../validation/symbol/processor/ValidMeta.kt | 27 +- .../symbol/processor/ValidSymbolProcessor.kt | 236 +-------------- .../processor/ValidSymbolProcessorProvider.kt | 2 - .../validation/symbol/processor/ValidUtils.kt | 7 +- .../symbol/processor/ValidatorGenerator.kt | 279 ++++++++++++++++++ .../processor/extension/ValidKoraExtension.kt | 19 +- .../extension/ValidKoraExtensionFactory.kt | 2 +- .../processor/ValidationExtensionTest.kt | 70 +++++ .../processor/ValidatorSealedTypeTest.kt | 95 ++++++ 18 files changed, 985 insertions(+), 524 deletions(-) create mode 100644 validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidatorGenerator.java create mode 100644 validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationSealedTypeTest.java create mode 100644 validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorGenerator.kt create mode 100644 validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidationExtensionTest.kt create mode 100644 validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorSealedTypeTest.kt diff --git a/annotation-processor-common/src/testFixtures/java/ru/tinkoff/kora/annotation/processor/common/AbstractAnnotationProcessorTest.java b/annotation-processor-common/src/testFixtures/java/ru/tinkoff/kora/annotation/processor/common/AbstractAnnotationProcessorTest.java index 0c4bcacae..33d4d75a0 100644 --- a/annotation-processor-common/src/testFixtures/java/ru/tinkoff/kora/annotation/processor/common/AbstractAnnotationProcessorTest.java +++ b/annotation-processor-common/src/testFixtures/java/ru/tinkoff/kora/annotation/processor/common/AbstractAnnotationProcessorTest.java @@ -19,11 +19,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; +import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.IntStream; @TestInstance(TestInstance.Lifecycle.PER_METHOD) public abstract class AbstractAnnotationProcessorTest { @@ -77,37 +76,25 @@ protected CompileResult compile(List processors, @Language("java") St var commonImports = this.commonImports(); var sourceList = Arrays.stream(sources).map(s -> "package %s;\n%s\n/**\n* @see %s#%s \n*/\n".formatted(testPackage, commonImports, testClass.getCanonicalName(), testMethod.getName()) + s) .map(s -> { - var classStart = s.indexOf("public sealed interface ") + 24; - if (classStart < 24) { - classStart = s.indexOf("public class ") + 13; - if (classStart < 13) { - classStart = s.indexOf("public final class ") + 19; - if (classStart < 19) { - classStart = s.indexOf("public interface ") + 17; - if (classStart < 17) { - classStart = s.indexOf("public @interface ") + 18; - if (classStart < 18) { - classStart = s.indexOf("public record ") + 14; - if (classStart < 14) { - classStart = s.indexOf("public enum ") + 12; - if (classStart < 12) { - throw new IllegalArgumentException(); - } - } - } - } - } - } - } - var firstSpace = s.indexOf(" ", classStart + 1); - var firstBracket = s.indexOf("(", classStart + 1); - var firstSquareBracket = s.indexOf("{", classStart + 1); - var classEnd = Math.min(firstSpace >= 0 ? firstSpace : Integer.MAX_VALUE, Math.min( - firstBracket >= 0 ? firstBracket : Integer.MAX_VALUE, - firstSquareBracket >= 0 ? firstSquareBracket : Integer.MAX_VALUE - )); - var className = s.substring(classStart, classEnd); - return new ByteArrayJavaFileObject(JavaFileObject.Kind.SOURCE, testPackage + "." + className, s.getBytes(StandardCharsets.UTF_8)); + var prefixes = List.of("class ", "interface ", "@interface ", "record ", "enum "); + var firstClass = prefixes.stream() + .map(p -> Map.entry(s.indexOf(p), p.length())) + .filter(e -> e.getKey() >= 0) + .map(e -> e.getKey() + e.getValue()) + .min(Comparator.comparing(Function.identity())) + .map(classStart -> { + var firstSpace = s.indexOf(" ", classStart + 1); + var firstBracket = s.indexOf("(", classStart + 1); + var firstSquareBracket = s.indexOf("{", classStart + 1); + var classEnd = IntStream.of(firstSpace, firstBracket, firstSquareBracket) + .filter(i -> i >= 0) + .min() + .getAsInt(); + return s.substring(classStart, classEnd).trim(); + }) + .get(); + + return new ByteArrayJavaFileObject(JavaFileObject.Kind.SOURCE, testPackage + "." + firstClass, s.getBytes(StandardCharsets.UTF_8)); }) .toList(); try (var delegate = javaCompiler.getStandardFileManager(diagnostic::add, Locale.US, StandardCharsets.UTF_8); diff --git a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/KspCommonUtils.kt b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/KspCommonUtils.kt index deaccd901..c23641222 100644 --- a/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/KspCommonUtils.kt +++ b/symbol-processor-common/src/main/kotlin/ru/tinkoff/kora/ksp/common/KspCommonUtils.kt @@ -298,6 +298,10 @@ fun KSAnnotated.getOuterClassesAsPrefix(): String { return prefix.toString() } +fun KSDeclaration.generatedClass(suffix: String): String { + return this.getOuterClassesAsPrefix() + this.simpleName.asString() + "_" + suffix +} + fun KSClassDeclaration.generatedClassName(postfix: String): String { val prefix = StringBuilder("$") var parent = this.parent diff --git a/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt b/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt index 592e6578a..e7cad79be 100644 --- a/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt +++ b/symbol-processor-common/src/testFixtures/kotlin/ru/tinkoff/kora/ksp/common/AbstractSymbolProcessorTest.kt @@ -73,32 +73,23 @@ abstract class AbstractSymbolProcessorTest { val sourceList: List = Arrays.stream(sources).map { s: String -> "package %s;\n%s\n/**\n* @see %s.%s \n*/\n".formatted(testPackage, commonImports, testClass.canonicalName, testMethod.name) + s } .map { s -> - var classStart = s.indexOf("\nclass ") + 7 - if (classStart < 7) { - classStart = s.indexOf("\nopen class ") + 12 - if (classStart < 12) { - classStart = s.indexOf("\ninterface ") + 11 - if (classStart < 11) { - classStart = s.indexOf("\nsealed interface ") + 18 - if (classStart < 18) { - classStart = s.indexOf("data class ") + 11 - if (classStart < 11) { - classStart = s.indexOf("enum class ") + 11 - require(classStart >= 12) - } - } - } + val firstClass = s.indexOf("class ") to "class ".length + val firstInterface = s.indexOf("interface ") to "interface ".length + val classNameLocation = sequenceOf(firstClass, firstInterface) + .filter { it.first >= 0 } + .map { it.first + it.second } + .flatMap { + sequenceOf( + s.indexOf(" ", it + 1), + s.indexOf("(", it + 1), + s.indexOf("{", it + 1), + s.indexOf(":", it + 1), + ) + .map { it1 -> it to it1 } } - } - val classEnd = sequenceOf( - s.indexOf(" ", classStart + 1), - s.indexOf("(", classStart + 1), - s.indexOf("{", classStart + 1), - s.indexOf(":", classStart + 1), - ) - .filter { it >= 0 } - .min() - val className = s.substring(classStart, classEnd) + .filter { it.second >= 0 } + .minBy { it.second } + val className = s.substring(classNameLocation.first - 1, classNameLocation.second) val fileName = "build/in-test-generated-ksp/sources/${testPackage.replace('.', '/')}/$className.kt" Files.createDirectories(File(fileName).toPath().parent) Files.deleteIfExists(Paths.get(fileName)) @@ -142,6 +133,12 @@ abstract class AbstractSymbolProcessorTest { throw RuntimeException(errorMessages.joinToString("\n")) } + fun assertSuccess() { + if (isFailed()) { + throw compilationException() + } + } + } protected fun symbolProcessFiles(srcFiles: List, annotationProcessorProviders: List): CompileResult { diff --git a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidAnnotationProcessor.java b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidAnnotationProcessor.java index f98ab0419..e89514c0d 100644 --- a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidAnnotationProcessor.java +++ b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidAnnotationProcessor.java @@ -1,24 +1,22 @@ package ru.tinkoff.kora.validation.annotation.processor; -import com.squareup.javapoet.*; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeSpec; import ru.tinkoff.kora.annotation.processor.common.AbstractKoraProcessor; -import ru.tinkoff.kora.annotation.processor.common.CommonUtils; import ru.tinkoff.kora.annotation.processor.common.ProcessingErrorException; -import javax.annotation.processing.Generated; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.element.*; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.tools.Diagnostic; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import java.util.List; +import java.util.Set; public final class ValidAnnotationProcessor extends AbstractKoraProcessor { + private ValidatorGenerator generator; + record ValidatorSpec(ValidMeta meta, TypeSpec spec, List parameterSpecs) {} @Override @@ -26,21 +24,18 @@ public Set getSupportedAnnotationTypes() { return Set.of(ValidMeta.VALID_TYPE.canonicalName()); } + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.generator = new ValidatorGenerator(processingEnv); + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { final List validatedElements = getValidatedTypeElements(processingEnv, roundEnv); for (var validatedElement : validatedElements) { try { - var validMeta = getValidatorMetas(validatedElement); - var validator = getValidatorSpecs(validMeta); - final PackageElement packageElement = elements.getPackageOf(validator.meta().sourceElement()); - final JavaFile javaFile = JavaFile.builder(packageElement.getQualifiedName().toString(), validator.spec()).build(); - try { - javaFile.writeTo(processingEnv.getFiler()); - processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Generated Validator for: " + validator.meta().source()); - } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Error on writing file: " + e.getMessage(), validator.meta().sourceElement()); - } + this.generator.generateFor(validatedElement); } catch (ProcessingErrorException e) { e.printError(this.processingEnv); } @@ -50,185 +45,16 @@ public boolean process(Set annotations, RoundEnvironment return false; } - private ValidatorSpec getValidatorSpecs(ValidMeta meta) { - final List parameterSpecs = new ArrayList<>(); - - final TypeName typeName = meta.validator().contract().asPoetType(processingEnv); - final TypeSpec.Builder validatorSpecBuilder = TypeSpec.classBuilder(meta.validator().implementation().simpleName()) - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addSuperinterface(typeName) - .addAnnotation(AnnotationSpec.builder(ClassName.get(Generated.class)) - .addMember("value", "$S", this.getClass().getCanonicalName()) - .build()); - - final Map constraintToFieldName = new HashMap<>(); - final Map validatedToFieldName = new HashMap<>(); - final List fieldConstraintBuilder = new ArrayList<>(); - for (int i = 0; i < meta.fields().size(); i++) { - final ValidMeta.Field field = meta.fields().get(i); - final String contextField = "_context" + i; - fieldConstraintBuilder.add(CodeBlock.of("\nvar $L = context.addPath($S);", contextField, field.name())); - - if (field.isNotNull() && !field.isPrimitive()) { - fieldConstraintBuilder.add(CodeBlock.of(""" - if(value.$L == null) { - _violations.add($L.violates(\"Should be not null, but was null\")); - if(context.isFailFast()) { - return _violations; - } - }""", field.accessor(), contextField)); - } - - if (!field.isPrimitive()) { - fieldConstraintBuilder.add(CodeBlock.of("if(value.$L != null) {$>", field.accessor())); - } - - for (int j = 0; j < field.constraint().size(); j++) { - final ValidMeta.Constraint constraint = field.constraint().get(j); - final String suffix = i + "_" + j; - final String constraintField = constraintToFieldName.computeIfAbsent(constraint.factory(), (k) -> "_constraint" + suffix); - - fieldConstraintBuilder.add(CodeBlock.of(""" - _violations.addAll($L.validate(value.$L, $L)); - if(context.isFailFast() && !_violations.isEmpty()) { - return _violations; - }""", constraintField, field.accessor(), contextField)); - } - - for (int j = 0; j < field.validates().size(); j++) { - final ValidMeta.Validated validated = field.validates().get(j); - final String suffix = i + "_" + j; - final String validatorField = validatedToFieldName.computeIfAbsent(validated, (k) -> "_validator" + suffix); - - fieldConstraintBuilder.add(CodeBlock.of(""" - _violations.addAll($L.validate(value.$L, $L)); - if(context.isFailFast() && !_violations.isEmpty()) { - return _violations; - }""", validatorField, field.accessor(), contextField)); - } - - if (!field.isPrimitive()) { - fieldConstraintBuilder.add(CodeBlock.of("$<}")); - } - } - - final MethodSpec.Builder constructorSpecBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); - for (var factoryToField : constraintToFieldName.entrySet()) { - var factory = factoryToField.getKey(); - final String fieldName = factoryToField.getValue(); - final String createParameters = factory.parameters().values().stream() - .map(Object::toString) - .collect(Collectors.joining(", ")); - - validatorSpecBuilder.addField(FieldSpec.builder( - factory.validator().asPoetType(processingEnv), - fieldName, - Modifier.PRIVATE, Modifier.FINAL).build()); - - final ParameterSpec parameterSpec = ParameterSpec.builder(factory.type().asPoetType(processingEnv), fieldName).build(); - parameterSpecs.add(parameterSpec); - constructorSpecBuilder - .addParameter(parameterSpec) - .addStatement("this.$L = $L.create($L)", fieldName, fieldName, createParameters); - } - - for (var validatedToField : validatedToFieldName.entrySet()) { - final String fieldName = validatedToField.getValue(); - final TypeName fieldType = validatedToField.getKey().validator().asPoetType(processingEnv); - validatorSpecBuilder.addField(FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE, Modifier.FINAL).build()); - - final ParameterSpec parameterSpec = ParameterSpec.builder(fieldType, fieldName).build(); - parameterSpecs.add(parameterSpec); - constructorSpecBuilder - .addParameter(parameterSpec) - .addStatement("this.$L = $L", fieldName, fieldName); - } - - final MethodSpec.Builder validateMethodSpecBuilder = MethodSpec.methodBuilder("validate") - .addAnnotation(Override.class) - .addModifiers(Modifier.PUBLIC) - .returns(ValidMeta.Type.ofClass(List.class, List.of(ValidMeta.Type.ofName(ValidMeta.VIOLATION_TYPE.canonicalName()))).asPoetType(processingEnv)) - .addParameter(ParameterSpec.builder(meta.source().asPoetType(processingEnv), "value").build()) - .addParameter(ParameterSpec.builder(ValidMeta.Type.ofName(ValidMeta.CONTEXT_TYPE.canonicalName()).asPoetType(processingEnv), "context").build()) - .addCode(CodeBlock.join(List.of( - CodeBlock.of(""" - if(value == null) { - return $T.of(context.violates(\"$L input value should be not null, but was null\")); - } - - final $T _violations = new $T<>();""", - List.class, meta.source().simpleName(), List.class, ArrayList.class), - CodeBlock.join(fieldConstraintBuilder, "\n"), - CodeBlock.of("return _violations;")), - "\n\n")); - - final TypeSpec validatorSpec = validatorSpecBuilder - .addMethod(constructorSpecBuilder.build()) - .addMethod(validateMethodSpecBuilder.build()) - .build(); - - return new ValidatorSpec(meta, validatorSpec, parameterSpecs); - } - - private ValidMeta getValidatorMetas(TypeElement element) { - final List elementFields = getFields(element); - final List fields = new ArrayList<>(); - for (VariableElement fieldElement : elementFields) { - final List constraints = getValidatedByConstraints(processingEnv, fieldElement); - final List validateds = getValidated(fieldElement); - - final boolean isNullable = CommonUtils.isNullable(fieldElement); - if (!isNullable || !constraints.isEmpty() || !validateds.isEmpty()) { - final boolean isPrimitive = fieldElement.asType() instanceof PrimitiveType; - final boolean isRecord = element.getKind() == ElementKind.RECORD; - final TypeMirror fieldType = ValidUtils.getBoxType(fieldElement.asType(), processingEnv); - - final ValidMeta.Field fieldMeta = new ValidMeta.Field( - ValidMeta.Type.ofType(fieldType), - fieldElement.getSimpleName().toString(), - isRecord, - isNullable, - isPrimitive, - constraints, - validateds); - - fields.add(fieldMeta); - } - } - return new ValidMeta(ValidMeta.Type.ofType(element.asType()), element, fields); - } - - private static List getValidatedByConstraints(ProcessingEnvironment env, VariableElement field) { - if (field.asType().getKind() == TypeKind.ERROR) { - throw new ProcessingErrorException("Type is error in this round", field); - } - return ValidUtils.getValidatedByConstraints(env, field.asType(), field.getAnnotationMirrors()); - } - - private static List getFields(TypeElement element) { - return element.getEnclosedElements().stream() - .filter(e -> e.getKind() == ElementKind.FIELD) - .filter(e -> e instanceof VariableElement) - .map(e -> ((VariableElement) e)) - .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) - .toList(); - } - - private static List getValidated(VariableElement field) { - if (field.getAnnotationMirrors().stream().anyMatch(a -> a.getAnnotationType().toString().equals(ValidMeta.VALID_TYPE.canonicalName()))) { - return List.of(new ValidMeta.Validated(ValidMeta.Type.ofType(field.asType()))); - } - - return Collections.emptyList(); - } - private List getValidatedTypeElements(ProcessingEnvironment processEnv, RoundEnvironment roundEnv) { final TypeElement annotation = processEnv.getElementUtils().getTypeElement(ValidMeta.VALID_TYPE.canonicalName()); return roundEnv.getElementsAnnotatedWith(annotation).stream() .filter(a -> a instanceof TypeElement) .map(element -> { - if (element.getKind() == ElementKind.ENUM || element.getKind() == ElementKind.INTERFACE) { + if (element.getKind() == ElementKind.ENUM) { + throw new ProcessingErrorException("Validation can't be generated for: " + element.getKind(), element); + } + if (element.getKind() == ElementKind.INTERFACE && !element.getModifiers().contains(Modifier.SEALED)) { throw new ProcessingErrorException("Validation can't be generated for: " + element.getKind(), element); } return ((TypeElement) element); diff --git a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidMeta.java b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidMeta.java index 2baec26cd..03b8da1a0 100644 --- a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidMeta.java +++ b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidMeta.java @@ -3,6 +3,7 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; +import ru.tinkoff.kora.annotation.processor.common.CommonUtils; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.TypeElement; @@ -27,7 +28,7 @@ public ValidMeta(Type source, TypeElement sourceElement, List fields) { this(source, new Validator( Type.ofName(VALIDATOR_TYPE.canonicalName(), List.of(source)), - new Type(source.packageName, "$" + source.simpleName() + "_Validator", List.of(source)) + new Type(source.packageName, CommonUtils.getOuterClassesAsPrefix(sourceElement) + sourceElement.getSimpleName() + "_Validator", List.of(source)) ), sourceElement, fields); } diff --git a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidatorGenerator.java b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidatorGenerator.java new file mode 100644 index 000000000..4c75518ce --- /dev/null +++ b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/ValidatorGenerator.java @@ -0,0 +1,271 @@ +package ru.tinkoff.kora.validation.annotation.processor; + +import com.squareup.javapoet.*; +import ru.tinkoff.kora.annotation.processor.common.CommonClassNames; +import ru.tinkoff.kora.annotation.processor.common.CommonUtils; +import ru.tinkoff.kora.annotation.processor.common.ProcessingErrorException; +import ru.tinkoff.kora.annotation.processor.common.SealedTypeUtils; + +import javax.annotation.Nullable; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.*; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +public class ValidatorGenerator { + private final Types types; + private final Elements elements; + private final Filer filer; + private final ProcessingEnvironment processingEnv; + + public ValidatorGenerator(ProcessingEnvironment env) { + this.types = env.getTypeUtils(); + this.elements = env.getElementUtils(); + this.filer = env.getFiler(); + this.processingEnv = env; + } + + public void generateFor(TypeElement validatedElement) { + if (validatedElement.getKind().isInterface()) { + this.generateForSealed(validatedElement); + return; + } + var validMeta = getValidatorMetas(validatedElement); + var validator = getValidatorSpecs(validMeta); + final PackageElement packageElement = elements.getPackageOf(validator.meta().sourceElement()); + final JavaFile javaFile = JavaFile.builder(packageElement.getQualifiedName().toString(), validator.spec()).build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + private void generateForSealed(TypeElement validatedElement) { + assert validatedElement.getModifiers().contains(Modifier.SEALED); + var validatedTypeName = TypeName.get(validatedElement.asType()); + var validatorType = ParameterizedTypeName.get(ValidMeta.VALIDATOR_TYPE, validatedTypeName); + + var validatorSpecBuilder = TypeSpec.classBuilder(CommonUtils.getOuterClassesAsPrefix(validatedElement) + validatedElement.getSimpleName() + "_Validator") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(validatorType) + .addAnnotation(AnnotationSpec.builder(CommonClassNames.koraGenerated) + .addMember("value", "$S", this.getClass().getCanonicalName()) + .build()); + for (var typeParameter : validatedElement.getTypeParameters()) { + validatorSpecBuilder.addTypeVariable(TypeVariableName.get(typeParameter)); + } + var constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + var method = MethodSpec.methodBuilder("validate") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get(CommonClassNames.list, ValidMeta.VIOLATION_TYPE)) + .addParameter(ParameterSpec.builder(validatedTypeName, "value").addAnnotation(Nullable.class).build()) + .addParameter(ValidMeta.CONTEXT_TYPE, "context"); + + var subclasses = SealedTypeUtils.collectFinalPermittedSubtypes(types, elements, validatedElement); + for (int i = 0; i < subclasses.size(); i++) { // TODO recursive subclasses + var permittedSubclass = subclasses.get(i); + var name = "_validator" + (i + 1); + var subclassTypeName = TypeName.get(permittedSubclass.asType()); + var fieldValidator = ParameterizedTypeName.get(ValidMeta.VALIDATOR_TYPE, subclassTypeName); + validatorSpecBuilder.addField(fieldValidator, name, Modifier.PRIVATE, Modifier.FINAL); + constructor.addParameter(fieldValidator, name); + constructor.addStatement("this.$N = $N;", name, name); + if (i > 0) { + method.nextControlFlow("else if (value instanceof $T casted)", subclassTypeName); + } else { + method.beginControlFlow("if (value instanceof $T casted)", subclassTypeName); + } + method.addStatement("return $N.validate(casted, context)", name); + } + validatorSpecBuilder.addMethod(method.endControlFlow().addStatement("throw new $T()", IllegalStateException.class).build()); + validatorSpecBuilder.addMethod(constructor.build()); + var javaFile = JavaFile.builder(elements.getPackageOf(validatedElement).getQualifiedName().toString(), validatorSpecBuilder.build()).build(); + try { + javaFile.writeTo(this.filer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private ValidAnnotationProcessor.ValidatorSpec getValidatorSpecs(ValidMeta meta) { + final List parameterSpecs = new ArrayList<>(); + + final TypeName typeName = meta.validator().contract().asPoetType(processingEnv); + final TypeSpec.Builder validatorSpecBuilder = TypeSpec.classBuilder(meta.validator().implementation().simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(typeName) + .addAnnotation(AnnotationSpec.builder(CommonClassNames.koraGenerated) + .addMember("value", "$S", this.getClass().getCanonicalName()) + .build()); + + final Map constraintToFieldName = new HashMap<>(); + final Map validatedToFieldName = new HashMap<>(); + final List fieldConstraintBuilder = new ArrayList<>(); + for (int i = 0; i < meta.fields().size(); i++) { + final ValidMeta.Field field = meta.fields().get(i); + final String contextField = "_context" + i; + fieldConstraintBuilder.add(CodeBlock.of("\nvar $L = context.addPath($S);", contextField, field.name())); + + if (field.isNotNull() && !field.isPrimitive()) { + fieldConstraintBuilder.add(CodeBlock.of(""" + if(value.$L == null) { + _violations.add($L.violates(\"Should be not null, but was null\")); + if(context.isFailFast()) { + return _violations; + } + }""", field.accessor(), contextField)); + } + + if (!field.isPrimitive()) { + fieldConstraintBuilder.add(CodeBlock.of("if(value.$L != null) {$>", field.accessor())); + } + + for (int j = 0; j < field.constraint().size(); j++) { + final ValidMeta.Constraint constraint = field.constraint().get(j); + final String suffix = i + "_" + j; + final String constraintField = constraintToFieldName.computeIfAbsent(constraint.factory(), (k) -> "_constraint" + suffix); + + fieldConstraintBuilder.add(CodeBlock.of(""" + _violations.addAll($L.validate(value.$L, $L)); + if(context.isFailFast() && !_violations.isEmpty()) { + return _violations; + }""", constraintField, field.accessor(), contextField)); + } + + for (int j = 0; j < field.validates().size(); j++) { + final ValidMeta.Validated validated = field.validates().get(j); + final String suffix = i + "_" + j; + final String validatorField = validatedToFieldName.computeIfAbsent(validated, (k) -> "_validator" + suffix); + + fieldConstraintBuilder.add(CodeBlock.of(""" + _violations.addAll($L.validate(value.$L, $L)); + if(context.isFailFast() && !_violations.isEmpty()) { + return _violations; + }""", validatorField, field.accessor(), contextField)); + } + + if (!field.isPrimitive()) { + fieldConstraintBuilder.add(CodeBlock.of("$<}")); + } + } + + final MethodSpec.Builder constructorSpecBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); + for (var factoryToField : constraintToFieldName.entrySet()) { + var factory = factoryToField.getKey(); + final String fieldName = factoryToField.getValue(); + final String createParameters = factory.parameters().values().stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + + validatorSpecBuilder.addField(FieldSpec.builder( + factory.validator().asPoetType(processingEnv), + fieldName, + Modifier.PRIVATE, Modifier.FINAL).build()); + + final ParameterSpec parameterSpec = ParameterSpec.builder(factory.type().asPoetType(processingEnv), fieldName).build(); + parameterSpecs.add(parameterSpec); + constructorSpecBuilder + .addParameter(parameterSpec) + .addStatement("this.$L = $L.create($L)", fieldName, fieldName, createParameters); + } + + for (var validatedToField : validatedToFieldName.entrySet()) { + final String fieldName = validatedToField.getValue(); + final TypeName fieldType = validatedToField.getKey().validator().asPoetType(processingEnv); + validatorSpecBuilder.addField(FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE, Modifier.FINAL).build()); + + final ParameterSpec parameterSpec = ParameterSpec.builder(fieldType, fieldName).build(); + parameterSpecs.add(parameterSpec); + constructorSpecBuilder + .addParameter(parameterSpec) + .addStatement("this.$L = $L", fieldName, fieldName); + } + + final MethodSpec.Builder validateMethodSpecBuilder = MethodSpec.methodBuilder("validate") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ValidMeta.Type.ofClass(List.class, List.of(ValidMeta.Type.ofName(ValidMeta.VIOLATION_TYPE.canonicalName()))).asPoetType(processingEnv)) + .addParameter(ParameterSpec.builder(meta.source().asPoetType(processingEnv), "value").build()) + .addParameter(ParameterSpec.builder(ValidMeta.Type.ofName(ValidMeta.CONTEXT_TYPE.canonicalName()).asPoetType(processingEnv), "context").build()) + .addCode(CodeBlock.join(List.of( + CodeBlock.of(""" + if(value == null) { + return $T.of(context.violates(\"$L input value should be not null, but was null\")); + } + + final $T _violations = new $T<>();""", + List.class, meta.source().simpleName(), List.class, ArrayList.class), + CodeBlock.join(fieldConstraintBuilder, "\n"), + CodeBlock.of("return _violations;")), + "\n\n")); + + final TypeSpec validatorSpec = validatorSpecBuilder + .addMethod(constructorSpecBuilder.build()) + .addMethod(validateMethodSpecBuilder.build()) + .build(); + + return new ValidAnnotationProcessor.ValidatorSpec(meta, validatorSpec, parameterSpecs); + } + + private ValidMeta getValidatorMetas(TypeElement element) { + final List elementFields = getFields(element); + final List fields = new ArrayList<>(); + for (VariableElement fieldElement : elementFields) { + final List constraints = getValidatedByConstraints(processingEnv, fieldElement); + final List validateds = getValidated(fieldElement); + + final boolean isNullable = CommonUtils.isNullable(fieldElement); + if (!isNullable || !constraints.isEmpty() || !validateds.isEmpty()) { + final boolean isPrimitive = fieldElement.asType() instanceof PrimitiveType; + final boolean isRecord = element.getKind() == ElementKind.RECORD; + final TypeMirror fieldType = ValidUtils.getBoxType(fieldElement.asType(), processingEnv); + + final ValidMeta.Field fieldMeta = new ValidMeta.Field( + ValidMeta.Type.ofType(fieldType), + fieldElement.getSimpleName().toString(), + isRecord, + isNullable, + isPrimitive, + constraints, + validateds); + + fields.add(fieldMeta); + } + } + return new ValidMeta(ValidMeta.Type.ofType(element.asType()), element, fields); + } + + private static List getValidatedByConstraints(ProcessingEnvironment env, VariableElement field) { + if (field.asType().getKind() == TypeKind.ERROR) { + throw new ProcessingErrorException("Type is error in this round", field); + } + return ValidUtils.getValidatedByConstraints(env, field.asType(), field.getAnnotationMirrors()); + } + + private static List getFields(TypeElement element) { + return element.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD) + .filter(e -> e instanceof VariableElement) + .map(e -> ((VariableElement) e)) + .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) + .toList(); + } + + private static List getValidated(VariableElement field) { + if (field.getAnnotationMirrors().stream().anyMatch(a -> a.getAnnotationType().toString().equals(ValidMeta.VALID_TYPE.canonicalName()))) { + return List.of(new ValidMeta.Validated(ValidMeta.Type.ofType(field.asType()))); + } + + return Collections.emptyList(); + } +} diff --git a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/extension/ValidKoraExtension.java b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/extension/ValidKoraExtension.java index 5e89d7992..d83cf3152 100644 --- a/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/extension/ValidKoraExtension.java +++ b/validation/validation-annotation-processor/src/main/java/ru/tinkoff/kora/validation/annotation/processor/extension/ValidKoraExtension.java @@ -1,7 +1,12 @@ package ru.tinkoff.kora.validation.annotation.processor.extension; +import ru.tinkoff.kora.annotation.processor.common.AnnotationUtils; +import ru.tinkoff.kora.annotation.processor.common.CommonUtils; +import ru.tinkoff.kora.annotation.processor.common.ProcessingErrorException; import ru.tinkoff.kora.kora.app.annotation.processor.extension.ExtensionResult; import ru.tinkoff.kora.kora.app.annotation.processor.extension.KoraExtension; +import ru.tinkoff.kora.validation.annotation.processor.ValidMeta; +import ru.tinkoff.kora.validation.annotation.processor.ValidatorGenerator; import javax.annotation.Nullable; import javax.annotation.processing.ProcessingEnvironment; @@ -15,39 +20,56 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import javax.tools.Diagnostic; public final class ValidKoraExtension implements KoraExtension { private final Types types; private final Elements elements; private final TypeMirror validatorType; + private final ValidatorGenerator generator; + private final ProcessingEnvironment processingEnv; public ValidKoraExtension(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; this.types = processingEnv.getTypeUtils(); this.elements = processingEnv.getElementUtils(); this.validatorType = types.erasure(elements.getTypeElement("ru.tinkoff.kora.validation.common.Validator").asType()); + this.generator = new ValidatorGenerator(processingEnv); } @Nullable @Override public KoraExtensionDependencyGenerator getDependencyGenerator(RoundEnvironment roundEnvironment, TypeMirror typeMirror) { var erasure = types.erasure(typeMirror); - if (types.isSameType(erasure, validatorType)) { - if (typeMirror instanceof DeclaredType dt) { - var validatorArgumentType = dt.getTypeArguments().get(0); - if (validatorArgumentType.getKind() != TypeKind.DECLARED) { - return null; - } + if (!types.isSameType(erasure, validatorType)) { + return null; + } + if (!(typeMirror instanceof DeclaredType dt)) { + return null; + } + var validatorArgumentType = dt.getTypeArguments().get(0); + if (validatorArgumentType.getKind() != TypeKind.DECLARED) { + return null; + } + + var validatedTypeElement = types.asElement(validatorArgumentType); + var packageElement = elements.getPackageOf(validatedTypeElement).getQualifiedName().toString(); + var validatorName = CommonUtils.getOuterClassesAsPrefix(validatedTypeElement) + validatedTypeElement.getSimpleName() + "_Validator"; + var componentElement = elements.getTypeElement(packageElement + "." + validatorName); - var validatorElement = types.asElement(validatorArgumentType); - var packageElement = elements.getPackageOf(validatorElement).getQualifiedName().toString(); - var validatorName = "$" + validatorElement.getSimpleName() + "_Validator"; - var componentElement = elements.getTypeElement(packageElement + "." + validatorName); - if (componentElement != null) { - return () -> buildExtensionResult((DeclaredType) validatorArgumentType, componentElement); - } else { - return ExtensionResult::nextRound; + if (componentElement != null) { + return () -> buildExtensionResult((DeclaredType) validatorArgumentType, componentElement); + } else if (AnnotationUtils.findAnnotation(validatedTypeElement, ValidMeta.VALID_TYPE) != null) { + return ExtensionResult::nextRound; + } else { + try { + this.generator.generateFor((TypeElement) validatedTypeElement); + } catch (ProcessingErrorException e) { + for (var error : e.getErrors()) { + this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, error.message(), error.element(), error.a(), error.v()); } + return null; } } diff --git a/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationExtensionTest.java b/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationExtensionTest.java index 065ab0d13..bbfb4ea08 100644 --- a/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationExtensionTest.java +++ b/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationExtensionTest.java @@ -37,4 +37,31 @@ public interface TestApp extends ValidatorModule{ var graph = compileResult.loadClass("TestAppGraph"); assertThat(graph).isNotNull(); } + + @Test + public void testExtensionNoAnnotationProcessor() throws Exception { + compile(List.of(new KoraAppProcessor(), new ValidAnnotationProcessor()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size; + + public record TestRecord(@Size(min = 1, max = 5) java.util.List list){} + """, + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.annotation.Root; + import ru.tinkoff.kora.validation.common.Validator; + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule; + @KoraApp + public interface TestApp extends ValidatorModule{ + @Root + default String root(Validator testRecordValidator) { return "";} + } + """); + compileResult.assertSuccess(); + + var validatorClass = compileResult.loadClass("$TestRecord_Validator"); + assertThat(validatorClass).isNotNull(); + var graph = compileResult.loadClass("TestAppGraph"); + assertThat(graph).isNotNull(); + } } diff --git a/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationSealedTypeTest.java b/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationSealedTypeTest.java new file mode 100644 index 000000000..0b5a35f5b --- /dev/null +++ b/validation/validation-annotation-processor/src/test/java/ru/tinkoff/kora/validation/annotation/processor/ValidationSealedTypeTest.java @@ -0,0 +1,99 @@ +package ru.tinkoff.kora.validation.annotation.processor; + +import org.junit.jupiter.api.Test; +import ru.tinkoff.kora.annotation.processor.common.AbstractAnnotationProcessorTest; +import ru.tinkoff.kora.application.graph.TypeRef; +import ru.tinkoff.kora.kora.app.annotation.processor.KoraAppProcessor; +import ru.tinkoff.kora.validation.common.Validator; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ValidationSealedTypeTest extends AbstractAnnotationProcessorTest { + @Test + public void testSealedInterface() throws Exception { + compile(List.of(new KoraAppProcessor(), new ValidAnnotationProcessor()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size; + import ru.tinkoff.kora.validation.common.annotation.Valid; + @Valid + public sealed interface TestInterface { + @Valid + record TestRecord(@Size(min = 1, max = 5) java.util.List list) implements TestInterface {} + } + """); + compileResult.assertSuccess(); + + var validatorClass = compileResult.loadClass("$TestInterface_Validator"); + assertThat(validatorClass).isNotNull(); + assertThat(validatorClass.getConstructors()).hasSize(1); + + var constructor = validatorClass.getConstructors()[0]; + var parameters = constructor.getParameterTypes(); + assertThat(parameters).containsExactly(Validator.class); + + assertThat(constructor.getGenericParameterTypes()).containsExactly(TypeRef.of(Validator.class, compileResult.loadClass("TestInterface$TestRecord"))); + } + + @Test + public void testExtensionForProcessedType() throws Exception { + compile(List.of(new KoraAppProcessor(), new ValidAnnotationProcessor()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size; + import ru.tinkoff.kora.validation.common.annotation.Valid; + @Valid + public sealed interface TestInterface { + @Valid + record TestRecord(@Size(min = 1, max = 5) java.util.List list) implements TestInterface {} + } + """, + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.annotation.Root; + import ru.tinkoff.kora.validation.common.Validator; + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule; + @KoraApp + public interface TestApp extends ValidatorModule{ + @Root + default String root(Validator testRecordValidator) { return "";} + } + """); + compileResult.assertSuccess(); + + var validatorClass = compileResult.loadClass("$TestInterface_Validator"); + assertThat(validatorClass).isNotNull(); + var graph = compileResult.loadClass("TestAppGraph"); + assertThat(graph).isNotNull(); + } + + @Test + public void testExtensionForNonProcessedType() throws Exception { + compile(List.of(new KoraAppProcessor(), new ValidAnnotationProcessor()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size; + + public sealed interface TestInterface { + record TestRecord(@Size(min = 1, max = 5) java.util.List list) implements TestInterface {} + } + """, + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.annotation.Root; + import ru.tinkoff.kora.validation.common.Validator; + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule; + @KoraApp + public interface TestApp extends ValidatorModule{ + @Root + default String root(Validator testRecordValidator) { return "";} + } + """); + compileResult.assertSuccess(); + + var validatorClass = compileResult.loadClass("$TestInterface_Validator"); + assertThat(validatorClass).isNotNull(); + var graph = compileResult.loadClass("TestAppGraph"); + assertThat(graph).isNotNull(); + } + +} diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidMeta.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidMeta.kt index d97b2b1e6..c143a2535 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidMeta.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidMeta.kt @@ -1,6 +1,5 @@ package ru.tinkoff.kora.validation.symbol.processor -import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.* @@ -9,16 +8,16 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.TypeName import java.util.stream.Collectors -val VALID_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.annotation.Valid") -val VALIDATE_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.annotation.Validate") -val VALIDATED_BY_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.annotation.ValidatedBy") -val CONTEXT_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.ValidationContext") -val VALIDATOR_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.Validator") -val VIOLATION_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.Violation") -val EXCEPTION_TYPE = ClassName.bestGuess("ru.tinkoff.kora.validation.common.ViolationException") +val VALID_TYPE = ClassName("ru.tinkoff.kora.validation.common.annotation", "Valid") +val VALIDATE_TYPE = ClassName("ru.tinkoff.kora.validation.common.annotation", "Validate") +val VALIDATED_BY_TYPE = ClassName("ru.tinkoff.kora.validation.common.annotation", "ValidatedBy") +val CONTEXT_TYPE = ClassName("ru.tinkoff.kora.validation.common", "ValidationContext") +val VALIDATOR_TYPE = ClassName("ru.tinkoff.kora.validation.common", "Validator") +val VIOLATION_TYPE = ClassName("ru.tinkoff.kora.validation.common", "Violation") +val EXCEPTION_TYPE = ClassName("ru.tinkoff.kora.validation.common", "ViolationException") data class ValidatorMeta( - val source: Type, + val source: TypeName, val sourceDeclaration: KSClassDeclaration, val validator: ValidatorType, val fields: List @@ -28,7 +27,7 @@ data class Validated(val target: Type) { fun validator(): Type = VALIDATOR_TYPE.canonicalName.asType(listOf(target)) } -data class ValidatorType(val contract: Type, val implementation: Type) +data class ValidatorType(val contract: TypeName) data class Field( val type: Type, @@ -88,18 +87,16 @@ data class Type(private val reference: KSTypeReference?, private val isNullable: } } - @KspExperimental fun asPoetType(): TypeName = asPoetType(isNullable) - @KspExperimental fun asPoetType(nullable: Boolean): TypeName { return if (generic.isEmpty()) { - ClassName.bestGuess(canonicalName()).copy(nullable) + ClassName(packageName, simpleName).copy(nullable) } else { val genericPoetTypes = generic.asSequence() .map { t -> t.asPoetType() } .toList() - ClassName.bestGuess(canonicalName()).parameterizedBy(genericPoetTypes).copy(nullable) + ClassName(packageName, simpleName).parameterizedBy(genericPoetTypes).copy(nullable) } } @@ -152,7 +149,7 @@ fun KSType.asType(): Type { emptyList() val asType = this.declaration.qualifiedName!!.asString().asType() - return Type(null, this.isMarkedNullable, asType.packageName, asType.simpleName, generic) + return Type(null, this.isMarkedNullable, this.declaration.packageName.asString(), asType.simpleName, generic) } fun String.asType(nullable: Boolean = false): Type = this.asType(emptyList(), nullable) diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessor.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessor.kt index 19009be8e..099d8947a 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessor.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessor.kt @@ -1,247 +1,27 @@ package ru.tinkoff.kora.validation.symbol.processor -import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessorEnvironment -import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.validate -import com.squareup.kotlinpoet.* -import com.squareup.kotlinpoet.ksp.writeTo +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeSpec import ru.tinkoff.kora.ksp.common.BaseSymbolProcessor -import ru.tinkoff.kora.ksp.common.exception.ProcessingError -import ru.tinkoff.kora.ksp.common.exception.ProcessingErrorException -import ru.tinkoff.kora.ksp.common.visitClass -import java.io.IOException -import javax.annotation.processing.Generated -@KspExperimental class ValidSymbolProcessor(private val environment: SymbolProcessorEnvironment) : BaseSymbolProcessor(environment) { + private val gen = ValidatorGenerator(environment.codeGenerator) data class ValidatorSpec(val meta: ValidatorMeta, val spec: TypeSpec, val parameterSpecs: List) override fun processRound(resolver: Resolver): List { val symbols = resolver.getSymbolsWithAnnotation(VALID_TYPE.canonicalName).toList() - - try { - val specs = symbols - .filter { it.validate() } - .mapNotNull { it.visitClass { clazz -> getValidatorMeta(clazz) } } - .map { getValidatorSpecs(it) } - .toList() - - for (validatorSpec in specs) { - val fileSpec = FileSpec.builder(validatorSpec.meta.validator.implementation.packageName, validatorSpec.meta.validator.implementation.simpleName) - .addType(validatorSpec.spec) - .build() - - fileSpec.writeTo(codeGenerator = environment.codeGenerator, aggregating = false) + for (symbol in symbols) { + if (!symbol.validate()) { + continue } - } catch (e: IOException) { - throw ProcessingErrorException(ProcessingError(e.message.toString(), null)) + gen.generate(symbol) } return symbols.filterNot { it.validate() }.toList() } - - private fun getValidatorSpecs(meta: ValidatorMeta): ValidatorSpec { - val parameterSpecs = ArrayList() - val typeName = meta.validator.contract.asPoetType() - val validatorSpecBuilder = TypeSpec.classBuilder(meta.validator.implementation.simpleName) - .addSuperinterface(typeName) - .addAnnotation( - AnnotationSpec.builder(Generated::class) - .addMember("%S", this.javaClass.canonicalName) - .build() - ) - - val constraintToFieldName = HashMap() - val validatedToFieldName = HashMap() - val contextBuilder = ArrayList() - val constraintBuilder = ArrayList() - for (i in meta.fields.indices) { - val field = meta.fields[i] - val contextField = "_context$i" - contextBuilder.add( - CodeBlock.of( - """ - var %L = context.addPath(%S) - - """.trimIndent(), contextField, field.name - ) - ) - - for (j in field.constraint.indices) { - val constraint = field.constraint[j] - val suffix = i.toString() + "_" + j - val constraintField = constraintToFieldName.computeIfAbsent(constraint.factory) { "_constraint$suffix" } - constraintBuilder.add( - CodeBlock.of( - """ - _violations.addAll(%L.validate(value.%L, %L)); - if(context.isFailFast && _violations.isNotEmpty()) { - return _violations; - } - - """.trimIndent(), - constraintField, field.accessor(), contextField - ) - ) - } - - for (j in field.validates.indices) { - val validated = field.validates[j] - val suffix = i.toString() + "_" + j - val validatorField = validatedToFieldName.computeIfAbsent(validated) { "_validator$suffix" } - if (field.isNotNull()) { - constraintBuilder.add( - CodeBlock.of( - """ - - _violations.addAll(%L.validate(value.%L, %L)); - if(context.isFailFast) { - return _violations; - } - - """.trimIndent(), validatorField, field.accessor(), contextField - ) - ) - } else { - constraintBuilder.add( - CodeBlock.of( - """ - if(value.%L != null) { - _violations.addAll(%L.validate(value.%L, %L)); - if(context.isFailFast) { - return _violations; - } - } - - """.trimIndent(), field.accessor(), validatorField, field.accessor(), contextField - ) - ) - } - } - } - - val constructorSpecBuilder = FunSpec.constructorBuilder().addModifiers(KModifier.PUBLIC) - for (entry in constraintToFieldName) { - val factory = entry.key - val fieldName = entry.value - val validatorType = factory.validator() - val createParameters = factory.parameters.values.joinToString(", ") { - if (it is String) { - CodeBlock.of("%S", it).toString() - } else { - CodeBlock.of("%L", it).toString() - } - } - - validatorSpecBuilder.addProperty( - PropertySpec.builder( - fieldName, - validatorType.asPoetType(), - KModifier.PRIVATE - ).build() - ) - - val parameterSpec = ParameterSpec.builder(fieldName, factory.type.asPoetType()).build() - parameterSpecs.add(parameterSpec) - constructorSpecBuilder - .addParameter(parameterSpec) - .addStatement("this.%L = %L.create(%L)", fieldName, fieldName, createParameters) - } - - for (entry in validatedToFieldName) { - val fieldName = entry.value - val fieldType = entry.key.validator().asPoetType() - PropertySpec.builder(fieldName, fieldType, KModifier.PRIVATE).build(); - validatorSpecBuilder.addProperty(PropertySpec.builder(fieldName, fieldType, KModifier.PRIVATE).build()) - val parameterSpec = ParameterSpec.builder(fieldName, fieldType).build() - parameterSpecs.add(parameterSpec) - constructorSpecBuilder - .addParameter(parameterSpec) - .addStatement("this.%L = %L", fieldName, fieldName) - } - - val validateMethodSpecBuilder = FunSpec.builder("validate") - .addModifiers(KModifier.OVERRIDE) - .returns("kotlin.collections.MutableList".asType(listOf(VIOLATION_TYPE.canonicalName.asType())).asPoetType()) - .addParameter(ParameterSpec.builder("value", meta.source.asPoetType(true)).build()) - .addParameter(ParameterSpec.builder("context", CONTEXT_TYPE.canonicalName.asType().asPoetType()).build()) - .addCode( - CodeBlock.of( - """ - if(value == null) { - return mutableListOf(context.violates("Input value is null")); - } - - val _violations = %T(); - - """.trimIndent(), ArrayList::class - ) - ) - - contextBuilder.forEach { b -> validateMethodSpecBuilder.addCode(b) } - constraintBuilder.forEach { b -> validateMethodSpecBuilder.addCode(b) } - validateMethodSpecBuilder.addCode(CodeBlock.of("return _violations")) - - val typeSpec = validatorSpecBuilder - .addFunction(constructorSpecBuilder.build()) - .addFunction(validateMethodSpecBuilder.build()) - .build() - - return ValidatorSpec(meta, typeSpec, parameterSpecs) - } - - private fun getValidatorMeta(declaration: KSClassDeclaration): ValidatorMeta { - if (declaration.classKind == ClassKind.INTERFACE || declaration.classKind == ClassKind.ENUM_CLASS) { - throw ProcessingErrorException("Validation can't be generated for: ${declaration.classKind}", declaration) - } - - val elementFields = declaration.getAllProperties() - .filter { p -> !p.modifiers.contains(Modifier.JAVA_STATIC) } - .filter { p -> !p.modifiers.contains(Modifier.CONST) } - .toList() - - val fields = ArrayList() - for (fieldProperty in elementFields) { - val constraints = getConstraints(fieldProperty) - val validateds = getValid(fieldProperty) - val isNullable = fieldProperty.type.resolve().isMarkedNullable - if (constraints.isNotEmpty() || validateds.isNotEmpty()) { - fields.add( - Field( - fieldProperty.type.asType(), - fieldProperty.simpleName.asString(), - declaration.modifiers.any { m -> m == Modifier.DATA }, - isNullable, - constraints, - validateds - ) - ) - } - } - - val source = declaration.asStarProjectedType().asType() - return ValidatorMeta( - source, - declaration, - ValidatorType( - VALIDATOR_TYPE.canonicalName.asType(listOf(source)), - "${source.packageName}.\$${source.simpleName}_Validator".asType(listOf(source)), - ), - fields - ) - } - - private fun getConstraints(field: KSPropertyDeclaration): List { - return ValidUtils.getConstraints(field.type, field.annotations) - } - - private fun getValid(field: KSPropertyDeclaration): List { - return if (field.annotations.any { a -> a.annotationType.asType().canonicalName() == VALID_TYPE.canonicalName }) - listOf(Validated(field.type.asType())) - else - emptyList() - } } diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessorProvider.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessorProvider.kt index 96070494b..689c56eb0 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessorProvider.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidSymbolProcessorProvider.kt @@ -1,11 +1,9 @@ package ru.tinkoff.kora.validation.symbol.processor -import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider -@KspExperimental class ValidSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidUtils.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidUtils.kt index 930dda4a2..59f8612ce 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidUtils.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidUtils.kt @@ -1,10 +1,9 @@ package ru.tinkoff.kora.validation.symbol.processor -import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.KSAnnotation -import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeReference +import com.squareup.kotlinpoet.ksp.toClassName class ValidUtils { @@ -14,7 +13,7 @@ class ValidUtils { return annotation .mapNotNull { origin -> origin.annotationType.resolve().declaration.annotations - .filter { a -> a.annotationType.asType().canonicalName() == VALIDATED_BY_TYPE.canonicalName } + .filter { a -> a.annotationType.resolve().toClassName() == VALIDATED_BY_TYPE } .map { validatedBy -> val parameters = origin.arguments.associate { a -> Pair(a.name!!.asString(), a.value!!) } val factory = validatedBy.arguments @@ -24,7 +23,7 @@ class ValidUtils { Constraint( origin.annotationType.asType(), - Constraint.Factory(factory.declaration.qualifiedName!!.asString().asType(listOf(type.resolve().makeNullable().asType())), parameters) + Constraint.Factory(factory.declaration.qualifiedName!!.asString().asType(listOf(type.resolve().makeNotNullable().asType())), parameters) ) } .firstOrNull() diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorGenerator.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorGenerator.kt new file mode 100644 index 000000000..37deaaa21 --- /dev/null +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorGenerator.kt @@ -0,0 +1,279 @@ +package ru.tinkoff.kora.validation.symbol.processor + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.symbol.* +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ksp.addOriginatingKSFile +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.toTypeVariableName +import com.squareup.kotlinpoet.ksp.writeTo +import ru.tinkoff.kora.ksp.common.CommonClassNames +import ru.tinkoff.kora.ksp.common.KspCommonUtils.collectFinalSealedSubtypes +import ru.tinkoff.kora.ksp.common.KspCommonUtils.generated +import ru.tinkoff.kora.ksp.common.exception.ProcessingErrorException +import ru.tinkoff.kora.ksp.common.generatedClassName +import ru.tinkoff.kora.ksp.common.visitClass + +class ValidatorGenerator(val codeGenerator: CodeGenerator) { + fun getValidatorSpec(meta: ValidatorMeta): ValidSymbolProcessor.ValidatorSpec { + val parameterSpecs = ArrayList() + val typeName = meta.validator.contract + val validatorSpecBuilder = TypeSpec.classBuilder(meta.sourceDeclaration.generatedClassName("Validator")) + .addSuperinterface(typeName) + .addAnnotation( + AnnotationSpec.builder(CommonClassNames.generated) + .addMember("%S", this.javaClass.canonicalName) + .build() + ) + + val constraintToFieldName = HashMap() + val validatedToFieldName = HashMap() + val contextBuilder = ArrayList() + val constraintBuilder = ArrayList() + for (i in meta.fields.indices) { + val field = meta.fields[i] + val contextField = "_context$i" + contextBuilder.add( + CodeBlock.of( + """ + var %L = context.addPath(%S) + + """.trimIndent(), contextField, field.name + ) + ) + + for (j in field.constraint.indices) { + val constraint = field.constraint[j] + val suffix = i.toString() + "_" + j + val constraintField = constraintToFieldName.computeIfAbsent(constraint.factory) { "_constraint$suffix" } + constraintBuilder.add( + CodeBlock.of( + """ + _violations.addAll(%L.validate(value.%L, %L)); + if(context.isFailFast && _violations.isNotEmpty()) { + return _violations; + } + + """.trimIndent(), + constraintField, field.accessor(), contextField + ) + ) + } + + for (j in field.validates.indices) { + val validated = field.validates[j] + val suffix = i.toString() + "_" + j + val validatorField = validatedToFieldName.computeIfAbsent(validated) { "_validator$suffix" } + if (field.isNotNull()) { + constraintBuilder.add( + CodeBlock.of( + """ + + _violations.addAll(%L.validate(value.%L, %L)); + if(context.isFailFast) { + return _violations; + } + + """.trimIndent(), validatorField, field.accessor(), contextField + ) + ) + } else { + constraintBuilder.add( + CodeBlock.of( + """ + if(value.%L != null) { + _violations.addAll(%L.validate(value.%L, %L)); + if(context.isFailFast) { + return _violations; + } + } + + """.trimIndent(), field.accessor(), validatorField, field.accessor(), contextField + ) + ) + } + } + } + + val constructorSpecBuilder = FunSpec.constructorBuilder().addModifiers(KModifier.PUBLIC) + for (entry in constraintToFieldName) { + val factory = entry.key + val fieldName = entry.value + val validatorType = factory.validator() + val createParameters = factory.parameters.values.joinToString(", ") { + if (it is String) { + CodeBlock.of("%S", it).toString() + } else { + CodeBlock.of("%L", it).toString() + } + } + + validatorSpecBuilder.addProperty( + PropertySpec.builder( + fieldName, + validatorType.asPoetType(), + KModifier.PRIVATE + ).build() + ) + + val parameterSpec = ParameterSpec.builder(fieldName, factory.type.asPoetType()).build() + parameterSpecs.add(parameterSpec) + constructorSpecBuilder + .addParameter(parameterSpec) + .addStatement("this.%L = %L.create(%L)", fieldName, fieldName, createParameters) + } + + for (entry in validatedToFieldName) { + val fieldName = entry.value + val fieldType = entry.key.validator().asPoetType() + PropertySpec.builder(fieldName, fieldType, KModifier.PRIVATE).build(); + validatorSpecBuilder.addProperty(PropertySpec.builder(fieldName, fieldType, KModifier.PRIVATE).build()) + val parameterSpec = ParameterSpec.builder(fieldName, fieldType).build() + parameterSpecs.add(parameterSpec) + constructorSpecBuilder + .addParameter(parameterSpec) + .addStatement("this.%L = %L", fieldName, fieldName) + } + + val validateMethodSpecBuilder = FunSpec.builder("validate") + .addModifiers(KModifier.OVERRIDE) + .returns("kotlin.collections.MutableList".asType(listOf(VIOLATION_TYPE.canonicalName.asType())).asPoetType()) + .addParameter(ParameterSpec.builder("value", meta.source.copy(true)).build()) + .addParameter(ParameterSpec.builder("context", CONTEXT_TYPE.canonicalName.asType().asPoetType()).build()) + .addCode( + CodeBlock.of( + """ + if(value == null) { + return mutableListOf(context.violates("Input value is null")); + } + + val _violations = %T(); + + """.trimIndent(), ArrayList::class + ) + ) + + contextBuilder.forEach { b -> validateMethodSpecBuilder.addCode(b) } + constraintBuilder.forEach { b -> validateMethodSpecBuilder.addCode(b) } + validateMethodSpecBuilder.addCode(CodeBlock.of("return _violations")) + + val typeSpec = validatorSpecBuilder + .addFunction(constructorSpecBuilder.build()) + .addFunction(validateMethodSpecBuilder.build()) + .build() + + return ValidSymbolProcessor.ValidatorSpec(meta, typeSpec, parameterSpecs) + } + + private fun getValidatorMeta(declaration: KSClassDeclaration): ValidatorMeta { + if (declaration.classKind == ClassKind.INTERFACE || declaration.classKind == ClassKind.ENUM_CLASS) { + throw ProcessingErrorException("Validation can't be generated for: ${declaration.classKind}", declaration) + } + + val elementFields = declaration.getAllProperties() + .filter { p -> !p.modifiers.contains(Modifier.JAVA_STATIC) } + .filter { p -> !p.modifiers.contains(Modifier.CONST) } + .toList() + + val fields = ArrayList() + for (fieldProperty in elementFields) { + val constraints = getConstraints(fieldProperty) + val validateds = getValid(fieldProperty) + val isNullable = fieldProperty.type.resolve().isMarkedNullable + if (constraints.isNotEmpty() || validateds.isNotEmpty()) { + fields.add( + Field( + fieldProperty.type.asType(), + fieldProperty.simpleName.asString(), + declaration.modifiers.any { m -> m == Modifier.DATA }, + isNullable, + constraints, + validateds + ) + ) + } + } + + val source = declaration.asType(listOf()).toTypeName() + return ValidatorMeta( + source, + declaration, + ValidatorType( + VALIDATOR_TYPE.parameterizedBy(declaration.asType(listOf()).toTypeName()) + ), + fields + ) + } + + private fun getConstraints(field: KSPropertyDeclaration): List { + return ValidUtils.getConstraints(field.type, field.annotations) + } + + private fun getValid(field: KSPropertyDeclaration): List { + return if (field.annotations.any { a -> a.annotationType.asType().canonicalName() == VALID_TYPE.canonicalName }) + listOf(Validated(field.type.asType())) + else + emptyList() + } + + fun generate(symbol: KSAnnotated) { + if (symbol is KSClassDeclaration && symbol.classKind == ClassKind.INTERFACE && symbol.modifiers.contains(Modifier.SEALED)) { + return this.generateForSealed(symbol) + } + val meta = symbol.visitClass { clazz -> getValidatorMeta(clazz) } + if (meta == null) { + return + } + val validatorSpec = getValidatorSpec(meta) + val fileSpec = FileSpec.builder(validatorSpec.meta.sourceDeclaration.packageName.asString(), validatorSpec.spec.name!!) + .addType(validatorSpec.spec) + .build() + + fileSpec.writeTo(codeGenerator = codeGenerator, aggregating = false) + } + + private fun generateForSealed(symbol: KSClassDeclaration) { + val typeName = symbol.asType(listOf()).toTypeName() + val validatorTypeName = VALIDATOR_TYPE.parameterizedBy(typeName) + val validatorSpecBuilder = TypeSpec.classBuilder(symbol.generatedClassName("Validator")) + .addSuperinterface(validatorTypeName) + .generated(ValidatorGenerator::class) + symbol.containingFile?.let(validatorSpecBuilder::addOriginatingKSFile) + for (typeParameter in symbol.typeParameters) { + validatorSpecBuilder.addTypeVariable(typeParameter.toTypeVariableName()) + } + + val constructor = FunSpec.constructorBuilder() + val method = FunSpec.builder("validate") + .addModifiers(KModifier.OVERRIDE) + .returns(CommonClassNames.list.parameterizedBy(VIOLATION_TYPE)) + .addParameter("value", typeName.copy(true)) + .addParameter("context", CONTEXT_TYPE) + + for ((i, subclass) in symbol.collectFinalSealedSubtypes().withIndex()) { + val name = "_validator${i + 1}" + val subtypeName = subclass.asType(listOf()).toTypeName() + val fieldValidator = VALIDATOR_TYPE.parameterizedBy(subtypeName) + validatorSpecBuilder.addProperty(name, fieldValidator, KModifier.PRIVATE) + constructor.addParameter(name, fieldValidator) + .addStatement("this.%N = %N", name, name) + if (i > 0) { + method.nextControlFlow("else if (value is %T)", subtypeName) + } else { + method.beginControlFlow("if (value is %T)", subtypeName) + } + method.addStatement("return %N.validate(value, context)", name) + } + validatorSpecBuilder.addFunction(method.endControlFlow().addStatement("throw %T()", IllegalStateException::class.asClassName()).build()) + validatorSpecBuilder.addFunction(constructor.build()) + + val spec = validatorSpecBuilder.build() + + val fileSpec = FileSpec.builder(symbol.packageName.asString(), spec.name!!) + .addType(spec) + .build() + + fileSpec.writeTo(codeGenerator = codeGenerator, aggregating = false) + } +} diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtension.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtension.kt index 6ddf97813..1f9189850 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtension.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtension.kt @@ -2,13 +2,19 @@ package ru.tinkoff.kora.validation.symbol.processor.extension import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.getConstructors +import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.* import ru.tinkoff.kora.kora.app.ksp.extension.ExtensionResult import ru.tinkoff.kora.kora.app.ksp.extension.KoraExtension +import ru.tinkoff.kora.ksp.common.AnnotationUtils.findAnnotation +import ru.tinkoff.kora.ksp.common.generatedClass import ru.tinkoff.kora.validation.symbol.processor.VALIDATOR_TYPE +import ru.tinkoff.kora.validation.symbol.processor.VALID_TYPE +import ru.tinkoff.kora.validation.symbol.processor.ValidatorGenerator -class ValidKoraExtension(resolver: Resolver) : KoraExtension { +class ValidKoraExtension(resolver: Resolver, codeGenerator: CodeGenerator) : KoraExtension { + private val gen = ValidatorGenerator(codeGenerator) private val validatorType = resolver.getClassDeclarationByName(VALIDATOR_TYPE.canonicalName)!!.asStarProjectedType() @@ -17,23 +23,26 @@ class ValidKoraExtension(resolver: Resolver) : KoraExtension { val erasure = actualType.starProjection() if (erasure == validatorType) { val possibleJsonClass = type.arguments[0] - return { generateWriter(resolver, possibleJsonClass) } + return { generateValidator(resolver, possibleJsonClass) } } return null } - private fun generateWriter(resolver: Resolver, componentArgumentType: KSTypeArgument): ExtensionResult { + private fun generateValidator(resolver: Resolver, componentArgumentType: KSTypeArgument): ExtensionResult { val argumentType = componentArgumentType.type!!.resolve() val argumentTypeClass = argumentType.declaration as KSClassDeclaration val packageElement = argumentTypeClass.packageName.asString() - val validatorName = "$" + argumentTypeClass.simpleName.asString() + "_Validator" + val validatorName = argumentTypeClass.generatedClass("Validator") val resultDeclaration = resolver.getClassDeclarationByName("$packageElement.$validatorName") if (resultDeclaration != null) { return ExtensionResult.fromConstructor(findDefaultConstructor(resultDeclaration), resultDeclaration) } - + if (argumentTypeClass.findAnnotation(VALID_TYPE) != null) { + return ExtensionResult.RequiresCompilingResult + } + gen.generate(argumentTypeClass) return ExtensionResult.RequiresCompilingResult } diff --git a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtensionFactory.kt b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtensionFactory.kt index 2f3b6794b..74e05c02b 100644 --- a/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtensionFactory.kt +++ b/validation/validation-symbol-processor/src/main/kotlin/ru/tinkoff/kora/validation/symbol/processor/extension/ValidKoraExtensionFactory.kt @@ -14,7 +14,7 @@ class ValidKoraExtensionFactory : ExtensionFactory { return if (json == null) { null } else { - ValidKoraExtension(resolver) + ValidKoraExtension(resolver, codeGenerator) } } } diff --git a/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidationExtensionTest.kt b/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidationExtensionTest.kt new file mode 100644 index 000000000..06636b3d1 --- /dev/null +++ b/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidationExtensionTest.kt @@ -0,0 +1,70 @@ +package ru.tinkoff.kora.validation.symbol.processor + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import ru.tinkoff.kora.kora.app.ksp.KoraAppProcessorProvider +import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest + +class ValidationExtensionTest : AbstractSymbolProcessorTest() { + @Test + fun testExtension() { + compile( + listOf(KoraAppProcessorProvider(), ValidSymbolProcessorProvider()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size + import ru.tinkoff.kora.validation.common.annotation.Valid + + @Valid + data class TestRecord(@Size(min = 1, max = 5) val list: List) {} + + """.trimIndent(), + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.annotation.Root; + import ru.tinkoff.kora.validation.common.Validator; + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule; + @KoraApp + interface TestApp : ValidatorModule { + @Root + fun root(testRecordValidator: Validator) = "" + } + + """.trimIndent() + ) + compileResult.assertSuccess() + val validatorClass = compileResult.loadClass("\$TestRecord_Validator") + assertThat(validatorClass).isNotNull() + val graph = compileResult.loadClass("TestAppGraph") + assertThat(graph).isNotNull() + } + + @Test + fun testExtensionNoAnnotationProcessor() { + compile( + listOf(KoraAppProcessorProvider(), ValidSymbolProcessorProvider()), + """ + import ru.tinkoff.kora.validation.common.annotation.Size + + data class TestRecord(@Size(min = 1, max = 5) val list: List) {} + """.trimIndent(), + """ + import ru.tinkoff.kora.common.KoraApp + import ru.tinkoff.kora.common.annotation.Root + import ru.tinkoff.kora.validation.common.Validator + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule + + @KoraApp + public interface TestApp : ValidatorModule { + @Root + fun root(testRecordValidator: Validator) = "" + } + + """.trimIndent() + ) + compileResult.assertSuccess() + val validatorClass = compileResult.loadClass("\$TestRecord_Validator") + assertThat(validatorClass).isNotNull() + val graph = compileResult.loadClass("TestAppGraph") + assertThat(graph).isNotNull() + } +} diff --git a/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorSealedTypeTest.kt b/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorSealedTypeTest.kt new file mode 100644 index 000000000..4567bf966 --- /dev/null +++ b/validation/validation-symbol-processor/src/test/kotlin/ru/tinkoff/kora/validation/symbol/processor/ValidatorSealedTypeTest.kt @@ -0,0 +1,95 @@ +package ru.tinkoff.kora.validation.symbol.processor + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import ru.tinkoff.kora.kora.app.ksp.KoraAppProcessorProvider +import ru.tinkoff.kora.ksp.common.AbstractSymbolProcessorTest + +class ValidatorSealedTypeTest : AbstractSymbolProcessorTest() { + override fun commonImports(): String { + return super.commonImports() + """ + import ru.tinkoff.kora.validation.common.annotation.*; + """.trimIndent() + } + + @Test + fun testSealedInterface() { + compile( + listOf(KoraAppProcessorProvider(), ValidSymbolProcessorProvider()), + """ + @Valid + sealed interface TestInterface { + @Valid + data class TestRecord(@Size(min = 1, max = 5) val list: List): TestInterface + } + + """.trimIndent() + ) + compileResult.assertSuccess() + val validatorClass = compileResult.loadClass("\$TestInterface_Validator") + assertThat(validatorClass).isNotNull() + } + + @Test + fun testExtension() { + compile( + listOf(KoraAppProcessorProvider(), ValidSymbolProcessorProvider()), + """ + @Valid + sealed interface TestInterface { + @Valid + data class TestRecord(@Size(min = 1, max = 5) val list: List): TestInterface + } + + """.trimIndent(), + """ + import ru.tinkoff.kora.common.KoraApp; + import ru.tinkoff.kora.common.annotation.Root; + import ru.tinkoff.kora.validation.common.Validator; + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule; + @KoraApp + interface TestApp : ValidatorModule { + @Root + fun root(testRecordValidator: Validator) = "" + } + + """.trimIndent() + ) + compileResult.assertSuccess() + val validatorClass = compileResult.loadClass("\$TestInterface_Validator") + assertThat(validatorClass).isNotNull() + val graph = compileResult.loadClass("TestAppGraph") + assertThat(graph).isNotNull() + } + + @Test + fun testExtensionNoAnnotationProcessor() { + compile( + listOf(KoraAppProcessorProvider(), ValidSymbolProcessorProvider()), + """ + sealed interface TestInterface { + data class TestRecord(@Size(min = 1, max = 5) val list: List): TestInterface + } + + """.trimIndent(), + """ + import ru.tinkoff.kora.common.KoraApp + import ru.tinkoff.kora.common.annotation.Root + import ru.tinkoff.kora.validation.common.Validator + import ru.tinkoff.kora.validation.common.constraint.ValidatorModule + + @KoraApp + public interface TestApp : ValidatorModule { + @Root + fun root(testRecordValidator: Validator) = "" + } + + """.trimIndent() + ) + compileResult.assertSuccess() + val validatorClass = compileResult.loadClass("\$TestInterface_Validator") + assertThat(validatorClass).isNotNull() + val graph = compileResult.loadClass("TestAppGraph") + assertThat(graph).isNotNull() + } +}