diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java index 9427dd4f4e5..ccc96bd62b3 100644 --- a/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java @@ -1104,9 +1104,17 @@ private AnnotationMetadata buildInternalMulti( Optional value = annotationMetadata.stringValue(DefaultScope.class); value.ifPresent(name -> annotationMetadata.addDeclaredAnnotation(name, Collections.emptyMap())); } + if (annotationMetadata instanceof MutableAnnotationMetadata mutableAnnotationMetadata) { + postProcess(mutableAnnotationMetadata, element); + } return annotationMetadata; } + protected void postProcess(MutableAnnotationMetadata mutableAnnotationMetadata, + T element) { + //no-op + } + private void includeAnnotations(DefaultAnnotationMetadata annotationMetadata, T element, boolean originatingElementIsSameParent, diff --git a/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java new file mode 100644 index 00000000000..95579e731dd --- /dev/null +++ b/core-processor/src/main/java/io/micronaut/inject/annotation/internal/KotlinDeprecatedTransformer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.annotation.internal; + +import io.micronaut.core.annotation.AnnotationValue; +import io.micronaut.inject.annotation.NamedAnnotationTransformer; +import io.micronaut.inject.visitor.VisitorContext; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +/** + * Allows treating the Kotlin deprecated annotation as the Java one. + * + * @since 4.0.0 + */ +public final class KotlinDeprecatedTransformer implements NamedAnnotationTransformer { + @Override + public String getName() { + return "kotlin.Deprecated"; + } + + @Override + public List> transform(AnnotationValue annotation, VisitorContext visitorContext) { + return Collections.singletonList( + AnnotationValue.builder(Deprecated.class).build() + ); + } +} diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java index 20478e793a4..6ffc5e37ec5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/ElementFactory.java @@ -51,8 +51,10 @@ public interface ElementFactory { * @param resolvedGenerics The resolved generics * @return The class element * @since 4.0.0 + * @deprecated no longer used */ @NonNull + @Deprecated ClassElement newClassElement(@NonNull C type, @NonNull ElementAnnotationMetadataFactory annotationMetadataFactory, @NonNull Map resolvedGenerics); diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java index cc0ada998a0..390cceb7412 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/PropertyElement.java @@ -127,6 +127,16 @@ default AccessKind getWriteAccessKind() { return AccessKind.METHOD; } + /** + * Does a this property override the given property. Supported only with languages that have native properties. + * @param overridden The overridden method. + * @return True this property overrides the given property. + * @since 4.0.0 + */ + default boolean overrides(PropertyElement overridden) { + return false; + } + /** * The access type for bean properties. * @since 4.0.0 diff --git a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java index a55912414a7..bd53ed6e6e5 100644 --- a/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java +++ b/core-processor/src/main/java/io/micronaut/inject/ast/utils/EnclosedElementsQuery.java @@ -19,18 +19,20 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ConstructorElement; +import io.micronaut.inject.ast.Element; import io.micronaut.inject.ast.ElementModifier; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.FieldElement; import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -51,7 +53,7 @@ @Internal public abstract class EnclosedElementsQuery { - private final Map elementsCache = new HashMap<>(); + private final Map elementsCache = new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(200).build(); /** * Return the elements that match the given query. @@ -172,6 +174,8 @@ private boolean reduceElements(io.micronaut.inject.ast.Element newElement, if (!result.isIncludeOverriddenMethods()) { if (newElement instanceof MethodElement && existingElement instanceof MethodElement) { return ((MethodElement) newElement).overrides((MethodElement) existingElement); + } else if (newElement instanceof PropertyElement newPropertyElement && existingElement instanceof PropertyElement existingPropertyElement) { + return newPropertyElement.overrides(existingPropertyElement); } } return false; @@ -188,7 +192,13 @@ private Collection getAllElements(C classNode, Set addedFromClassElements = new LinkedHashSet<>(); classElements: for (N element : classElements) { - io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(element, this::toAstElement); + N cacheKey = getCacheKey(element); + io.micronaut.inject.ast.Element newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + if (!result.getElementType().isInstance(newElement)) { + // dirty cache + elementsCache.remove(cacheKey); + newElement = elementsCache.computeIfAbsent(cacheKey, e -> this.toAstElement(e, result.getElementType())); + } for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { io.micronaut.inject.ast.Element existingElement = iterator.next(); if (newElement.equals(existingElement)) { @@ -208,6 +218,15 @@ private Collection getAllElements(C classNode, return elements; } + /** + * get the cache key. + * @param element The element + * @return The cache key + */ + protected N getCacheKey(N element) { + return element; + } + private void collectHierarchy(C classNode, boolean onlyDeclared, List> hierarchy, @@ -279,9 +298,10 @@ protected Set getExcludedNativeElements(@NonNull ElementQuery.Result resul * Converts the native element to the AST element. * * @param enclosedElement The native element. + * @param elementType The result type * @return The AST element */ @NonNull - protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement); + protected abstract io.micronaut.inject.ast.Element toAstElement(N enclosedElement, Class elementType); } diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 0517e9f43e8..b45f3d3b1ca 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -829,7 +829,7 @@ private void writeInstantiateMethod(ClassWriter classWriter, MethodElement const private void invokeBeanConstructor(GeneratorAdapter writer, MethodElement constructor, BiConsumer argumentsPusher) { boolean isConstructor = constructor instanceof ConstructorElement; - boolean isCompanion = constructor != defaultConstructor && constructor.getDeclaringType().getSimpleName().endsWith("$Companion"); + boolean isCompanion = constructor.getDeclaringType().getSimpleName().endsWith("$Companion"); List constructorArguments = Arrays.asList(constructor.getParameters()); Collection argumentTypes = constructorArguments.stream().map(pe -> diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java index e2fd4cfad59..04ce327a635 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java @@ -21,6 +21,7 @@ import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.aop.writer.AopProxyWriter; import io.micronaut.context.RequiresCondition; +import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationUtil; @@ -90,7 +91,7 @@ protected void visitAnnotationMetadata(BeanDefinitionVisitor writer, AnnotationM annotation.stringValue(RequiresCondition.MEMBER_BEAN_PROPERTY) .ifPresent(beanProperty -> { annotation.stringValue(RequiresCondition.MEMBER_BEAN) - .map(className -> visitorContext.getClassElement(className, visitorContext.getElementAnnotationMetadataFactory().readOnly()).get()) + .flatMap(className -> visitorContext.getClassElement(className, visitorContext.getElementAnnotationMetadataFactory().readOnly())) .ifPresent(classElement -> { String requiredValue = annotation.stringValue().orElse(null); String notEqualsValue = annotation.stringValue(RequiresCondition.MEMBER_NOT_EQUALS).orElse(null); @@ -121,6 +122,12 @@ protected boolean visitIntrospectedMethod(BeanDefinitionVisitor visitor, ClassEl || InterceptedMethodUtil.hasDeclaredAroundAdvice(methodElement.getAnnotationMetadata())) { addToIntroduction(aopProxyWriter, typeElement, methodElement, false); return true; + } else if (!methodElement.isAbstract() && methodElement.hasDeclaredStereotype(Executable.class)) { + aopProxyWriter.visitExecutableMethod( + typeElement, + methodElement, + visitorContext + ); } return false; } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java index 522358f2a24..8b386174870 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/BeanDefinitionCreatorFactory.java @@ -15,6 +15,7 @@ */ package io.micronaut.inject.processing; +import io.micronaut.aop.Interceptor; import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil; import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.DefaultScope; @@ -70,6 +71,9 @@ public static BeanDefinitionCreator produce(ClassElement classElement, VisitorCo if (classElement.hasStereotype("groovy.lang.Singleton")) { throw new ProcessingException(classElement, "Class annotated with groovy.lang.Singleton instead of jakarta.inject.Singleton. Import jakarta.inject.Singleton to use Micronaut Dependency Injection."); } + if (classElement.isEnum()) { + throw new ProcessingException(classElement, "Enum types cannot be defined as beans"); + } return new DeclaredBeanElementCreator(classElement, visitorContext, false); } return Collections::emptyList; @@ -110,7 +114,7 @@ private static boolean containsInjectPoint(AnnotationMetadata annotationMetadata } private static boolean isAopProxyType(ClassElement classElement) { - return !classElement.isAssignable("io.micronaut.aop.Interceptor") && InterceptedMethodUtil.hasAroundStereotype(classElement.getAnnotationMetadata()); + return !classElement.isAssignable(Interceptor.class) && InterceptedMethodUtil.hasAroundStereotype(classElement.getAnnotationMetadata()); } public static boolean isDeclaredBeanInMetadata(AnnotationMetadata concreteClassMetadata) { diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java index 1a8806df2c0..79846771093 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/DeclaredBeanElementCreator.java @@ -158,6 +158,7 @@ private void build(BeanDefinitionVisitor visitor) { if (processAsProperties()) { memberQuery = memberQuery.excludePropertyElements(); for (PropertyElement propertyElement : classElement.getBeanProperties()) { + propertyElement.getField().ifPresent(processedFields::add); visitPropertyInternal(visitor, propertyElement); } } else { @@ -176,7 +177,7 @@ private void build(BeanDefinitionVisitor visitor) { visitFieldInternal(visitor, fieldElement); } else if (memberElement instanceof MethodElement methodElement) { visitMethodInternal(visitor, methodElement); - } else { + } else if (!(memberElement instanceof PropertyElement)) { throw new IllegalStateException("Unknown element"); } } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java index 7e11e77e6e5..9bdeb03146f 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/FactoryBeanElementCreator.java @@ -186,8 +186,20 @@ private void buildProducedBeanDefinition(BeanDefinitionWriter producedBeanDefini producedType.annotate(ConfigurationReader.class, builder -> builder.member(ConfigurationReader.PREFIX, ConfigurationUtils.getRequiredTypePath(producedType))); } - if (producingElement instanceof MethodElement) { - producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, (MethodElement) producingElement); + if (producingElement instanceof PropertyElement propertyElement) { + MethodElement readMethod = propertyElement.getReadMethod().orElse(null); + if (readMethod != null) { + producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, readMethod); + } else { + FieldElement fieldElement = propertyElement.getField().orElse(null); + if (fieldElement != null && fieldElement.isAccessible()) { + producedBeanDefinitionWriter.visitBeanFactoryField(classElement, fieldElement); + } else { + throw new ProcessingException(producingElement, "A property element that defines the @Bean annotation must have an accessible getter or field"); + } + } + } else if (producingElement instanceof MethodElement methodElement) { + producedBeanDefinitionWriter.visitBeanFactoryMethod(classElement, methodElement); } else { producedBeanDefinitionWriter.visitBeanFactoryField(classElement, (FieldElement) producingElement); } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java index f717d4284f4..c61847e5e45 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/IntroductionInterfaceBeanElementCreator.java @@ -19,6 +19,7 @@ import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.ElementQuery; import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.PropertyElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.BeanDefinitionVisitor; @@ -70,7 +71,22 @@ public void buildInternal() { for (MethodElement methodElement : methods) { visitIntrospectedMethod(aopProxyWriter, classElement, methodElement); } + List beanProperties = classElement.getSyntheticBeanProperties(); + for (PropertyElement beanProperty : beanProperties) { + handlePropertyMethod(aopProxyWriter, methods, beanProperty.getReadMethod().orElse(null)); + handlePropertyMethod(aopProxyWriter, methods, beanProperty.getWriteMethod().orElse(null)); + } beanDefinitionWriters.add(aopProxyWriter); } + private void handlePropertyMethod(BeanDefinitionVisitor aopProxyWriter, List methods, MethodElement method) { + if (method != null && method.isAbstract() && !methods.contains(method)) { + visitIntrospectedMethod( + aopProxyWriter, + this.classElement, + method + ); + } + } + } diff --git a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java index f1d8899b74c..6176af8ec98 100644 --- a/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java +++ b/core-processor/src/main/java/io/micronaut/inject/processing/ProcessingException.java @@ -27,11 +27,15 @@ public final class ProcessingException extends RuntimeException { private final transient Element originatingElement; - private final String message; public ProcessingException(Element element, String message) { + super(message); this.originatingElement = element; - this.message = message; + } + + public ProcessingException(Element originatingElement, String message, Throwable cause) { + super(message, cause); + this.originatingElement = originatingElement; } @Nullable @@ -42,8 +46,4 @@ public Object getOriginatingElement() { return null; } - @Override - public String getMessage() { - return message; - } } diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java index 4db6bb10595..e6534e5b26b 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionVisitor.java @@ -18,7 +18,12 @@ import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.util.Toggleable; import io.micronaut.inject.BeanDefinition; -import io.micronaut.inject.ast.*; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.FieldElement; +import io.micronaut.inject.ast.MethodElement; +import io.micronaut.inject.ast.ParameterElement; +import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.configuration.ConfigurationMetadataBuilder; import io.micronaut.inject.visitor.VisitorContext; import org.objectweb.asm.Type; @@ -136,7 +141,7 @@ void visitDefaultConstructor( /** * Alter the super class of this bean definition. The passed class should be a subclass of - * {@link io.micronaut.context.AbstractBeanDefinition}. + * {@link io.micronaut.context.AbstractInitializableBeanDefinition}. * * @param name The super type */ diff --git a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index f1542a3a9fe..37ec9c8565e 100644 --- a/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -637,6 +637,21 @@ public BeanDefinitionWriter(Element beanProducingElement, } final ClassElement declaringType = factoryMethodElement.getOwningType(); this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + upperCaseMethodName + uniqueIdentifier + CLASS_SUFFIX; + } else if (beanProducingElement instanceof PropertyElement factoryPropertyElement) { + autoApplyNamedToBeanProducingElement(beanProducingElement); + final ClassElement producedElement = factoryPropertyElement.getGenericType(); + this.beanTypeElement = producedElement; + this.packageName = producedElement.getPackageName(); + this.isInterface = producedElement.isInterface(); + this.isAbstract = beanProducingElement.isAbstract(); + this.beanFullClassName = producedElement.getName(); + this.beanSimpleClassName = producedElement.getSimpleName(); + String upperCaseMethodName = NameUtils.capitalize(factoryPropertyElement.getName()); + if (uniqueIdentifier == null) { + throw new IllegalArgumentException("Factory methods require passing a unique identifier"); + } + final ClassElement declaringType = factoryPropertyElement.getOwningType(); + this.beanDefinitionName = declaringType.getPackageName() + "." + prefixClassName(declaringType.getSimpleName()) + "$" + upperCaseMethodName + uniqueIdentifier + CLASS_SUFFIX; } else if (beanProducingElement instanceof FieldElement factoryMethodElement) { autoApplyNamedToBeanProducingElement(beanProducingElement); final ClassElement producedElement = factoryMethodElement.getGenericField(); @@ -663,11 +678,10 @@ public BeanDefinitionWriter(Element beanProducingElement, throw new IllegalArgumentException("Beans produced by addAssociatedBean(..) require passing a unique identifier"); } final Element originatingElement = beanElementBuilder.getOriginatingElement(); - if (originatingElement instanceof ClassElement) { - ClassElement originatingClass = (ClassElement) originatingElement; + if (originatingElement instanceof ClassElement originatingClass) { this.beanDefinitionName = getAssociatedBeanName(uniqueIdentifier, originatingClass); - } else if (originatingElement instanceof MethodElement) { - ClassElement originatingClass = ((MethodElement) originatingElement).getDeclaringType(); + } else if (originatingElement instanceof MethodElement methodElement) { + ClassElement originatingClass = methodElement.getDeclaringType(); this.beanDefinitionName = getAssociatedBeanName(uniqueIdentifier, originatingClass); } else { throw new IllegalArgumentException("Unsupported originating element"); diff --git a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer index 5d45d537b5b..eca914babbf 100644 --- a/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer +++ b/core-processor/src/main/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -2,6 +2,7 @@ io.micronaut.inject.annotation.internal.CoreNullableTransformer io.micronaut.inject.annotation.internal.CoreNonNullTransformer io.micronaut.inject.annotation.internal.KotlinNullableMapper io.micronaut.inject.annotation.internal.KotlinNotNullMapper +io.micronaut.inject.annotation.internal.KotlinDeprecatedTransformer io.micronaut.inject.annotation.internal.JakartaPostConstructTransformer io.micronaut.inject.annotation.internal.JakartaPreDestroyTransformer io.micronaut.inject.annotation.internal.JakartaNullableTransformer diff --git a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java index 5ab0941b3f7..948aa36a3aa 100644 --- a/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java +++ b/core/src/main/java/io/micronaut/core/annotation/AnnotationUtil.java @@ -42,6 +42,7 @@ public class AnnotationUtil { "javax.annotation.meta.TypeQualifier", "javax.annotation.meta.TypeQualifierNickname", "kotlin.annotation.Retention", + "kotlin.Annotation", Inherited.class.getName(), SuppressWarnings.class.getName(), Override.class.getName(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a7718c76a3..5f507d0c927 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ junit5 = "5.9.1" junit-platform="1.9.1" kotlin = "1.7.20" kotlin-coroutines = "1.6.4" +ksp-testing = "1.4.9" ktor = "1.6.8" managed-logback = "1.4.5" logbook-netty = "2.14.0" @@ -74,6 +75,7 @@ managed-slf4j = "2.0.4" managed-snakeyaml = "1.33" managed-validation = "2.0.1.Final" managed-java-parser-core = "3.24.9" +managed-ksp = "1.8.0-Beta-1.0.8" micronaut-docs = "2.0.0" [libraries] @@ -106,8 +108,9 @@ managed-jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jac managed-jackson-module-afterburner = { module = "com.fasterxml.jackson.module:jackson-module-afterburner", version.ref = "managed-jackson" } managed-jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "managed-jackson" } managed-jackson-module-parameterNames = { module = "com.fasterxml.jackson.module:jackson-module-parameter-names", version.ref = "managed-jackson" } +managed-ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "managed-ksp" } +managed-ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "managed-ksp" } managed-java-parser-core = { module = "com.github.javaparser:javaparser-symbol-solver-core", version.ref = "managed-java-parser-core" } - managed-methvin-directoryWatcher = { module = "io.methvin:directory-watcher", version.ref = "managed-methvin-directory-watcher" } managed-netty-buffer = { module = "io.netty:netty-buffer", version.ref = "managed-netty" } @@ -204,6 +207,8 @@ kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "kotlin-coroutines" } kotlinx-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } +ksp-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "ksp-testing" } + log4j = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } diff --git a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java index 9c54f78404b..3b709fe96b5 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java @@ -139,7 +139,7 @@ public HttpClientIntroductionAdvice( } /** - * Interceptor to apply headers, cookies, parameter and body arguements. + * Interceptor to apply headers, cookies, parameter and body arguments. * * @param context The context * @return httpClient or future diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java index f468f183303..7c876348b6b 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyClassElement.java @@ -905,7 +905,7 @@ protected boolean excludeClass(ClassNode classNode) { } @Override - protected Element toAstElement(AnnotatedNode enclosedElement) { + protected Element toAstElement(AnnotatedNode enclosedElement, Class elementType) { final GroovyElementFactory elementFactory = visitorContext.getElementFactory(); if (isSource) { if (!(enclosedElement instanceof ConstructorNode) && enclosedElement instanceof MethodNode methodNode) { diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java index dfcc9acb390..389c55ae167 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/visitor/GroovyMethodElement.java @@ -104,8 +104,7 @@ public ClassElement[] getThrownTypes() { return Arrays.stream(exceptions) .map(cn -> getGenericElement(cn, visitorContext.getElementFactory().newClassElement( cn, - elementAnnotationMetadataFactory, - Collections.emptyMap() + elementAnnotationMetadataFactory ))).toArray(ClassElement[]::new); } return ClassElement.ZERO_CLASS_ELEMENTS; diff --git a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy index 4d9203eb4c2..4231e0566bb 100644 --- a/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy +++ b/inject-java-test/src/test/groovy/io/micronaut/inject/visitor/beans/BeanIntrospectionSpec.groovy @@ -302,7 +302,6 @@ package fieldaccess; import io.micronaut.core.annotation.*; - @Introspected(accessKind={Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD}) class Test { public String one; diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java index b482d52c88f..0f39bb3790b 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaClassElement.java @@ -666,7 +666,7 @@ public List getBoundGenericTypes() { return typeArguments.stream() //return getGenericTypeInfo().getOrDefault(classElement.getQualifiedName().toString(), Collections.emptyMap()).values().stream() .map(tm -> mirrorToClassElement(tm, visitorContext, getGenericTypeInfo())) - .collect(Collectors.toList()); + .toList(); } @NonNull @@ -675,7 +675,7 @@ public List getDeclaredGenericPlaceholders( return classElement.getTypeParameters().stream() // we want the *declared* variables, so we don't pass in our genericsInfo. .map(tpe -> (GenericPlaceholderElement) mirrorToClassElement(tpe.asType(), visitorContext)) - .collect(Collectors.toList()); + .toList(); } @NonNull @@ -901,7 +901,7 @@ protected boolean excludeClass(TypeElement classNode) { } @Override - protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement) { + protected io.micronaut.inject.ast.Element toAstElement(Element enclosedElement, Class elementType) { final JavaElementFactory elementFactory = visitorContext.getElementFactory(); return switch (enclosedElement.getKind()) { case METHOD -> elementFactory.newMethodElement( diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java index fc16506a18c..2a26ef2772c 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/visitor/JavaMethodElement.java @@ -276,7 +276,7 @@ protected ClassElement returnType(Map> info) { tm = ((WildcardType) tm).getSuperBound(); } // check Void - if ((tm instanceof DeclaredType) && sameType("kotlin.Unit", (DeclaredType) tm)) { + if ((tm instanceof DeclaredType dt) && sameType("kotlin.Unit", dt)) { return PrimitiveElement.VOID; } else { return mirrorToClassElement(tm, visitorContext, info, true); diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy index 2aa04cd70b2..c03291c40a2 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/AroundConstructCompileSpec.groovy @@ -676,24 +676,12 @@ class MyBean { } } -@io.micronaut.context.annotation.Factory -class MyFactory { - @TestAnn - @Singleton - MyOtherBean test(io.micronaut.context.env.Environment env) { - return new MyOtherBean(); - } -} - -class MyOtherBean {} - @Retention(RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @AroundConstruct @interface TestAnn { } - @Factory class InterceptorFactory { boolean aroundConstructInvoked = false; @@ -722,10 +710,8 @@ class InterceptorFactory { !(instance instanceof Intercepted) factory.aroundConstructInvoked - cleanup: context.close() - } void 'test around construct with introduction advice'() { diff --git a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy index 9c4daec3088..b24b710b500 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/compile/LifeCycleWithProxyTargetSpec.groovy @@ -1,14 +1,16 @@ package io.micronaut.aop.compile import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition import io.micronaut.inject.writer.BeanDefinitionWriter class LifeCycleWithProxyTargetSpec extends AbstractTypeElementSpec { void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at class level"() { when: - BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' + ApplicationContext context = buildContext( ''' package test; import io.micronaut.aop.proxytarget.*; @@ -21,16 +23,16 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + public String someMethod() { return "good"; } - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -38,12 +40,27 @@ class MyBean { } ''') + def beanDefinition = getBeanDefinition(context, 'test.MyBean') + then: !beanDefinition.isAbstract() beanDefinition != null beanDefinition.postConstructMethods.size() == 1 beanDefinition.preDestroyMethods.size() == 1 + when: + def instance = getBean(context, 'test.MyBean') + + then:"proxy post construct methods are not invoked" + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 0 + + and:"proxy target post construct methods are invoked" + instance.interceptedTarget().count == 1 + + cleanup: + context.close() } void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { @@ -60,7 +77,7 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @Mutating("someVal") public String someMethod() { return "good"; @@ -70,7 +87,7 @@ class MyBean { void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; @@ -99,17 +116,17 @@ class MyBean { @jakarta.inject.Inject public ConversionService conversionService; public int count = 0; - + @jakarta.annotation.PostConstruct void created() { count++; } - + @javax.annotation.PreDestroy void destroyed() { count--; } - + @Mutating("someVal") public String someMethod() { return "good"; diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy index bfa0fa6a334..0b6a6b7602b 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -28,6 +28,30 @@ import io.micronaut.inject.writer.BeanDefinitionVisitor */ class IntroductionAdviceWithNewInterfaceSpec extends AbstractTypeElementSpec { + + void "test introduction advice with primitive generics"() { + when: + def context = buildContext( 'test.MyRepo', ''' +package test; + +import io.micronaut.aop.introduction.*; +import javax.validation.constraints.NotNull; + +@RepoDef +interface MyRepo extends DeleteByIdCrudRepo { + + @Override void deleteById(@NotNull Integer integer); +} + + +''', true) + + def bean = + getBean(context, 'test.MyRepo') + then: + bean != null + } + void "test that it is possible for @Introduction advice to implement additional interfaces on concrete classes"() { when: BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' @@ -138,7 +162,7 @@ interface MyBean { beanDefinition != null ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) beanDefinition.injectedFields.size() == 0 - beanDefinition.executableMethods.size() == 2 + beanDefinition.executableMethods.size() == 3 beanDefinition.findMethod("getBar").isPresent() beanDefinition.findMethod("onApplicationEvent", Object).isPresent() diff --git a/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java b/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java index 128f7a785d1..3aeaa3162d4 100644 --- a/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java +++ b/inject-java/src/test/groovy/io/micronaut/aop/introduction/Stub.java @@ -16,7 +16,6 @@ package io.micronaut.aop.introduction; import io.micronaut.aop.Introduction; -import io.micronaut.context.annotation.Executable; import io.micronaut.context.annotation.Type; import java.lang.annotation.Documented; diff --git a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy index 596932084e7..708a650ee32 100644 --- a/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/visitors/ClassElementSpec.groovy @@ -31,6 +31,7 @@ import io.micronaut.inject.ast.MemberElement import io.micronaut.inject.ast.MethodElement import io.micronaut.inject.ast.PackageElement import io.micronaut.inject.ast.PrimitiveElement +import io.micronaut.inject.ast.PropertyElement import jakarta.inject.Singleton import spock.lang.IgnoreIf import spock.lang.Issue @@ -42,10 +43,213 @@ import java.sql.SQLException import java.util.function.Supplier class ClassElementSpec extends AbstractTypeElementSpec { + void "test class element generics"() { + given: + ClassElement classElement = buildClassElement(''' +package ast.test; + +import java.util.*; + +final class Test extends Parent implements One { + Test(String constructorProp) { + super(constructorProp); + } +} + +abstract class Parent extends java.util.AbstractCollection { + private final T parentConstructorProp; + private T conventionProp; + + Parent(T parentConstructorProp) { + this.parentConstructorProp = parentConstructorProp; + } + + public void setConventionProp(T conventionProp) { + this.conventionProp = conventionProp; + } + public T getConventionProp() { + return conventionProp; + } + public T getParentConstructorProp() { + return parentConstructorProp; + } + + @Override public int size() { + return 0; + } + + @Override public Iterator iterator() { + return null; + } + + public T publicFunc(T name) { + return name; + } + + public T parentFunc(T name) { + return name; + } + +} + +interface One {} +''') + List propertyElements = classElement.getBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + expect: + methodMap['add'].parameters[0].genericType.simpleName == 'String' + methodMap['add'].parameters[0].type.simpleName == 'Object' + methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'CharSequence' // why? + methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + propMap['conventionProp'].type.simpleName == 'String' + propMap['conventionProp'].genericType.simpleName == 'String' + propMap['conventionProp'].genericType.simpleName == 'String' + propMap['conventionProp'].readMethod.get().returnType.simpleName == 'CharSequence' + propMap['conventionProp'].readMethod.get().genericReturnType.simpleName == 'String' + propMap['conventionProp'].writeMethod.get().parameters[0].type.simpleName == 'CharSequence' + propMap['conventionProp'].writeMethod.get().parameters[0].genericType.simpleName == 'String' + propMap['parentConstructorProp'].type.simpleName == 'String' + propMap['parentConstructorProp'].genericType.simpleName == 'String' + methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + methodMap['parentFunc'].genericReturnType.simpleName == 'String' + methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } + + void "test class element generics - records"() { + given: + ClassElement classElement = buildClassElement(''' +package ast.test; + +import org.jetbrains.annotations.NotNull;import java.util.*; + +record Test(String constructorProp) implements Parent, One { + @Override public String publicFunc(String name) { + return null; + } + @Override public int size() { + return 0; + } + @Override public boolean isEmpty() { + return false; + } + @Override public boolean contains(Object o) { + return false; + } + @NotNull @Override public Iterator iterator() { + return null; + } + @NotNull@Override public Object[] toArray() { + return new Object[0]; + } + @NotNull@Override public T[] toArray(@NotNull T[] a) { + return null; + } + @Override public boolean add(String s) { + return false; + } + @Override public boolean remove(Object o) { + return false; + } + @Override public boolean containsAll(@NotNull Collection c) { + return false; + } + @Override public boolean addAll(@NotNull Collection c) { + return false; + } + @Override public boolean addAll(int index,@NotNull Collection c) { + return false; + } + @Override public boolean removeAll(@NotNull Collection c) { + return false; + } + @Override public boolean retainAll(@NotNull Collection c) { + return false; + } + @Override public void clear() { + + } + @Override public void add(int index, String element) { + + + }@Override public String remove(int index) { + return null; + } + @Override public int indexOf(Object o) { + return 0; + } + @Override public int lastIndexOf(Object o) { + return 0; + } + @NotNull @Override public ListIterator listIterator() { + return null; + } + @NotNull @Override public ListIterator listIterator(int index) { + return null; + } + @NotNull @Override public List subList(int fromIndex, int toIndex) { + return null; + } +} + +interface Parent extends java.util.List { + + public T constructorProp(); + + public T publicFunc(T name); + + default T parentFunc(T name) { + return name; + } + + @Override default T get(int index) { + return null; + } + @Override default T set(int index, T element) { + return null; + } +} + +interface One {} +''') + List propertyElements = classElement.getBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + expect: + methodMap['add'].parameters[1].genericType.simpleName == 'String' + methodMap['add'].parameters[1].type.simpleName == 'String' + methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'String' + methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + propMap['constructorProp'].readMethod.get().returnType.simpleName == 'String' + propMap['constructorProp'].readMethod.get().genericReturnType.simpleName == 'String' + propMap['constructorProp'].type.simpleName == 'String' + propMap['constructorProp'].genericType.simpleName == 'String' + methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + methodMap['parentFunc'].genericReturnType.simpleName == 'String' + methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } void "test equals with primitive"() { given: - def element = buildClassElement(""" + def element = buildClassElement(""" package test; class Test { @@ -54,14 +258,14 @@ class Test { """) expect: - element != PrimitiveElement.BOOLEAN - element != PrimitiveElement.VOID - element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) - PrimitiveElement.VOID != element - PrimitiveElement.INT != element - PrimitiveElement.INT.withArrayDimensions(2) != element - element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN - PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() + element != PrimitiveElement.BOOLEAN + element != PrimitiveElement.VOID + element != PrimitiveElement.BOOLEAN.withArrayDimensions(4) + PrimitiveElement.VOID != element + PrimitiveElement.INT != element + PrimitiveElement.INT.withArrayDimensions(2) != element + element.getFields().get(0).getType() == PrimitiveElement.BOOLEAN + PrimitiveElement.BOOLEAN == element.getFields().get(0).getType() } void "test resolve receiver type on method"() { @@ -1071,7 +1275,7 @@ class Person { void "test find enum fields using ElementQuery"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package elementquery; enum Test { @@ -1100,7 +1304,7 @@ enum Test { } ''') when: - List allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) + List allFields = classElement.getEnclosedElements(ElementQuery.ALL_FIELDS) List expected = [ 'publicStaticFinalField', @@ -1139,10 +1343,11 @@ enum Test { } @Issue("https://github.com/eclipse-ee4j/cdi-tck/blob/master/lang-model/src/main/java/org/jboss/cdi/lang/model/tck/InheritedMethods.java") - @Requires({ jvm.isJava9Compatible() }) // private static Since Java 9 + @Requires({ jvm.isJava9Compatible() }) + // private static Since Java 9 void "test inherited methods using ElementQuery"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package elementquery; class InheritedMethods extends SuperClassWithMethods implements SuperInterfaceWithMethods { @@ -1388,13 +1593,13 @@ public class TestController { ''') expect: - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].owningType.name == 'test.TestController' - AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].declaringType.name == 'test.TestController' + AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].owningType.name == 'test.TestController' + AllElementsVisitor.VISITED_METHOD_ELEMENTS[0].declaringType.name == 'test.TestController' } void "test fields selection"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package test; import io.micronaut.http.annotation.*; @@ -1427,46 +1632,46 @@ class Pet { ''') when: - List publicFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) + List publicFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.modifiers(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1)) then: - publicFields.size() == 1 - publicFields.stream().map(FieldElement::getName).toList() == ["pub"] + publicFields.size() == 1 + publicFields.stream().map(FieldElement::getName).toList() == ["pub"] when: - List publicFields2 = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) + List publicFields2 = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPublic())) then: - publicFields2.size() == 2 - publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] + publicFields2.size() == 2 + publicFields2.stream().map(FieldElement::getName).toList() == ["pub", "PUB_CONST"] when: - List protectedFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) + List protectedFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isProtected())) then: - protectedFields.size() == 2 - protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] + protectedFields.size() == 2 + protectedFields.stream().map(FieldElement::getName).toList() == ["protectme", "PROT_CONST"] when: - List privateFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) + List privateFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPrivate())) then: - privateFields.size() == 2 - privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] + privateFields.size() == 2 + privateFields.stream().map(FieldElement::getName).toList() == ["prvn", "PRV_CONST"] when: - List packPrvFields = classElement.getFirstTypeArgument() - .get() - .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) + List packPrvFields = classElement.getFirstTypeArgument() + .get() + .getEnclosedElements(ElementQuery.ALL_FIELDS.filter(e -> e.isPackagePrivate())) then: - packPrvFields.size() == 2 - packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] + packPrvFields.size() == 2 + packPrvFields.stream().map(FieldElement::getName).toList() == ["packprivme", "PACK_PRV_CONST"] } void "test annotations on generic type"() { given: - ClassElement classElement = buildClassElement(''' + ClassElement classElement = buildClassElement(''' package test; import io.micronaut.core.annotation.Introspected; @@ -1486,13 +1691,13 @@ class Pet { ''') when: - def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) - def returnType = method.getReturnType() - def genericReturnType = method.getGenericReturnType() + def method = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.named("save")).get(0) + def returnType = method.getReturnType() + def genericReturnType = method.getGenericReturnType() then: - returnType.hasAnnotation(Introspected) - genericReturnType.hasAnnotation(Introspected) + returnType.hasAnnotation(Introspected) + genericReturnType.hasAnnotation(Introspected) } private void assertMethodsByName(List allMethods, String name, List expectedDeclaringTypeSimpleNames) { @@ -1513,7 +1718,7 @@ class Pet { private boolean oneElementPresentWithDeclaringType(Collection elements, String declaringTypeSimpleName) { elements.stream() - .filter { it -> it.getDeclaringType().getSimpleName() == declaringTypeSimpleName} + .filter { it -> it.getDeclaringType().getSimpleName() == declaringTypeSimpleName } .count() == 1 } diff --git a/inject-kotlin-test/build.gradle b/inject-kotlin-test/build.gradle index c1ba6607294..cf4b5160630 100644 --- a/inject-kotlin-test/build.gradle +++ b/inject-kotlin-test/build.gradle @@ -4,9 +4,7 @@ plugins { } dependencies { - annotationProcessor project(":inject-java") - - api project(":inject-java") + api project(":inject-kotlin") api libs.managed.groovy api(libs.spock) { exclude module:'groovy-all' @@ -14,16 +12,11 @@ dependencies { if (!JavaVersion.current().isJava9Compatible()) { api files(org.gradle.internal.jvm.Jvm.current().toolsJar) } - - testAnnotationProcessor project(":inject-java") - testCompileOnly project(":inject-groovy") + api(libs.ksp.testing) testImplementation libs.managed.validation testImplementation libs.javax.persistence testImplementation project(":runtime") api libs.blaze.persistence.core - - implementation libs.kotlin.compiler.embeddable - implementation libs.kotlin.annotation.processing.embeddable implementation libs.kotlin.stdlib } diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy index 29dee21db30..9b14f0c32b6 100644 --- a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/AbstractKotlinCompilerSpec.groovy @@ -15,27 +15,26 @@ */ package io.micronaut.annotation.processing.test -import io.micronaut.aop.internal.InterceptorRegistryBean + import io.micronaut.context.ApplicationContext -import io.micronaut.context.DefaultApplicationContext import io.micronaut.context.Qualifier -import io.micronaut.context.event.ApplicationEventPublisherFactory +import io.micronaut.core.annotation.Experimental +import io.micronaut.core.annotation.NonNull import io.micronaut.core.beans.BeanIntrospection -import io.micronaut.core.io.scan.ClassPathResourceLoader import io.micronaut.core.naming.NameUtils import io.micronaut.inject.BeanDefinition -import io.micronaut.inject.BeanDefinitionReference -import io.micronaut.inject.provider.BeanProviderDefinition -import io.micronaut.inject.provider.JakartaProviderBeanDefinition +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.WildcardElement import org.intellij.lang.annotations.Language import spock.lang.Specification +import java.util.function.Consumer import java.util.stream.Collectors class AbstractKotlinCompilerSpec extends Specification { protected ClassLoader buildClassLoader(String className, @Language("kotlin") String cls) { - def result = KotlinCompileHelper.INSTANCE.run(className, cls) - result.classLoader + KotlinCompiler.buildClassLoader(className, cls) } /** @@ -58,40 +57,105 @@ class AbstractKotlinCompilerSpec extends Specification { * @return the introspection if it is correct */ protected ApplicationContext buildContext(String className, @Language("kotlin") String cls, boolean includeAllBeans = false) { - def result = KotlinCompileHelper.INSTANCE.run(className, cls) - ClassLoader classLoader = result.classLoader + KotlinCompiler.buildContext(cls, includeAllBeans) + } - return new DefaultApplicationContext(ClassPathResourceLoader.defaultLoader(classLoader), "test") { - @Override - protected List resolveBeanDefinitionReferences() { - // we want only the definitions we just compiled - def stream = result.fileNames.stream() - .filter(s -> s.endsWith('$Definition$Reference.class')) - .map(n -> classLoader.loadClass(n.substring(0, n.size() - 6).replace('/', '.')).newInstance()) - return stream.collect(Collectors.toList()) + (includeAllBeans ? super.resolveBeanDefinitionReferences() : [ - new InterceptorRegistryBean(), - new BeanProviderDefinition(), - new JakartaProviderBeanDefinition(), - new ApplicationEventPublisherFactory<>() - ]) + /** + * Build and return a {@link io.micronaut.core.beans.BeanIntrospection} for the given class name and class data. + * + * @return the introspection if it is correct + */ + protected ApplicationContext buildContext(@Language("kotlin") String cls, boolean includeAllBeans = false) { + KotlinCompiler.buildContext(cls, includeAllBeans) + } + + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + ClassElement buildClassElement(String className, @Language("kotlin") String cls) { + List elements = [] + KotlinCompiler.compile(className, cls, { + elements.add(it) + }) + return elements.find { it.name == className } + } + + /** + * Builds a class element for the given source code. + * @param cls The source + * @return The class element + */ + boolean buildClassElement(String className, @Language("kotlin") String cls, @NonNull Consumer processor) { + boolean invoked = false + KotlinCompiler.compile(className, cls) { + if (it.name == className) { + processor.accept(it) + invoked = true } - }.start() + } + return true } Object getBean(ApplicationContext context, String className, Qualifier qualifier = null) { context.getBean(context.classLoader.loadClass(className), qualifier) } + /** + * Gets a bean definition from the context for the given class name + * @param context The context + * @param className The class name + * @return The bean instance + */ + BeanDefinition getBeanDefinition(ApplicationContext context, String className, Qualifier qualifier = null) { + context.getBeanDefinition(context.classLoader.loadClass(className), qualifier) + } + protected BeanDefinition buildBeanDefinition(String className, @Language("kotlin") String cls) { - def beanDefName= '$' + NameUtils.getSimpleName(className) + '$Definition' - def packageName = NameUtils.getPackageName(className) - String beanFullName = "${packageName}.${beanDefName}" + KotlinCompiler.buildBeanDefinition(className, cls) + } - ClassLoader classLoader = buildClassLoader(className, cls) - try { - return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance() - } catch (ClassNotFoundException e) { - return null + /** + * Create a rough source signature of the given ClassElement, using {@link io.micronaut.inject.ast.ClassElement#getBoundGenericTypes()}. + * Can be used to test that {@link io.micronaut.inject.ast.ClassElement#getBoundGenericTypes()} returns the right types in the right + * context. + * + * @param classElement The class element to reconstruct + * @param typeVarsAsDeclarations Whether type variables should be represented as declarations + * @return a String representing the type signature. + */ + @Experimental + protected static String reconstructTypeSignature(ClassElement classElement, boolean typeVarsAsDeclarations = false) { + if (classElement.isArray()) { + return "Array<" + reconstructTypeSignature(classElement.fromArray()) + ">" + } else if (classElement.isGenericPlaceholder()) { + def freeVar = (GenericPlaceholderElement) classElement + def name = freeVar.variableName + if (typeVarsAsDeclarations) { + def bounds = freeVar.bounds + if (reconstructTypeSignature(bounds[0]) != 'Object') { + name += bounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " out ", "")) + } + } + return name + } else if (classElement.isWildcard()) { + def we = (WildcardElement) classElement + if (!we.lowerBounds.isEmpty()) { + return we.lowerBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" | ", "in ", "")) + } else if (we.upperBounds.size() == 1 && reconstructTypeSignature(we.upperBounds.get(0)) == "Object") { + return "*" + } else { + return we.upperBounds.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "out ", "")) + } + } else { + def boundTypeArguments = classElement.getBoundGenericTypes() + if (boundTypeArguments.isEmpty()) { + return classElement.getSimpleName() + } else { + return classElement.getSimpleName() + + boundTypeArguments.stream().map(AbstractKotlinCompilerSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">")) + } } } -} \ No newline at end of file +} diff --git a/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java new file mode 100644 index 00000000000..17de74126bd --- /dev/null +++ b/inject-kotlin-test/src/main/groovy/io/micronaut/annotation/processing/test/KotlinCompiler.java @@ -0,0 +1,327 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing.test; + +import com.google.devtools.ksp.processing.SymbolProcessor; +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment; +import com.google.devtools.ksp.symbol.KSClassDeclaration; +import com.tschuchort.compiletesting.KotlinCompilation; +import com.tschuchort.compiletesting.KspKt; +import com.tschuchort.compiletesting.SourceFile; +import io.micronaut.aop.internal.InterceptorRegistryBean; +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.ApplicationContextConfiguration; +import io.micronaut.context.BeanContext; +import io.micronaut.context.DefaultApplicationContext; +import io.micronaut.context.env.Environment; +import io.micronaut.context.event.ApplicationEventPublisherFactory; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.beans.BeanIntrospector; +import io.micronaut.core.naming.NameUtils; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.BeanDefinitionReference; +import io.micronaut.inject.ast.ClassElement; +import io.micronaut.inject.provider.BeanProviderDefinition; +import io.micronaut.inject.writer.BeanDefinitionReferenceWriter; +import io.micronaut.inject.writer.BeanDefinitionVisitor; +import io.micronaut.inject.writer.BeanDefinitionWriter; +import io.micronaut.kotlin.processing.beans.BeanDefinitionProcessorProvider; +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext; +import io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessor; +import io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessorProvider; +import kotlin.Pair; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class KotlinCompiler { + private static final KotlinCompilation KOTLIN_COMPILATION = new KotlinCompilation(); + private static final KotlinCompilation KSP_COMPILATION = new KotlinCompilation(); + + static { + + KOTLIN_COMPILATION.setJvmDefault("all"); + KOTLIN_COMPILATION.setInheritClassPath(true); + + KSP_COMPILATION.setJavacArguments(Collections.singletonList("-Xopt-in=kotlin.RequiresOptIn")); + KSP_COMPILATION.setInheritClassPath(true); + KSP_COMPILATION.setClasspaths(Arrays.asList( + new File(KSP_COMPILATION.getWorkingDir(), "ksp/classes"), + new File(KSP_COMPILATION.getWorkingDir(), "ksp/sources/resources"), + KOTLIN_COMPILATION.getClassesDir())); + } + + public static URLClassLoader buildClassLoader(String name, @Language("kotlin") String clazz) { + Pair, Pair> resultPair = compile(name, clazz, classElement -> { + }); + return toClassLoader(resultPair); + } + + @NotNull + private static URLClassLoader toClassLoader(Pair, Pair> resultPair) { + try { + Pair sourcesCompilation = resultPair.component1(); + Pair kspCompilation = resultPair.component2(); + + KotlinCompilation.Result sourcesCompileResult = sourcesCompilation.component2(); + KotlinCompilation.Result kspCompileResult = kspCompilation.component2(); + List classpath = new ArrayList<>(); + classpath.add(sourcesCompileResult.getOutputDirectory().toURI().toURL()); + classpath.add(kspCompileResult.getOutputDirectory().toURI().toURL()); + classpath.addAll(kspCompilation.component1().getClasspaths().stream().flatMap(f -> { + try { + return Stream.of(f.toURI().toURL()); + } catch (MalformedURLException e) { + return Stream.empty(); + } + }).toList()); + classpath.addAll(sourcesCompilation.component1().getClasspaths().stream().flatMap(f -> { + try { + return Stream.of(f.toURI().toURL()); + } catch (MalformedURLException e) { + return Stream.empty(); + } + }).toList()); + + return new URLClassLoader( + classpath.toArray(URL[]::new), + KotlinCompiler.class.getClassLoader() + ); + } catch (MalformedURLException e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + + public static Pair, Pair> compile(String name, @Language("kotlin") String clazz, Consumer classElements) { + try { + Files.deleteIfExists(KOTLIN_COMPILATION.getWorkingDir().toPath()); + } catch (IOException e) { + // ignore + } + KOTLIN_COMPILATION.setSources(Collections.singletonList(SourceFile.Companion.kotlin(name + ".kt", clazz, true))); + KotlinCompilation.Result result = KOTLIN_COMPILATION.compile(); + if (result.getExitCode() != KotlinCompilation.ExitCode.OK) { + throw new RuntimeException(result.getMessages()); + } + + KSP_COMPILATION.setSources(KOTLIN_COMPILATION.getSources()); + ClassElementTypeElementSymbolProcessorProvider classElementTypeElementSymbolProcessorProvider = new ClassElementTypeElementSymbolProcessorProvider(classElements); + KspKt.setSymbolProcessorProviders(KSP_COMPILATION, Arrays.asList(classElementTypeElementSymbolProcessorProvider, new BeanDefinitionProcessorProvider())); + KotlinCompilation.Result kspResult = KSP_COMPILATION.compile(); + if (kspResult.getExitCode() != KotlinCompilation.ExitCode.OK) { + throw new RuntimeException(kspResult.getMessages()); + } + + return new Pair<>(new Pair<>(KOTLIN_COMPILATION, result), new Pair<>(KSP_COMPILATION, kspResult)); + } + + public static BeanIntrospection buildBeanIntrospection(String name, @Language("kotlin") String clazz) { + final URLClassLoader classLoader = buildClassLoader(name, clazz); + try { + return BeanIntrospector.forClassLoader(classLoader).findIntrospection(classLoader.loadClass(name)).orElse(null); + } catch (ClassNotFoundException e) { + return null; + } + } + + public static BeanDefinition buildBeanDefinition(String name, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return buildBeanDefinition(NameUtils.getPackageName(name), + NameUtils.getSimpleName(name), + clazz); + } + + public static BeanDefinition buildBeanDefinition(String packageName, String simpleName, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final URLClassLoader classLoader = buildClassLoader(packageName + "." + simpleName, clazz); + String beanDefName = (simpleName.startsWith("$") ? "" : '$') + simpleName + BeanDefinitionWriter.CLASS_SUFFIX; + String beanFullName = packageName + "." + beanDefName; + return (BeanDefinition) loadDefinition(classLoader, beanFullName); + } + + public static BeanDefinitionReference buildBeanDefinitionReference(String name, @Language("kotlin") String clazz) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(name, clazz, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionReferenceWriter.REF_SUFFIX); + } + + public static BeanDefinition buildInterceptedBeanDefinition(String className, @Language("kotlin") String cls) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(className, cls, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX); + } + + public static BeanDefinitionReference buildInterceptedBeanDefinitionReference(String className, @Language("kotlin") String cls) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return loadReference(className, cls, BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionReferenceWriter.REF_SUFFIX); + } + + private static T loadReference(String className, + @Language("kotlin") String cls, + String suffix + ) throws InstantiationException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + String simpleName = NameUtils.getSimpleName(className); + String beanDefName = (simpleName.startsWith("$") ? "" : '$') + simpleName + suffix; + String packageName = NameUtils.getPackageName(className); + String beanFullName = packageName + "." + beanDefName; + + ClassLoader classLoader = buildClassLoader(className, cls); + return (T) loadDefinition(classLoader, beanFullName); + } + + public static byte[] getClassBytes(String name, @Language("kotlin") String clazz) throws FileNotFoundException, IOException { + String simpleName = NameUtils.getSimpleName(name); + String className = (simpleName.startsWith("$") ? "" : '$') + simpleName; + String packageName = NameUtils.getPackageName(name); + String fileName = packageName.replace('.', '/') + '/' + className + ".class"; + URLClassLoader classLoader = buildClassLoader(className, clazz); + File file = null; + for (URL url: classLoader.getURLs()) { + file = new File(url.getFile(), fileName); + if (file.exists()) { + break; + } else { + file = null; + } + } + if (file != null) { + try (InputStream is = new FileInputStream(file)) { + ByteArrayOutputStream answer = new ByteArrayOutputStream(); + byte[] byteBuffer = new byte[8192]; + int nbByteRead; + while ((nbByteRead = is.read(byteBuffer)) != -1) { + answer.write(byteBuffer, 0, nbByteRead); + } + return answer.toByteArray(); + } + } + return null; + } + + public static ApplicationContext buildContext(@Language("kotlin") String clazz) { + return buildContext(clazz, false); + } + + public static ApplicationContext + buildContext(@Language("kotlin") String clazz, boolean includeAllBeans) { + return buildContext(clazz, includeAllBeans, Collections.emptyMap()); + } + + @SuppressWarnings("java:S2095") + public static ApplicationContext + buildContext(@Language("kotlin") String clazz, boolean includeAllBeans, Map config) { + Pair, Pair> pair = compile("temp", clazz, classElement -> { + }); + ClassLoader classLoader = toClassLoader(pair); + var builder = ApplicationContext.builder(); + builder.classLoader(classLoader); + builder.environments("test"); + builder.properties(config); + Environment environment = builder.build().getEnvironment(); + return new DefaultApplicationContext((ApplicationContextConfiguration) builder) { + + @Override + public Environment getEnvironment() { + return environment; + } + + @Override + protected List resolveBeanDefinitionReferences() { + List beanDefinitionNames = pair.component2().component1(). + getClasspaths().stream().filter(f -> f.toURI().toString().contains("/ksp/sources/resources")) + .flatMap(dir -> { + File[] files = new File(dir, "META-INF/micronaut/io.micronaut.inject.BeanDefinitionReference").listFiles(); + if (files == null) { + return Stream.empty(); + } + return Stream.of(files).filter(f -> f.isFile()); + }).map(f -> f.getName()).toList(); + + List beanDefinitions = new ArrayList<>(beanDefinitionNames.size()); + for (String name : beanDefinitionNames) { + try { + BeanDefinitionReference br = (BeanDefinitionReference) loadDefinition(classLoader, name); + if (br != null) { + beanDefinitions.add(br); + } + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + } + } + if (includeAllBeans) { + beanDefinitions.addAll(super.resolveBeanDefinitionReferences()); + } else { + beanDefinitions.add(new InterceptorRegistryBean()); + beanDefinitions.add(new BeanProviderDefinition()); + beanDefinitions.add(new ApplicationEventPublisherFactory<>()); + } + return beanDefinitions; + } + }.start(); + } + + public static Object getBean(BeanContext beanContext, String className) throws ClassNotFoundException { + return beanContext.getBean(beanContext.getClassLoader().loadClass(className)); + } + + public static BeanDefinition getBeanDefinition(BeanContext beanContext, String className) throws ClassNotFoundException { + return beanContext.getBeanDefinition(beanContext.getClassLoader().loadClass(className)); + } + + private static Object loadDefinition(ClassLoader classLoader, String name) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + try { + Class c = classLoader.loadClass(name); + Constructor constructor = c.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (ClassNotFoundException e) { + return null; + } + } + + private static class ClassElementTypeElementSymbolProcessorProvider extends TypeElementSymbolProcessorProvider { + Consumer classElements; + + public ClassElementTypeElementSymbolProcessorProvider(Consumer classElements) { + this.classElements = classElements; + } + + @NotNull + @Override + public SymbolProcessor create(@NotNull SymbolProcessorEnvironment environment) { + return new TypeElementSymbolProcessor(environment) { + @NotNull + @Override + public ClassElement newClassElement(@NotNull KotlinVisitorContext visitorContext, @NotNull KSClassDeclaration classDeclaration) { + ClassElement classElement = super.newClassElement(visitorContext, classDeclaration); + classElements.accept(classElement); + return classElement; + } + }; + } + } +} diff --git a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt b/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt deleted file mode 100644 index 2735410a962..00000000000 --- a/inject-kotlin-test/src/main/kotlin/io/micronaut/annotation/processing/test/KotlinCompileHelper.kt +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.annotation.processing.test - -import org.jetbrains.kotlin.base.kapt3.DetectMemoryLeaksMode -import org.jetbrains.kotlin.base.kapt3.KaptOptions -import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation -import org.jetbrains.kotlin.cli.common.messages.MessageCollector -import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace -import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment -import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM -import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot -import org.jetbrains.kotlin.codegen.ClassBuilderMode -import org.jetbrains.kotlin.codegen.DefaultCodegenFactory -import org.jetbrains.kotlin.codegen.KotlinCodegenFacade -import org.jetbrains.kotlin.codegen.OriginCollectingClassBuilderFactory -import org.jetbrains.kotlin.codegen.state.GenerationState -import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory -import org.jetbrains.kotlin.com.intellij.psi.impl.PsiFileFactoryImpl -import org.jetbrains.kotlin.com.intellij.testFramework.LightVirtualFile -import org.jetbrains.kotlin.config.CommonConfigurationKeys -import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.JVMConfigurationKeys -import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.kapt3.AbstractKapt3Extension -import org.jetbrains.kotlin.kapt3.base.LoadedProcessors -import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType -import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor -import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.resolve.AnalyzingUtils -import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension -import java.io.File -import java.io.IOException -import java.net.URL -import java.net.URLClassLoader -import java.nio.charset.StandardCharsets -import java.nio.file.FileVisitResult -import java.nio.file.FileVisitor -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.attribute.BasicFileAttributes -import java.util.* -import javax.annotation.processing.Processor - -object KotlinCompileHelper { - init { - System.setProperty("idea.ignore.disabled.plugins", "true") - System.setProperty("idea.io.use.nio2", "true") - } - - fun run(className: String, code: String): Result { - val tmp = Files.createTempDirectory("KotlinCompileHelper") - try { - return run0(tmp, className, code) - } finally { - // delete tmp dir - Files.walkFileTree(tmp, object : FileVisitor { - override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { - return FileVisitResult.CONTINUE - } - - override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { - Files.delete(file) - return FileVisitResult.CONTINUE - } - - override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult { - throw exc - } - - override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { - Files.delete(dir) - return FileVisitResult.CONTINUE - } - }) - } - } - - private fun run0( - tmp: Path, - className: String, - code: String - ): Result { - // hack around org.jetbrains.kotlin.com.intellij.openapi.util.BuildNumber - System.setProperty("idea.home.path", tmp.toAbsolutePath().toString()) - Files.write(tmp.resolve("build.txt"), "999.SNAPSHOT".toByteArray()) - - val outDir = tmp.resolve("out") - Files.createDirectory(outDir) - val stubsDir = tmp.resolve("stubs") - Files.createDirectory(stubsDir) - - val configuration = CompilerConfiguration() - configuration.put(CommonConfigurationKeys.MODULE_NAME, "test-module") - val messageCollector = object : MessageCollector { - override fun clear() { - } - - override fun hasErrors() = false - - override fun report( - severity: CompilerMessageSeverity, - message: String, - location: CompilerMessageSourceLocation? - ) { - // With Java 17 and Groovy 4.x this breaks inject-kotlin-test:KotlinCompilerTest as it throws an AssertionError for the Note: message - if (severity == CompilerMessageSeverity.ERROR && !message.startsWith("Note:")) { - throw AssertionError("Error reported in processing: $message") - } - } - } - configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) - configuration.put(JVMConfigurationKeys.IR, false) - configuration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, outDir.toFile()) - configuration.put(JVMConfigurationKeys.JDK_HOME, File(System.getProperty("java.home"))) - - val env = - KotlinCoreEnvironment.createForTests({ }, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) - - val cp = getClasspath(KotlinCompileHelper::class.java.classLoader) + getClasspathFromSystemProperty("java.class.path") + getClasspathFromSystemProperty("sun.boot.class.path") - env.updateClasspath(cp.map { - JvmClasspathRoot(it, false) - }) - - val kaptOptions = KaptOptions.Builder() - kaptOptions.projectBaseDir = tmp.toFile() - kaptOptions.sourcesOutputDir = outDir.toFile() - kaptOptions.classesOutputDir = outDir.toFile() - kaptOptions.stubsOutputDir = stubsDir.toFile() - kaptOptions.detectMemoryLeaks = DetectMemoryLeaksMode.NONE - kaptOptions.compileClasspath.addAll(cp) - - class KaptExtension : AbstractKapt3Extension( - kaptOptions.build(), - MessageCollectorBackedKaptLogger( - isVerbose = false, - isInfoAsWarnings = true, - messageCollector = messageCollector - ), - configuration - ) { - override fun loadProcessors() = LoadedProcessors( - ServiceLoader.load(Processor::class.java) - .map { IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL, logger) }, - KotlinCompileHelper::class.java.classLoader - ) - } - - AnalysisHandlerExtension.registerExtension(env.project, KaptExtension()) - - val classBuilderFactory = OriginCollectingClassBuilderFactory(ClassBuilderMode.FULL) - val vFile = - LightVirtualFile(className.substring(className.lastIndexOf('.') + 1) + ".kt", KotlinLanguage.INSTANCE, code) - vFile.charset = StandardCharsets.UTF_8 - val psiFileFactory = PsiFileFactory.getInstance(env.project) as PsiFileFactoryImpl - val ktFile = psiFileFactory.trySetupPsiForFile(vFile, KotlinLanguage.INSTANCE, true, false) as KtFile - - val trace = CliBindingTrace() - val analysisResult = TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( - env.project, - listOf(ktFile), - trace, - configuration, - env::createPackagePartProvider - ) - if (analysisResult.isError()) { - throw analysisResult.error - } - AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext) - - val genState = GenerationState.Builder( - env.project, - classBuilderFactory, - analysisResult.moduleDescriptor, - trace.bindingContext, - listOf(ktFile), - configuration - ).codegenFactory(DefaultCodegenFactory).isIrBackend(false).build() - KotlinCodegenFacade.compileCorrectFiles(genState) - - AnalyzingUtils.throwExceptionOnErrors(genState.collectedExtraJvmDiagnostics) - - val cl = MemoryClassLoader(KotlinCompileHelper::class.java.classLoader) - for (outputFile in genState.factory.currentOutput) { - cl.files[outputFile.relativePath] = outputFile.asByteArray() - } - Files.walk(outDir).filter(Files::isRegularFile).forEach { p -> - cl.files[outDir.relativize(p).toString()] = Files.readAllBytes(p) - } - - return Result(cl, cl.files.keys) - } - - private fun getClasspath(cl: ClassLoader): List = - getClasspathSingle(cl) + (cl.parent?.let { getClasspath(it) } ?: emptyList()) - - private fun getClasspathSingle(cl: ClassLoader): List { - if (cl is URLClassLoader) { - return cl.urLs.map { File(it.toURI()) } - } - // ideally, we'd look at the system class loaders too (jdk.internal.loader.BuiltinClassLoader), but they're - // protected from reflection in newer JDKs. So, we fall back to using the java.class.path system property in the - // code above, and ignore those class loaders here. - - return emptyList() - } - - private fun getClasspathFromSystemProperty(prop: String): List { - val value = System.getProperty(prop) ?: return emptyList() - return value.split(System.getProperty("path.separator")).map { File(it) } - } - - data class Result( - val classLoader: ClassLoader, - val fileNames: Collection - ) - - private class MemoryClassLoader(parent: ClassLoader?) : ClassLoader(parent) { - val files = mutableMapOf() - - override fun findResource(name: String): URL? { - val resource = files[name] ?: return null - return URL("data:text/plain;base64," + Base64.getUrlEncoder().encodeToString(resource)) - } - - override fun findClass(name: String): Class<*> { - val path = name.replace('.', '/') + ".class" - val file = files[path] ?: throw ClassNotFoundException(name) - return defineClass(name, file, 0, file.size) - } - } -} diff --git a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy index ed249f9cc34..6ca9b2bbefe 100644 --- a/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy +++ b/inject-kotlin-test/src/test/groovy/io/micronaut/annotation/processing/test/KotlinCompilerTest.groovy @@ -28,7 +28,7 @@ import io.micronaut.core.annotation.Introspected @Introspected class Test { - val a: String + val a: String = "test" } ''') expect: @@ -87,6 +87,7 @@ class Test( var name: String, var getSurname: String, var isDeleted: Boolean, + var isOptional: Boolean?, val isImportant: Boolean, var corrected: Boolean, val upgraded: Boolean, @@ -105,6 +106,6 @@ class Test( } ''') expect: - introspection.propertyNames.toList() == ['id', 'name', 'getSurname', 'isDeleted', 'isImportant', 'corrected', 'upgraded', 'isMyBool', 'isMyBool2', 'myBool3', 'myBool4', 'myBool5'] + introspection.propertyNames as Set == ['id', 'name', 'getSurname', 'isDeleted', 'isOptional', 'isImportant', 'corrected', 'upgraded', 'isMyBool', 'isMyBool2', 'myBool3', 'myBool4', 'myBool5'] as Set } } diff --git a/inject-kotlin/build.gradle b/inject-kotlin/build.gradle new file mode 100644 index 00000000000..2cf77e47a30 --- /dev/null +++ b/inject-kotlin/build.gradle @@ -0,0 +1,65 @@ +plugins { + id "io.micronaut.build.internal.convention-library" + id "org.jetbrains.kotlin.jvm" + id "com.google.devtools.ksp" version "1.8.0-Beta-1.0.8" + +} + +micronautBuild { + core { + usesMicronautTest() + } +} + +dependencies { + api project(":core-processor") + + implementation(libs.managed.ksp.api) + if (!JavaVersion.current().isJava9Compatible()) { + api files(org.gradle.internal.jvm.Jvm.current().toolsJar) + } + kspTest(project) + testImplementation project(":jackson-databind") + testImplementation project(":inject-kotlin-test") + testImplementation libs.kotlin.stdlib + testImplementation project(':http-client') + testImplementation libs.managed.jackson.annotations + testImplementation libs.managed.validation + testImplementation libs.managed.reactor + testImplementation libs.hibernate + testImplementation project(":validation") + testImplementation libs.javax.persistence + testImplementation project(":runtime") + testImplementation(libs.neo4j.bolt) + testImplementation libs.kotlinx.coroutines.core + testImplementation libs.kotlinx.coroutines.jdk8 + testImplementation libs.kotlinx.coroutines.rx2 + +} + +afterEvaluate { + sourcesJar { + from "$projectDir/src/main/kotlin" + } +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ['-Xjvm-default=all'] + } +} + +tasks.named("compileTestGroovy") { + classpath += files(tasks.compileTestKotlin) +} + +tasks.named("test") { + classpath += files(tasks.compileTestKotlin) +// testLogging { +// showStandardStreams = true +// } + maxHeapSize("1G") + forkEvery = 40 + maxParallelForks = 2 +} + diff --git a/inject-kotlin/gradle.properties b/inject-kotlin/gradle.properties new file mode 100644 index 00000000000..48335b1e143 --- /dev/null +++ b/inject-kotlin/gradle.properties @@ -0,0 +1 @@ +ksp.incremental = false diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt new file mode 100644 index 00000000000..4938996dd6e --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/KotlinOutputVisitor.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing + +import com.google.devtools.ksp.containingFile +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSFile +import io.micronaut.inject.ast.Element +import io.micronaut.inject.writer.AbstractClassWriterOutputVisitor +import io.micronaut.inject.writer.GeneratedFile +import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.io.File +import java.io.OutputStream +import java.util.* + +class KotlinOutputVisitor(private val environment: SymbolProcessorEnvironment): AbstractClassWriterOutputVisitor(false) { + + override fun visitClass(classname: String, vararg originatingElements: Element): OutputStream { + return environment.codeGenerator.createNewFile( + getNativeElements(originatingElements), + classname.substringBeforeLast('.'), + classname.substringAfterLast('.'), + "class") + } + + override fun visitServiceDescriptor(type: String, classname: String, originatingElement: Element) { + environment.codeGenerator.createNewFile( + getNativeElements(arrayOf(originatingElement)), + "META-INF.micronaut", + "${type}${File.separator}${classname}", + "").use { + it.bufferedWriter().write("") + } + } + + override fun visitMetaInfFile(path: String, vararg originatingElements: Element): Optional { + val elements = path.split(File.separator).toMutableList() + elements.add(0, "META-INF") + val file = elements.removeAt(elements.size - 1) + + val stream = environment.codeGenerator.createNewFile( + getNativeElements(originatingElements), + elements.joinToString("."), + file.substringBeforeLast('.'), + file.substringAfterLast('.')) + + return Optional.of(KotlinVisitorContext.KspGeneratedFile(stream, elements.joinToString(File.separator))) + } + + override fun visitGeneratedFile(path: String?): Optional { + TODO("Not yet implemented") + } + + private fun getNativeElements(originatingElements: Array): Dependencies { + val originatingFiles: MutableList = ArrayList(originatingElements.size) + for (originatingElement in originatingElements) { + if (originatingElement is AbstractKotlinElement<*>) { + val nativeType = originatingElement.nativeType.unwrap().containingFile + if (nativeType is KSFile) { + originatingFiles.add(nativeType) + } + } + } + return Dependencies(aggregating = false, sources = originatingFiles.toTypedArray()) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt new file mode 100644 index 00000000000..3a51a2de0b5 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinAnnotationMetadataBuilder.kt @@ -0,0 +1,591 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotation + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getDeclaredProperties +import com.google.devtools.ksp.isConstructor +import com.google.devtools.ksp.isDefault +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.context.annotation.Property +import io.micronaut.core.annotation.AnnotationClassValue +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.core.util.ArrayUtils +import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap +import io.micronaut.core.value.OptionalValues +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.annotation.MutableAnnotationMetadata +import io.micronaut.inject.visitor.VisitorContext +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.getClassDeclaration +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.lang.annotation.Inherited +import java.lang.annotation.RetentionPolicy +import java.lang.reflect.Method +import java.util.* + +class KotlinAnnotationMetadataBuilder(private val symbolProcessorEnvironment: SymbolProcessorEnvironment, + private val resolver: Resolver, + private val visitorContext: KotlinVisitorContext): AbstractAnnotationMetadataBuilder() { + + private val annotationDefaultsCache: ConcurrentLinkedHashMap> = + ConcurrentLinkedHashMap.Builder>().maximumWeightedCapacity(200).build() + + companion object { + private fun getTypeForAnnotation(annotationMirror: KSAnnotation, visitorContext: KotlinVisitorContext): KSClassDeclaration { + return annotationMirror.annotationType.resolve().declaration.getClassDeclaration(visitorContext) + } + fun getAnnotationTypeName(annotationMirror: KSAnnotation, visitorContext: KotlinVisitorContext): String { + val type = getTypeForAnnotation(annotationMirror, visitorContext) + return if (type.qualifiedName != null) { + type.qualifiedName!!.asString() + } else { + println("Failed to get the qualified name of ${annotationMirror.shortName.asString()} annotation") + annotationMirror.shortName.asString() + } + } + } + + override fun getTypeForAnnotation(annotationMirror: KSAnnotation): KSClassDeclaration { + return Companion.getTypeForAnnotation(annotationMirror, visitorContext) + } + + override fun hasAnnotation(element: KSAnnotated, annotation: Class): Boolean { + return hasAnnotation(element, annotation.name) + } + + override fun hasAnnotation(element: KSAnnotated, annotation: String): Boolean { + return element.annotations.map { + it.annotationType.resolve().declaration.qualifiedName + }.any { + it?.asString() == annotation + } + } + + override fun hasAnnotations(element: KSAnnotated): Boolean { + return if (element is KSPropertyDeclaration) { + element.annotations.iterator().hasNext() || + element.getter?.annotations?.iterator()?.hasNext() ?: false + } else { + element.annotations.iterator().hasNext() + } + } + + override fun getAnnotationTypeName(annotationMirror: KSAnnotation): String { + return Companion.getAnnotationTypeName(annotationMirror, visitorContext) + } + + override fun getElementName(element: KSAnnotated): String { + if (element is KSDeclaration) { + return if (element is KSClassDeclaration) { + element.qualifiedName!!.asString() + } else { + element.simpleName.asString() + } + } + TODO("Not yet implemented") + } + + override fun getAnnotationsForType(element: KSAnnotated): MutableList { + val annotationMirrors : MutableList = mutableListOf() + + if (element is KSValueParameter) { + // fuse annotations for setter and property + val parent = element.parent + if (parent is KSPropertySetter) { + val property = parent.parent + if (property is KSPropertyDeclaration) { + annotationMirrors.addAll(property.annotations) + } + annotationMirrors.addAll(parent.annotations) + } + annotationMirrors.addAll(element.annotations) + } else if (element is KSPropertyGetter || element is KSPropertySetter) { + val property = element.parent + if (property is KSPropertyDeclaration) { + annotationMirrors.addAll(property.annotations) + } + annotationMirrors.addAll(element.annotations) + } else if (element is KSPropertyDeclaration) { + val parent : KSClassDeclaration? = findClassDeclaration(element) + if (parent is KSClassDeclaration && parent.classKind == ClassKind.ANNOTATION_CLASS) { + annotationMirrors.addAll(element.annotations) + val getter = element.getter + if (getter != null) { + annotationMirrors.addAll(getter.annotations) + } + } else { + annotationMirrors.addAll(element.annotations) + } + } else { + annotationMirrors.addAll(element.annotations) + } + val expanded : MutableList = mutableListOf() + for (ann in annotationMirrors) { + val annotationName = getAnnotationTypeName(ann) + var repeateable = false + var hasOtherMembers = false + for (arg in ann.arguments) { + if ("value" == arg.name?.asString()) { + val value = arg.value + if (value is Iterable<*>) { + for (nested in value) { + if (nested is KSAnnotation) { + val repeatableName = getRepeatableName(nested) + if (repeatableName != null && repeatableName == annotationName) { + expanded.add(nested) + repeateable = true + } + } + } + } + } else { + hasOtherMembers = true + } + } + + if (!repeateable || hasOtherMembers) { + expanded.add(ann) + } + } + return expanded + } + + private fun findClassDeclaration(element: KSPropertyDeclaration): KSClassDeclaration? { + var parent = element.parent + while (parent != null) { + if (parent is KSClassDeclaration) { + return parent + } + parent = parent.parent + } + return null + } + + override fun postProcess(annotationMetadata: MutableAnnotationMetadata, element: KSAnnotated) { + if (element is KSValueParameter && element.type.resolve().isMarkedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } else if (element is KSFunctionDeclaration) { + val markedNullable = element.returnType?.resolve()?.isMarkedNullable + if (markedNullable != null && markedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } + } else if (element is KSPropertyDeclaration) { + val markedNullable = element.type.resolve().isMarkedNullable + if (markedNullable) { + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.NULLABLE, emptyMap()) + } + } else if (element is KSPropertySetter) { + if (!annotationMetadata.hasAnnotation(JvmField::class.java) && (annotationMetadata.hasStereotype(AnnotationUtil.QUALIFIER) || annotationMetadata.hasAnnotation(Property::class.java))) { + // implicitly inject + annotationMetadata.addDeclaredAnnotation(AnnotationUtil.INJECT, emptyMap()) + } + } + } + + override fun buildHierarchy( + element: KSAnnotated, + inheritTypeAnnotations: Boolean, + declaredOnly: Boolean + ): MutableList { + if (declaredOnly) { + return mutableListOf(element) + } + if (element is KSClassDeclaration) { + val hierarchy = mutableListOf() + hierarchy.add(element) + if (element.classKind == ClassKind.ANNOTATION_CLASS) { + return hierarchy + } + populateTypeHierarchy(element, hierarchy) + hierarchy.reverse() + return hierarchy + } else if (element is KSFunctionDeclaration) { + return if (element.isConstructor()) { + mutableListOf(element) + } else { + val hierarchy = mutableListOf(element) + var overidden = element.findOverridee() + while (overidden != null) { + hierarchy.add(overidden) + overidden = (overidden as KSFunctionDeclaration).findOverridee() + } + hierarchy + } + } else { + return mutableListOf(element) + } + } + + override fun readAnnotationRawValues( + originatingElement: KSAnnotated, + annotationName: String, + member: KSAnnotated, + memberName: String, + annotationValue: Any, + annotationValues: MutableMap + ) { + if (!annotationValues.containsKey(memberName)) { + val value = readAnnotationValue(originatingElement, member, memberName, annotationValue) + if (value != null) { + validateAnnotationValue(originatingElement, annotationName, member, memberName, value) + annotationValues[memberName] = value + } + } + } + + override fun isValidationRequired(member: KSAnnotated?): Boolean { + if (member != null) { + return member.annotations.any { + val name = it.annotationType.resolve().declaration.qualifiedName?.asString() + if (name != null) { + return name.startsWith("javax.validation") || name.startsWith("jakarta.validation") + } else { + return false + } + } + } + return false + } + + override fun addError(originatingElement: KSAnnotated, error: String) { + symbolProcessorEnvironment.logger.error(error, originatingElement) + } + + override fun addWarning(originatingElement: KSAnnotated, warning: String) { + symbolProcessorEnvironment.logger.warn(warning, originatingElement) + } + + override fun readAnnotationValue( + originatingElement: KSAnnotated, + member: KSAnnotated, + memberName: String, + annotationValue: Any + ): Any? { + return when (annotationValue) { + is Collection<*> -> { + toArray(annotationValue, originatingElement) + } + is Array<*> -> { + toArray(annotationValue.toList(), originatingElement) + } + else -> readAnnotationValue(originatingElement, annotationValue) + } + } + + private fun toArray( + annotationValue: Collection<*>, + originatingElement: KSAnnotated + ): Array? { + var valueType = Any::class.java + val collection = annotationValue.map { + val v = readAnnotationValue(originatingElement, it) + if (v != null) { + valueType = v.javaClass + } + v + } + return ArrayUtils.toArray(collection, valueType) + } + + override fun readAnnotationDefaultValues(annotationMirror: KSAnnotation): MutableMap { + val defaultArguments = annotationMirror.defaultArguments + val declaration = annotationMirror.annotationType.getClassDeclaration(visitorContext) + val allProperties = declaration.getAllProperties() + val map = mutableMapOf() + for (defaultArgument in defaultArguments) { + val name = defaultArgument.name + val value = defaultArgument.value + if (name != null && value != null) { + val dec = allProperties.find { it.simpleName.asString() == name.asString() } + if (dec != null) { + map[dec] = value + } + } + } + return map + } + + override fun readAnnotationDefaultValues( + annotationName: String, + annotationType: KSAnnotated + ): MutableMap { + // issue getting default values for an annotation here + // TODO: awful hack due to https://github.com/google/ksp/issues/642 and not being able to access annotation defaults for a type + val classDeclaration = annotationType.getClassDeclaration(visitorContext) + val qualifiedName = classDeclaration.qualifiedName + return if (qualifiedName != null) { + annotationDefaultsCache.computeIfAbsent(qualifiedName.asString()) { + readDefaultValuesReflectively( + classDeclaration, + annotationType, + "getDescriptor", + "getJClass", + "getMethods" + ) + } + } else { + mutableMapOf() + } + } + + private fun readDefaultValuesReflectively(classDeclaration : KSClassDeclaration, annotationType: KSAnnotated, vararg path : String): MutableMap { + var o: Any? = findValueReflectively(annotationType, *path) + val declaredProperties = classDeclaration.getDeclaredProperties() + val map = mutableMapOf() + if (o != null) { + if (o is Iterable<*>) { + for (m in o) { + if (m != null) { + val name = findValueReflectively(m, "getName") + // currently only handles JavaLiteralAnnotationArgument but probably should handle others + val value = + findValueReflectively(m, "getAnnotationParameterDefaultValue", "getValue") + if (value != null && name != null) { + val ksPropertyDeclaration = declaredProperties.find { it.simpleName.asString() == name.toString() } + if (ksPropertyDeclaration != null) { + map[ksPropertyDeclaration] = value + } + } + } + } + } + } + return map + } + + private fun findValueReflectively( + root: Any, + vararg path : String + ): Any? { + var m: Method? + var o: Any = root + for (p in path) { + m = ReflectionUtils.findMethod(o.javaClass, p).orElse(null) + if (m == null) { + return null + } else { + try { + o = m.invoke(o) + if (o == null) { + return null + } + } catch (e: Exception) { + return null + } + } + } + return o + } + + override fun readAnnotationRawValues(annotationMirror: KSAnnotation): MutableMap { + val map = mutableMapOf() + val declaration = annotationMirror.annotationType.resolve().declaration.getClassDeclaration(visitorContext) + declaration.getAllProperties().forEach { prop -> + val argument = annotationMirror.arguments.find { it.name == prop.simpleName } + if (argument?.value != null && !argument.isDefault()) { + val value = argument.value!! + map[prop] = value + } + } + return map + } + + override fun getAnnotationValues( + originatingElement: KSAnnotated, + member: KSAnnotated, + annotationType: Class<*> + ): OptionalValues<*> { + val annotationMirrors: MutableList = (member as KSPropertyDeclaration).getter!!.annotations.toMutableList() + annotationMirrors.addAll(member.annotations.toList()) + val annotationName = annotationType.name + for (annotationMirror in annotationMirrors) { + if (annotationMirror.annotationType.resolve().declaration.qualifiedName?.asString() == annotationName) { + val values: Map = readAnnotationRawValues(annotationMirror) + val converted: MutableMap = mutableMapOf() + for ((key, value1) in values) { + val value = value1!! + readAnnotationRawValues( + originatingElement, + annotationName, + member, + key.simpleName.asString(), + value, + converted + ) + } + return OptionalValues.of(Any::class.java, converted) + } + } + return OptionalValues.empty() + } + + override fun getAnnotationMemberName(member: KSAnnotated): String { + if (member is KSDeclaration) { + return member.simpleName.asString() + } + TODO("Not yet implemented") + } + + override fun getRepeatableName(annotationMirror: KSAnnotation): String? { + return getRepeatableNameForType(annotationMirror.annotationType) + } + + override fun getRepeatableNameForType(annotationType: KSAnnotated): String? { + val name = java.lang.annotation.Repeatable::class.java.name + val repeatable = annotationType.getClassDeclaration(visitorContext).annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == name + } + if (repeatable != null) { + val value = repeatable.arguments.find { it.name?.asString() == "value" }?.value + if (value != null) { + val declaration = (value as KSType).declaration.getClassDeclaration(visitorContext) + return declaration.getBinaryName(resolver, visitorContext) + } + } + return null + } + + override fun getAnnotationMirror(annotationName: String): Optional { + return Optional.ofNullable(resolver.getClassDeclarationByName(annotationName)) + } + + override fun getAnnotationMember(originatingElement: KSAnnotated, member: CharSequence): KSAnnotated? { + if (originatingElement is KSAnnotation) { + return originatingElement.arguments.find { it.name == member } + } + return null + } + + override fun createVisitorContext(): VisitorContext { + return KotlinVisitorContext(symbolProcessorEnvironment, resolver) + } + + override fun getRetentionPolicy(annotation: KSAnnotated): RetentionPolicy { + var retention = annotation.annotations.find { + getAnnotationTypeName(it) == java.lang.annotation.Retention::class.java.name + } + if (retention != null) { + val value = retention.arguments.find { it.name?.asString() == "value" }?.value + if (value is KSType) { + return toRetentionPolicy(value) + } + } else { + retention = annotation.annotations.find { + getAnnotationTypeName(it) == Retention::class.java.name + } + if (retention != null) { + val value = retention.arguments.find { it.name?.asString() == "value" }?.value + if (value is KSType) { + return toJavaRetentionPolicy(value) + } + } + } + return RetentionPolicy.RUNTIME + } + + private fun toRetentionPolicy(value: KSType) = + RetentionPolicy.valueOf(value.declaration.qualifiedName!!.getShortName()) + + private fun toJavaRetentionPolicy(value: KSType) = + when (AnnotationRetention.valueOf(value.declaration.qualifiedName!!.getShortName())) { + AnnotationRetention.RUNTIME -> { + RetentionPolicy.RUNTIME + } + + AnnotationRetention.SOURCE -> { + RetentionPolicy.SOURCE + } + + AnnotationRetention.BINARY -> { + RetentionPolicy.CLASS + } + } + + override fun isInheritedAnnotation(annotationMirror: KSAnnotation): Boolean { + return annotationMirror.annotationType.resolve().declaration.annotations.any { + it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName + } + } + + override fun isInheritedAnnotationType(annotationType: KSAnnotated): Boolean { + return annotationType.annotations.any { + it.annotationType.resolve().declaration.qualifiedName?.asString() == Inherited::class.qualifiedName + } + } + + private fun populateTypeHierarchy(element: KSClassDeclaration, hierarchy: MutableList) { + element.superTypes.forEach { + val t = it.resolve() + if (t != resolver.builtIns.anyType) { + val declaration = t.declaration + if (!hierarchy.contains(declaration)) { + hierarchy.add(declaration) + populateTypeHierarchy(declaration.getClassDeclaration(visitorContext), hierarchy) + } + } + } + } + + private fun readAnnotationValue(originatingElement: KSAnnotated, value: Any?): Any? { + if (value == null) { + return null + } + if (value is KSType) { + val declaration = value.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.ENUM_ENTRY) { + return declaration.qualifiedName?.getShortName() + } + if (declaration.classKind == ClassKind.CLASS || + declaration.classKind == ClassKind.INTERFACE || + declaration.classKind == ClassKind.ANNOTATION_CLASS) { + return AnnotationClassValue(declaration.getBinaryName(resolver, visitorContext)) + } + } + } + if (value is KSAnnotation) { + return readNestedAnnotationValue(originatingElement, value) + } + return value + } + + override fun getAnnotationMembers(annotationType: String): MutableMap { + val annotationMirror = getAnnotationMirror(annotationType) + val members = mutableMapOf() + if (annotationMirror.isPresent) { + (annotationMirror.get().getClassDeclaration(visitorContext)).getDeclaredProperties() + .forEach { + members[it.simpleName.asString()] = it + } + } + return members + } + + override fun hasSimpleAnnotation(element: KSAnnotated, simpleName: String): Boolean { + val annotationMirrors: MutableList = element.annotations.toMutableList() + if (element is KSPropertyDeclaration) { + annotationMirrors.addAll(element.getter!!.annotations) + } + return annotationMirrors.any { + it.annotationType.resolve().declaration.simpleName.asString() == simpleName + } + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt new file mode 100644 index 00000000000..ffb636f20fa --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/annotation/KotlinElementAnnotationMetadataFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.annotation + +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import io.micronaut.inject.ast.annotation.AbstractElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinElementAnnotationMetadataFactory( + isReadOnly: Boolean, + metadataBuilder: KotlinAnnotationMetadataBuilder +) : AbstractElementAnnotationMetadataFactory(isReadOnly, metadataBuilder) { + override fun readOnly(): ElementAnnotationMetadataFactory { + return KotlinElementAnnotationMetadataFactory(true, metadataBuilder as KotlinAnnotationMetadataBuilder) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt new file mode 100644 index 00000000000..dc5243f8217 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessor.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.Context +import io.micronaut.core.annotation.Generated +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.processing.BeanDefinitionCreator +import io.micronaut.inject.processing.BeanDefinitionCreatorFactory +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.visitor.VisitorConfiguration +import io.micronaut.inject.writer.BeanDefinitionReferenceWriter +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.visitor.KotlinClassElement +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.io.IOException + +class BeanDefinitionProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { + + private val beanDefinitionMap = mutableMapOf() + + override fun process(resolver: Resolver): List { + val visitorContext = object : KotlinVisitorContext(environment, resolver) { + override fun getConfiguration(): VisitorConfiguration { + return object : VisitorConfiguration { + override fun includeTypeLevelAnnotationsInGenericArguments(): Boolean { + return false + } + } + } + } + + val elements = resolver.getAllFiles() + .flatMap { file: KSFile -> + file.declarations + } + .filterIsInstance() + .filter { declaration: KSClassDeclaration -> + declaration.annotations.none { ksAnnotation -> + ksAnnotation.shortName.getQualifier() == Generated::class.simpleName + } + } + .toList() + + processClassDeclarations(elements, visitorContext) + return emptyList() + } + + private fun processClassDeclarations( + elements: List, + visitorContext: KotlinVisitorContext + ) { + for (classDeclaration in elements) { + if (classDeclaration.classKind != ClassKind.ANNOTATION_CLASS) { + val classElement = + visitorContext.elementFactory.newClassElement(classDeclaration.asStarProjectedType()) as KotlinClassElement + val innerClasses = + classDeclaration.declarations + .filter { it is KSClassDeclaration } + .map { it as KSClassDeclaration } + .filter { !it.modifiers.contains(Modifier.INNER) } + .toList() + if (innerClasses.isNotEmpty()) { + processClassDeclarations(innerClasses, visitorContext) + } + beanDefinitionMap.computeIfAbsent(classElement.name) { + BeanDefinitionCreatorFactory.produce(classElement, visitorContext) + } + } + } + } + + override fun finish() { + try { + val outputVisitor = KotlinOutputVisitor(environment) + val processed = HashSet() + for (beanDefinitionCreator in beanDefinitionMap.values) { + for (writer in beanDefinitionCreator.build()) { + if (processed.add(writer.beanDefinitionName)) { + processBeanDefinitions(writer, outputVisitor, processed) + } + } + } + } catch (e: ProcessingException) { + environment.logger.error(e.message!!, e.originatingElement as KSNode) + } finally { + AbstractAnnotationMetadataBuilder.clearMutated() + beanDefinitionMap.clear() + } + } + + private fun processBeanDefinitions( + beanDefinitionWriter: BeanDefinitionVisitor, + outputVisitor: KotlinOutputVisitor, + processed: HashSet + ) { + try { + beanDefinitionWriter.visitBeanDefinitionEnd() + if (beanDefinitionWriter.isEnabled) { + beanDefinitionWriter.accept(outputVisitor) + val beanDefinitionReferenceWriter = BeanDefinitionReferenceWriter(beanDefinitionWriter) + beanDefinitionReferenceWriter.setRequiresMethodProcessing(beanDefinitionWriter.requiresMethodProcessing()) + val className = beanDefinitionReferenceWriter.beanDefinitionQualifiedClassName + processed.add(className) + beanDefinitionReferenceWriter.setContextScope( + beanDefinitionWriter.annotationMetadata.hasDeclaredAnnotation(Context::class.java) + ) + beanDefinitionReferenceWriter.accept(outputVisitor) + } + } catch (e: IOException) { + // raise a compile error + val message = e.message + error("Unexpected error ${e.javaClass.simpleName}:" + (message ?: e.javaClass.simpleName)) + } + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt new file mode 100644 index 00000000000..ebcb347eb6a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/beans/BeanDefinitionProcessorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class BeanDefinitionProcessorProvider: SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return BeanDefinitionProcessor(environment) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt new file mode 100644 index 00000000000..e0e674d4ab8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/extensions.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.* +import io.micronaut.inject.ast.Element +import io.micronaut.kotlin.processing.visitor.AbstractKotlinElement +import io.micronaut.kotlin.processing.visitor.KSAnnotatedReference +import io.micronaut.kotlin.processing.visitor.KotlinVisitorContext +import java.lang.StringBuilder + +@OptIn(KspExperimental::class) +fun KSDeclaration.getBinaryName(resolver: Resolver, visitorContext: KotlinVisitorContext): String { + var declaration = unwrap() as KSDeclaration + if (declaration is KSFunctionDeclaration) { + val parent = declaration.parentDeclaration + if (parent != null) { + declaration = parent + } + } + val binaryName = resolver.mapKotlinNameToJava(declaration.qualifiedName!!)?.asString() + return if (binaryName != null) { + binaryName + } else { + val classDeclaration = declaration.getClassDeclaration(visitorContext) + val qn = classDeclaration.qualifiedName + if (qn != null) { + resolver.mapKotlinNameToJava(qn)?.asString() ?: computeName(declaration) + } else { + computeName(declaration) + } + } +} + +private fun computeName(declaration: KSDeclaration): String { + val className = StringBuilder(declaration.packageName.asString()) + val hierarchy = mutableListOf(declaration) + var parentDeclaration = declaration.parentDeclaration + while (parentDeclaration is KSClassDeclaration) { + hierarchy.add(0, parentDeclaration) + parentDeclaration = parentDeclaration.parentDeclaration + } + hierarchy.joinTo(className, "$", ".") + return className.toString() +} + +fun KSNode.unwrap() : KSNode { + return if (this is KSAnnotatedReference) { + this.node + } else { + this + } +} + +fun Element.kspNode() : Any { + return if (this is AbstractKotlinElement<*>) { + this.nativeType.unwrap() + } else { + this.nativeType + } +} + +fun KSPropertySetter.getVisibility(): Visibility { + val modifierSet = try { + this.modifiers + } catch (e: IllegalStateException) { + // KSP bug: IllegalStateException: unhandled visibility: invisible_fake + setOf(Modifier.INTERNAL) + } + return when { + modifierSet.contains(Modifier.PUBLIC) -> Visibility.PUBLIC + modifierSet.contains(Modifier.PRIVATE) -> Visibility.PRIVATE + modifierSet.contains(Modifier.PROTECTED) || + modifierSet.contains(Modifier.OVERRIDE) -> Visibility.PROTECTED + modifierSet.contains(Modifier.INTERNAL) -> Visibility.INTERNAL + else -> if (this.origin != Origin.JAVA && this.origin != Origin.JAVA_LIB) + Visibility.PUBLIC else Visibility.JAVA_PACKAGE + } +} + +@OptIn(KspExperimental::class) +fun KSAnnotated.getClassDeclaration(visitorContext: KotlinVisitorContext) : KSClassDeclaration { + when (this) { + is KSType -> { + return this.declaration.getClassDeclaration(visitorContext) + } + is KSClassDeclaration -> { + return this + } + is KSTypeReference -> { + return this.resolve().declaration.getClassDeclaration(visitorContext) + } + is KSTypeParameter -> { + return resolveDeclaration( + this.bounds.firstOrNull()?.resolve()?.declaration, + visitorContext + ) + } + is KSTypeArgument -> { + return resolveDeclaration(this.type?.resolve()?.declaration, visitorContext) + } + is KSTypeAlias -> { + val declaration = this.type.resolve().declaration + return declaration.getClassDeclaration(visitorContext) + } + else -> { + return visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } + } +} + +@OptIn(KspExperimental::class) +private fun resolveDeclaration( + declaration: KSDeclaration?, + visitorContext: KotlinVisitorContext +): KSClassDeclaration { + return if (declaration is KSClassDeclaration) { + declaration + } else { + visitorContext.resolver.getJavaClassByName(Object::class.java.name)!! + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt new file mode 100644 index 00000000000..0106ab79c54 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/AbstractKotlinElement.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.isJavaPackagePrivate +import com.google.devtools.ksp.isOpen +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.AnnotationValueBuilder +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadata +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.ElementMutableAnnotationMetadataDelegate +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.unwrap +import java.util.* +import java.util.function.Consumer +import java.util.function.Predicate + +abstract class AbstractKotlinElement(val declaration: T, + protected val annotationMetadataFactory: ElementAnnotationMetadataFactory, + protected val visitorContext: KotlinVisitorContext) : Element, ElementMutableAnnotationMetadataDelegate { + + protected var presetAnnotationMetadata: AnnotationMetadata? = null + private var elementAnnotationMetadata: ElementAnnotationMetadata? = null + + override fun getNativeType(): T { + return declaration + } + + override fun isProtected(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PROTECTED + } else { + false + } + } + + override fun isStatic(): Boolean { + return if (declaration is KSDeclaration) { + declaration.modifiers.contains(Modifier.JAVA_STATIC) + } else { + false + } + } + + protected fun makeCopy(): AbstractKotlinElement { + val element: AbstractKotlinElement = copyThis() + copyValues(element) + return element + } + + /** + * @return copy of this element + */ + protected abstract fun copyThis(): AbstractKotlinElement + + /** + * @param element the values to be copied to + */ + protected open fun copyValues(element: AbstractKotlinElement) { + element.presetAnnotationMetadata = presetAnnotationMetadata + } + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): Element? { + val kotlinElement: AbstractKotlinElement = makeCopy() + kotlinElement.presetAnnotationMetadata = annotationMetadata + return kotlinElement + } + + override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { + if (elementAnnotationMetadata == null) { + + val factory = annotationMetadataFactory + if (presetAnnotationMetadata == null) { + elementAnnotationMetadata = factory.build(this) + } else { + elementAnnotationMetadata = factory.build(this, presetAnnotationMetadata) + } + } + return elementAnnotationMetadata!! + } + + override fun isPublic(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PUBLIC + } else { + false + } + } + + override fun isPrivate(): Boolean { + return if (declaration is KSDeclaration) { + declaration.getVisibility() == Visibility.PRIVATE + } else { + false + } + } + + override fun isFinal(): Boolean { + return if (declaration is KSDeclaration) { + !declaration.isOpen() || declaration.modifiers.contains(Modifier.FINAL) + } else { + false + } + } + + override fun isAbstract(): Boolean { + return if (declaration is KSModifierListOwner) { + declaration.modifiers.contains(Modifier.ABSTRACT) + } else { + false + } + } + + @OptIn(KspExperimental::class) + override fun getModifiers(): MutableSet { + val dec = declaration.unwrap() + if (dec is KSDeclaration) { + val javaModifiers = visitorContext.resolver.effectiveJavaModifiers(dec) + return javaModifiers.mapNotNull { + when (it) { + Modifier.ABSTRACT -> ElementModifier.ABSTRACT + Modifier.FINAL -> ElementModifier.FINAL + Modifier.PRIVATE -> ElementModifier.PRIVATE + Modifier.PROTECTED -> ElementModifier.PROTECTED + Modifier.PUBLIC, Modifier.INTERNAL -> ElementModifier.PUBLIC + Modifier.JAVA_STATIC -> ElementModifier.STATIC + Modifier.JAVA_TRANSIENT -> ElementModifier.TRANSIENT + Modifier.JAVA_DEFAULT -> ElementModifier.DEFAULT + Modifier.JAVA_SYNCHRONIZED -> ElementModifier.SYNCHRONIZED + Modifier.JAVA_VOLATILE -> ElementModifier.VOLATILE + Modifier.JAVA_NATIVE -> ElementModifier.NATIVE + Modifier.JAVA_STRICT -> ElementModifier.STRICTFP + else -> null + } + }.toMutableSet() + } + return super.getModifiers() + } + + override fun annotate( + annotationType: String?, + consumer: Consumer>? + ): Element { + return super.annotate(annotationType, consumer) + } + + override fun annotate(annotationType: String?): Element { + return super.annotate(annotationType) + } + + override fun annotate( + annotationType: Class?, + consumer: Consumer>? + ): Element { + return super.annotate(annotationType, consumer) + } + + override fun annotate(annotationType: Class?): Element? { + return super.annotate(annotationType) + } + override fun annotate(annotationValue: AnnotationValue?): Element { + return super.annotate(annotationValue) + } + + override fun removeAnnotation(annotationType: String?): Element { + return super.removeAnnotation(annotationType) + } + + override fun removeAnnotation(annotationType: Class?): Element { + return super.removeAnnotation(annotationType) + } + + override fun removeAnnotationIf(predicate: Predicate>?): Element { + return super.removeAnnotationIf(predicate) + } + + override fun removeStereotype(annotationType: String?): Element { + return super.removeStereotype(annotationType) + } + + override fun removeStereotype(annotationType: Class?): Element { + return super.removeStereotype(annotationType) + } + + override fun isPackagePrivate(): Boolean { + return if (declaration is KSDeclaration) { + declaration.isJavaPackagePrivate() + } else { + false + } + } + + override fun getDocumentation(): Optional { + return if (declaration is KSDeclaration) { + Optional.ofNullable(declaration.docString) + } else { + Optional.empty() + } + } + + override fun getReturnInstance(): Element { + return this + } + + protected fun resolveGeneric( + parent: KSNode?, + type: ClassElement, + owningClass: ClassElement, + visitorContext: KotlinVisitorContext + ): ClassElement { + var resolvedType = type + if (parent is KSDeclaration && owningClass is KotlinClassElement) { + if (type is GenericPlaceholderElement) { + + val variableName = type.variableName + val genericTypeInfo = owningClass.getGenericTypeInfo() + val boundInfo = genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] + if (boundInfo != null) { + val ksType = boundInfo[variableName] + if (ksType != null) { + resolvedType = visitorContext.elementFactory.newClassElement( + ksType, + visitorContext.elementAnnotationMetadataFactory, + true + ) + if (type.isArray) { + resolvedType = resolvedType.toArray() + } + } + } + } else if (type.declaredGenericPlaceholders.isNotEmpty() && type is KotlinClassElement) { + val genericTypeInfo = owningClass.getGenericTypeInfo() + val kotlinType = type.kotlinType + val boundInfo = if (parent.qualifiedName != null) genericTypeInfo[parent.getBinaryName(visitorContext.resolver, visitorContext)] else null + resolvedType = if (boundInfo != null) { + val boundArgs = kotlinType.arguments.map { arg -> + resolveTypeArgument(arg, boundInfo, visitorContext) + }.toMutableList() + type.withBoundGenericTypes(boundArgs) + } else { + type + } + } + } + return resolvedType + } + + private fun resolveTypeArgument( + arg: KSTypeArgument, + boundInfo: Map, + visitorContext: KotlinVisitorContext + ): ClassElement { + val n = arg.type?.toString() + val resolved = boundInfo[n] + return if (resolved != null) { + visitorContext.elementFactory.newClassElement( + resolved, + annotationMetadataFactory, + false + ) + } else { + if (arg.type != null) { + val t = arg.type!!.resolve() + if (t.arguments.isNotEmpty()) { + visitorContext.elementFactory.newClassElement( + t, + annotationMetadataFactory, + false + ).withBoundGenericTypes( + t.arguments.map { + resolveTypeArgument(it, boundInfo, visitorContext) + } + ) + } else { + visitorContext.elementFactory.newClassElement( + t, + annotationMetadataFactory, + false + ) + } + } else { + visitorContext.getClassElement(Object::class.java.name).get() + } + } + } + + override fun toString(): String { + return getDescription(false) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AbstractKotlinElement<*> + + if (nativeType != other.nativeType) return false + + return true + } + + override fun hashCode(): Int { + return nativeType.hashCode() + } + + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt new file mode 100644 index 00000000000..92005234a71 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KSAnnotatedReference.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.reflect.ReflectionUtils.findMethod + +open class KSAnnotatedReference(open val nativeType: Any, val node: KSNode) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is KSAnnotatedReference) return false + + if (nativeType != other.nativeType) return false + + return true + } + + override fun hashCode(): Int { + return nativeType.hashCode() + } + + companion object Helper { + fun resolveNativeType(nativeType: Any, kind: String): Any { + val javaClass = nativeType.javaClass + val method = findMethod(javaClass, "getKt$kind") + .orElseGet { + findMethod(javaClass, "getPsi").orElseGet { + findMethod(javaClass, "getDescriptor").orElse(null) + } + } + + return if (method != null && method.canAccess(nativeType)) { + method.invoke(nativeType) + } else { + nativeType + } + } + } +} + +class KSClassReference( + private val nt: KSClassDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "ClassOrObject"), nt), KSClassDeclaration by nt { + override fun toString(): String { + return "Class(${nt.qualifiedName?.asString()})" + } +} + +class KSValueParameterReference( + private val nt: KSValueParameter +) : KSAnnotatedReference(resolveNativeType(nt, "Parameter"), nt), KSValueParameter by nt { + override fun toString(): String { + return "Parameter(${nt.name?.asString()})" + } +} + +class KSPropertyReference( + private val nt: KSPropertyDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "Property"), nt), KSPropertyDeclaration by nt { + override fun toString(): String { + return "Property(${nt.qualifiedName?.asString()})" + } +} + +class KSPropertySetterReference( + private val nt: KSPropertySetter +) : KSAnnotatedReference(resolveNativeType(nt, "PropertySetter"), nt), KSPropertySetter by nt { + override fun toString(): String { + return "PropertySetter(${nt.receiver.qualifiedName?.asString()})" + } +} + +class KSPropertyGetterReference( + private val nt: KSPropertyGetter +) : KSAnnotatedReference(resolveNativeType(nt, "PropertyGetter"), nt), KSPropertyGetter by nt { + override fun toString(): String { + return "PropertyGetter(${nt.receiver.qualifiedName?.asString()})" + } +} + + +class KSFunctionReference( + private val nt: KSFunctionDeclaration +) : KSAnnotatedReference(resolveNativeType(nt, "Function"), nt), KSFunctionDeclaration by nt { + override fun toString(): String { + return "Function(${nt.qualifiedName?.asString()})" + } +} + diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt new file mode 100644 index 00000000000..21c51ed8e3b --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinClassElement.kt @@ -0,0 +1,876 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.* +import com.google.devtools.ksp.symbol.* +import io.micronaut.context.annotation.BeanProperties +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.utils.AstBeanPropertiesUtils +import io.micronaut.inject.ast.utils.EnclosedElementsQuery +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.kotlin.processing.getBinaryName +import io.micronaut.kotlin.processing.getClassDeclaration +import java.util.* +import java.util.function.Function +import java.util.stream.Stream +import kotlin.collections.LinkedHashMap + +open class KotlinClassElement(val kotlinType: KSType, + protected val classDeclaration: KSClassDeclaration, + private val annotationInfo: KSAnnotated, + protected val elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + private val arrayDimensions: Int = 0, + private val typeVariable: Boolean = false): AbstractKotlinElement(annotationInfo, elementAnnotationMetadataFactory, visitorContext), ArrayableClassElement { + + constructor( + ref: KSAnnotated, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0, + typeVariable: Boolean = false + ) : this(getType(ref, visitorContext), ref.getClassDeclaration(visitorContext), ref, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + + constructor( + type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0, + typeVariable: Boolean = false + ) : this(type, type.declaration.getClassDeclaration(visitorContext), type.declaration.getClassDeclaration(visitorContext), elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + + + val outerType: KSType? by lazy { + val outerDecl = classDeclaration.parentDeclaration as? KSClassDeclaration + outerDecl?.asType(kotlinType.arguments.subList(classDeclaration.typeParameters.size, kotlinType.arguments.size)) + } + + private val resolvedProperties : List by lazy { + getBeanProperties(PropertyElementQuery.of(this)) + } + private val enclosedElementsQuery = KotlinEnclosedElementsQuery() + private val nativeProperties : List by lazy { + classDeclaration.getAllProperties() + .filter { !it.isPrivate() } + .map { KotlinPropertyElement( + this, + visitorContext.elementFactory.newClassElement(it.type.resolve(), elementAnnotationMetadataFactory), + it, + elementAnnotationMetadataFactory, visitorContext + ) } + .filter { !it.hasAnnotation(JvmField::class.java) } + .toList() + } + private val internalGenerics : Map> by lazy { + val boundMirrors : Map = getBoundTypeMirrors() + val data = mutableMapOf>() + if (boundMirrors.isNotEmpty()) { + data[this.name] = boundMirrors + } + val classDeclaration = classDeclaration + populateGenericInfo(classDeclaration, data, boundMirrors) + data + } + private val internalCanonicalName : String by lazy { + classDeclaration.qualifiedName!!.asString() + } + private val internalName : String by lazy { + classDeclaration.getBinaryName(visitorContext.resolver, visitorContext) + } + + private var overrideBoundGenericTypes: MutableList? = null + private var resolvedTypeArguments : MutableMap? = null + + private val nt : KSAnnotated = if (annotationInfo is KSTypeArgument) annotationInfo else KSClassReference(classDeclaration) + override fun getNativeType(): KSAnnotated { + return nt + } + + companion object Helper { + fun getType(ref: KSAnnotated, visitorContext: KotlinVisitorContext) : KSType { + if (ref is KSType) { + return ref + } else if (ref is KSTypeReference) { + return ref.resolve() + } else if (ref is KSTypeParameter) { + return ref.bounds.firstOrNull()?.resolve() ?: visitorContext.resolver.builtIns.anyType + } else if (ref is KSClassDeclaration) { + return ref.asStarProjectedType() + } else if (ref is KSTypeArgument) { + val ksType = ref.type?.resolve() + if (ksType != null) { + return ksType + } else { + throw IllegalArgumentException("Unresolvable type argument $ref") + } + } else if (ref is KSTypeAlias) { + return ref.type.resolve() + } else { + throw IllegalArgumentException("Not a type $ref") + } + } + + + + } + + override fun getName(): String { + return internalName + } + + override fun getCanonicalName(): String { + return internalCanonicalName + } + + override fun getPackageName(): String { + return classDeclaration.packageName.asString() + } + + override fun isDeclaredNullable(): Boolean { + return kotlinType.isMarkedNullable + } + + override fun isNullable(): Boolean { + return kotlinType.isMarkedNullable + } + + override fun getSyntheticBeanProperties(): List { + return nativeProperties + } + + override fun getAccessibleStaticCreators(): MutableList { + val staticCreators: MutableList = mutableListOf() + staticCreators.addAll(super.getAccessibleStaticCreators()) + return staticCreators.ifEmpty { + val companion = classDeclaration.declarations.filter { + it is KSClassDeclaration && it.isCompanionObject + }.map { it as KSClassDeclaration } + .map { visitorContext.elementFactory.newClassElement(it, elementAnnotationMetadataFactory, false) } + .firstOrNull() + + if (companion != null) { + return companion.getEnclosedElements( + ElementQuery.ALL_METHODS + .annotated { it.hasStereotype( + Creator::class.java + )} + .modifiers { it.isEmpty() || it.contains(ElementModifier.PUBLIC) } + .filter { method -> + method.returnType.isAssignable(this) + } + ) + + } else { + return mutableListOf() + } + } + } + + override fun getBeanProperties(): List { + return resolvedProperties + } + + override fun getDeclaredGenericPlaceholders(): MutableList { + return kotlinType.declaration.typeParameters.map { + KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + }.toMutableList() + } + + override fun withBoundGenericTypes(typeArguments: MutableList?): ClassElement { + if (typeArguments != null && typeArguments.size == kotlinType.declaration.typeParameters.size) { + val copy = copyThis() + copy.overrideBoundGenericTypes = typeArguments + + val i = typeArguments.iterator() + copy.resolvedTypeArguments = kotlinType.declaration.typeParameters.associate { + it.name.asString() to i.next() + }.toMutableMap() + return copy + } + return this + } + + override fun getBoundGenericTypes(): MutableList { + if (overrideBoundGenericTypes == null) { + val arguments = kotlinType.arguments + if (arguments.isEmpty()) { + return mutableListOf() + } else { + val elementFactory = visitorContext.elementFactory + this.overrideBoundGenericTypes = arguments.map { arg -> + when(arg.variance) { + Variance.STAR, Variance.COVARIANT, Variance.CONTRAVARIANT -> KotlinWildcardElement( // example List<*> + resolveUpperBounds(arg, elementFactory, visitorContext), + resolveLowerBounds(arg, elementFactory), + elementAnnotationMetadataFactory, visitorContext + ) + else -> elementFactory.newClassElement( // other cases + arg, + elementAnnotationMetadataFactory, + false + ) + } + }.toMutableList() + } + } + return overrideBoundGenericTypes!! + } + + fun getGenericTypeInfo() : Map> { + return this.internalGenerics + } + + private fun populateGenericInfo( + classDeclaration: KSClassDeclaration, + data: MutableMap>, + boundMirrors: Map? + ) { + classDeclaration.superTypes.forEach { + val superType = it.resolve() + if (superType != visitorContext.resolver.builtIns.anyType) { + val declaration = superType.declaration + val name = declaration.qualifiedName?.asString() + val binaryName = declaration.getBinaryName(visitorContext.resolver, visitorContext) + if (name != null && !data.containsKey(name)) { + val typeParameters = declaration.typeParameters + if (typeParameters.isEmpty()) { + data[binaryName] = emptyMap() + } else { + val ksTypeArguments = superType.arguments + if (typeParameters.size == ksTypeArguments.size) { + val resolved = LinkedHashMap() + var i = 0 + typeParameters.forEach { typeParameter -> + val parameterName = typeParameter.name.asString() + val typeArgument = ksTypeArguments[i] + val argumentType = typeArgument.type?.resolve() + val argumentName = argumentType?.declaration?.simpleName?.asString() + val bound = if (argumentName != null ) boundMirrors?.get(argumentName) else null + if (bound != null) { + resolved[parameterName] = bound + } else { + resolved[parameterName] = argumentType ?: typeParameter.bounds.firstOrNull()?.resolve() + ?: visitorContext.resolver.builtIns.anyType + } + i++ + } + data[binaryName] = resolved + } + } + if (declaration is KSClassDeclaration) { + val newBounds = data[binaryName] + populateGenericInfo( + declaration, + data, + newBounds + ) + } + } + } + + } + } + + private fun getBoundTypeMirrors(): Map { + val typeParameters: List = kotlinType.arguments + val parameterIterator = classDeclaration.typeParameters.iterator() + val tpi = typeParameters.iterator() + val map: MutableMap = LinkedHashMap() + while (tpi.hasNext() && parameterIterator.hasNext()) { + val tpe = tpi.next() + val parameter = parameterIterator.next() + val resolvedType = tpe.type?.resolve() + if (resolvedType != null) { + map[parameter.name.asString()] = resolvedType + } else { + map[parameter.name.asString()] = visitorContext.resolver.builtIns.anyType + } + } + return Collections.unmodifiableMap(map) + } + + private fun resolveLowerBounds(arg: KSTypeArgument, elementFactory: KotlinElementFactory): List { + return if (arg.variance == Variance.CONTRAVARIANT) { + listOf( + elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } else { + return emptyList() + } + } + + private fun resolveUpperBounds( + arg: KSTypeArgument, + elementFactory: KotlinElementFactory, + visitorContext: KotlinVisitorContext + ): List { + return if (arg.variance == Variance.COVARIANT) { + listOf( + elementFactory.newClassElement(arg.type?.resolve()!!, elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } else { + val objectType = visitorContext.resolver.getClassDeclarationByName(Object::class.java.name)!! + listOf( + elementFactory.newClassElement(objectType.asStarProjectedType(), elementAnnotationMetadataFactory, false) as KotlinClassElement + ) + } + } + + override fun getBeanProperties(propertyElementQuery: PropertyElementQuery): MutableList { + val customReaderPropertyNameResolver = + Function> { Optional.empty() } + val customWriterPropertyNameResolver = + Function> { Optional.empty() } + val accessKinds = propertyElementQuery.accessKinds + val fieldAccess = accessKinds.contains(BeanProperties.AccessKind.FIELD) && !propertyElementQuery.accessKinds.contains(BeanProperties.AccessKind.METHOD) + if (fieldAccess) { + // all kotlin fields are private + return mutableListOf() + } + + val eq = ElementQuery.of(PropertyElement::class.java) + .named { n -> !propertyElementQuery.excludes.contains(n) } + .named { n -> propertyElementQuery.includes.isEmpty() || propertyElementQuery.includes.contains(n) } + .modifiers { + val visibility = propertyElementQuery.visibility + if (visibility == BeanProperties.Visibility.PUBLIC) { + it.contains(ElementModifier.PUBLIC) + } else { + !it.contains(ElementModifier.PRIVATE) + } + }.annotated { prop -> + if(prop.hasAnnotation(JvmField::class.java)) { + false + } else { + val excludedAnnotations = propertyElementQuery.excludedAnnotations + excludedAnnotations.isEmpty() || !excludedAnnotations.any { prop.hasAnnotation(it) } + } + } + + val allProperties : MutableList = mutableListOf() + // unfortunate hack since these are not excluded? + if (hasDeclaredStereotype(ConfigurationReader::class.java)) { + val configurationBuilderQuery = ElementQuery.of(PropertyElement::class.java) + .annotated { it.hasDeclaredAnnotation(ConfigurationBuilder::class.java) } + .onlyInstance() + val configBuilderProps = enclosedElementsQuery.getEnclosedElements(this, configurationBuilderQuery) + allProperties.addAll(configBuilderProps) + } + + allProperties.addAll(enclosedElementsQuery.getEnclosedElements(this, eq)) + val propertyNames = allProperties.map { it.name }.toSet() + + val resolvedProperties : MutableList = mutableListOf() + val methodProperties = AstBeanPropertiesUtils.resolveBeanProperties(propertyElementQuery, + this, + { + getEnclosedElements( + ElementQuery.ALL_METHODS + ) + }, + { + emptyList() + }, + false, propertyNames, + customReaderPropertyNameResolver, + customWriterPropertyNameResolver, + { value: AstBeanPropertiesUtils.BeanPropertyData -> + if (!value.isExcluded) { + this.mapToPropertyElement( + value + ) + } else { + null + } + }) + resolvedProperties.addAll(methodProperties) + resolvedProperties.addAll(allProperties) + return resolvedProperties + } + + private fun mapToPropertyElement(value: AstBeanPropertiesUtils.BeanPropertyData): KotlinPropertyElement { + return KotlinPropertyElement( + this@KotlinClassElement, + value.type, + value.propertyName, + value.field, + value.getter, + value.setter, + elementAnnotationMetadataFactory, + visitorContext, + value.isExcluded + ) + } + + @OptIn(KspExperimental::class) + override fun getSimpleName(): String { + var parentDeclaration = classDeclaration.parentDeclaration + return if (parentDeclaration == null) { + val qualifiedName = classDeclaration.qualifiedName + if (qualifiedName != null) { + visitorContext.resolver.mapKotlinNameToJava(qualifiedName)?.getShortName() + ?: classDeclaration.simpleName.asString() + } else + classDeclaration.simpleName.asString() + } else { + val builder = StringBuilder(classDeclaration.simpleName.asString()) + while (parentDeclaration != null) { + builder.insert(0, '$') + .insert(0, parentDeclaration.simpleName.asString()) + parentDeclaration = parentDeclaration.parentDeclaration + } + builder.toString() + } + } + + override fun getSuperType(): Optional { + val superType = classDeclaration.superTypes.firstOrNull { + val resolved = it.resolve() + if (resolved == visitorContext.resolver.builtIns.anyType) { + false + } else { + val declaration = resolved.declaration + declaration is KSClassDeclaration && declaration.classKind != ClassKind.INTERFACE + } + } + return Optional.ofNullable(superType) + .map { + visitorContext.elementFactory.newClassElement(it.resolve()) + } + } + + override fun getInterfaces(): Collection { + return classDeclaration.superTypes.map { it.resolve() } + .filter { + it != visitorContext.resolver.builtIns.anyType + } + .filter { + val declaration = it.declaration + declaration is KSClassDeclaration && declaration.classKind == ClassKind.INTERFACE + }.map { + visitorContext.elementFactory.newClassElement(it) + }.toList() + } + + override fun isStatic(): Boolean { + return if (isInner) { + // inner classes in Kotlin are by default static unless + // the 'inner' keyword is used + !classDeclaration.modifiers.contains(Modifier.INNER) + } else { + super.isStatic() + } + } + + override fun isInterface(): Boolean { + return classDeclaration.classKind == ClassKind.INTERFACE + } + + override fun isTypeVariable(): Boolean = typeVariable + + @OptIn(KspExperimental::class) + override fun isAssignable(type: String): Boolean { + var ksType = visitorContext.resolver.getClassDeclarationByName(type)?.asStarProjectedType() + if (ksType != null) { + if (ksType.isAssignableFrom(kotlinType)) { + return true + } + val kotlinName = visitorContext.resolver.mapJavaNameToKotlin( + visitorContext.resolver.getKSNameFromString(type)) + if (kotlinName != null) { + ksType = visitorContext.resolver.getKotlinClassByName(kotlinName)?.asStarProjectedType() + if (ksType != null && kotlinType.starProjection().isAssignableFrom(ksType)) { + return true + } + } + return false + } + return false + } + + override fun isAssignable(type: ClassElement): Boolean { + if (type is KotlinClassElement) { + return type.kotlinType.isAssignableFrom(kotlinType) + } + return super.isAssignable(type) + } + + override fun copyThis(): KotlinClassElement { + val copy = KotlinClassElement( + kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable + ) + copy.resolvedTypeArguments = resolvedTypeArguments + return copy + } + + override fun withTypeArguments(typeArguments: MutableMap?): ClassElement { + val copy = copyThis() + copy.resolvedTypeArguments = typeArguments + return copy + } + + override fun isAbstract(): Boolean { + return classDeclaration.isAbstract() + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { + return super.withAnnotationMetadata(annotationMetadata) as ClassElement + } + + override fun isArray(): Boolean { + return arrayDimensions > 0 + } + + override fun getArrayDimensions(): Int { + return arrayDimensions + } + + override fun withArrayDimensions(arrayDimensions: Int): ClassElement { + return KotlinClassElement(kotlinType, classDeclaration, annotationInfo, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, typeVariable) + } + + override fun isInner(): Boolean { + return outerType != null + } + + override fun getPrimaryConstructor(): Optional { + val primaryConstructor = super.getPrimaryConstructor() + return if (primaryConstructor.isPresent) { + primaryConstructor + } else { + Optional.ofNullable(classDeclaration.primaryConstructor) + .filter { !it.isPrivate() } + .map { visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) } + } + } + + override fun getDefaultConstructor(): Optional { + val defaultConstructor = super.getDefaultConstructor() + return if (defaultConstructor.isPresent) { + defaultConstructor + } else { + Optional.ofNullable(classDeclaration.primaryConstructor) + .filter { !it.isPrivate() && it.parameters.isEmpty() } + .map { visitorContext.elementFactory.newConstructorElement( + this, + it, + elementAnnotationMetadataFactory + ) } + } + } + + override fun getTypeArguments(): Map { + if (resolvedTypeArguments == null) { + val typeArguments = mutableMapOf() + val elementFactory = visitorContext.elementFactory + val typeParameters = kotlinType.declaration.typeParameters + if (kotlinType.arguments.isEmpty()) { + typeParameters.forEach { + typeArguments[it.name.asString()] = KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + } + } else { + kotlinType.arguments.forEachIndexed { i, argument -> + val typeElement = elementFactory.newClassElement( + argument, + annotationMetadataFactory, + false + ) + typeArguments[typeParameters[i].name.asString()] = typeElement + } + } + resolvedTypeArguments = typeArguments + } + return resolvedTypeArguments!! + } + + override fun getTypeArguments(type: String): Map { + return allTypeArguments.getOrElse(type) { emptyMap() } + } + + override fun getAllTypeArguments(): Map> { + val genericInfo = getGenericTypeInfo() + return genericInfo.mapValues { entry -> + entry.value.mapValues { data -> + visitorContext.elementFactory.newClassElement(data.value, elementAnnotationMetadataFactory, false) + } + } + } + + override fun getEnclosingType(): Optional { + if (isInner) { + return Optional.of( + visitorContext.elementFactory.newClassElement( + outerType!!, + visitorContext.elementAnnotationMetadataFactory + ) + ) + } + return Optional.empty() + } + + override fun getEnclosedElements(@NonNull query: ElementQuery): MutableList { + val classElementToInspect: ClassElement = if (this is GenericPlaceholderElement) { + val bounds: List = this.bounds + if (bounds.isEmpty()) { + return mutableListOf() + } + bounds[0] + } else { + this + } + return enclosedElementsQuery.getEnclosedElements(classElementToInspect, query) + + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as KotlinClassElement + + if (arrayDimensions != other.arrayDimensions) return false + if (typeVariable != other.typeVariable) return false + if (internalCanonicalName != other.internalCanonicalName) return false + if (overrideBoundGenericTypes != other.overrideBoundGenericTypes) return false + + return true + } + + override fun hashCode(): Int { + var result = arrayDimensions + result = 31 * result + typeVariable.hashCode() + result = 31 * result + internalCanonicalName.hashCode() + result = 31 * result + (overrideBoundGenericTypes?.hashCode() ?: 0) + return result + } + + private inner class KotlinEnclosedElementsQuery : + EnclosedElementsQuery() { + override fun getExcludedNativeElements(result: ElementQuery.Result<*>): Set { + if (result.isExcludePropertyElements) { + val excludeElements: MutableSet = HashSet() + for (excludePropertyElement in beanProperties) { + excludePropertyElement.readMethod.ifPresent { methodElement: MethodElement -> + excludeElements.add( + methodElement.nativeType as KSNode + ) + } + excludePropertyElement.writeMethod.ifPresent { methodElement: MethodElement -> + excludeElements.add( + methodElement.nativeType as KSNode + ) + } + excludePropertyElement.field.ifPresent { fieldElement: FieldElement -> + excludeElements.add( + fieldElement.nativeType as KSNode + ) + } + } + return excludeElements + } + return emptySet() + } + + override fun getCacheKey(element: KSNode): KSNode { + return when(element) { + is KSFunctionDeclaration -> KSFunctionReference(element) + is KSPropertyDeclaration -> KSPropertyReference(element) + is KSClassDeclaration -> KSClassReference(element) + is KSValueParameter -> KSValueParameterReference(element) + is KSPropertyGetter -> KSPropertyGetterReference(element) + is KSPropertySetter -> KSPropertySetterReference(element) + else -> element + } + } + + override fun getSuperClass(classNode: KSClassDeclaration): KSClassDeclaration? { + val superTypes = classNode.superTypes + for (superclass in superTypes) { + val resolved = superclass.resolve() + val declaration = resolved.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.CLASS && declaration.qualifiedName?.asString() != Any::class.qualifiedName) { + return declaration + } + } + } + + return null + } + + override fun getInterfaces(classDeclaration: KSClassDeclaration): Collection { + val superTypes = classDeclaration.superTypes + val result: MutableCollection = ArrayList() + for (superclass in superTypes) { + val resolved = superclass.resolve() + val declaration = resolved.declaration + if (declaration is KSClassDeclaration) { + if (declaration.classKind == ClassKind.INTERFACE) { + result.add(declaration) + } + } + } + return result + } + + override fun getEnclosedElements( + classNode: KSClassDeclaration, + result: ElementQuery.Result<*> + ): List { + val elementType: Class<*> = result.elementType + return getEnclosedElements(classNode, result, elementType) + } + + private fun getEnclosedElements( + classNode: KSClassDeclaration, + result: ElementQuery.Result<*>, + elementType: Class<*> + ): List { + return when (elementType) { + MemberElement::class.java -> { + Stream.concat( + getEnclosedElements(classNode, result, FieldElement::class.java).stream(), + getEnclosedElements(classNode, result, MethodElement::class.java).stream() + ).toList() + } + MethodElement::class.java -> { + classNode.getDeclaredFunctions() + .filter { func: KSFunctionDeclaration -> + !func.isConstructor() && + func.origin != Origin.SYNTHETIC && + // this is a hack but no other way it seems + !listOf("hashCode", "toString", "equals").contains(func.simpleName.asString()) + } + .toList() + } + FieldElement::class.java -> { + classNode.getDeclaredProperties() + .filter { + it.hasBackingField && + it.origin != Origin.SYNTHETIC + } + .toList() + } + PropertyElement::class.java -> { + classNode.getDeclaredProperties().toList() + } + ConstructorElement::class.java -> { + classNode.getConstructors().toList() + } + ClassElement::class.java -> { + classNode.declarations.filter { + it is KSClassDeclaration + }.toList() + } + else -> { + throw java.lang.IllegalStateException("Unknown result type: $elementType") + } + } + } + + override fun excludeClass(classNode: KSClassDeclaration): Boolean { + val t = classNode.asStarProjectedType() + val builtIns = visitorContext.resolver.builtIns + return t == builtIns.anyType || + t == builtIns.nothingType || + t == builtIns.unitType || + classNode.qualifiedName.toString() == Enum::class.java.name + } + + override fun toAstElement( + enclosedElement: KSNode, + elementType: Class<*> + ): Element { + var ee = enclosedElement + if (ee is KSAnnotatedReference) { + ee = ee.node + } + val elementFactory: KotlinElementFactory = visitorContext.elementFactory + return when (ee) { + is KSFunctionDeclaration -> { + if (ee.isConstructor()) { + return elementFactory.newConstructorElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } else { + return elementFactory.newMethodElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } + + is KSPropertyDeclaration -> { + if (elementType == PropertyElement::class.java) { + val prop = KotlinPropertyElement( + this@KotlinClassElement, + visitorContext.elementFactory.newClassElement( + ee.type.resolve(), + elementAnnotationMetadataFactory + ), + ee, + elementAnnotationMetadataFactory, visitorContext + ) + if (!prop.hasAnnotation(JvmField::class.java)) { + return prop + } else { + return elementFactory.newFieldElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } else { + return elementFactory.newFieldElement( + this@KotlinClassElement, + ee, + elementAnnotationMetadataFactory + ) + } + } + + is KSType -> elementFactory.newClassElement( + ee, + elementAnnotationMetadataFactory + ) + + is KSClassDeclaration -> elementFactory.newClassElement( + ee, + elementAnnotationMetadataFactory, + false + ) + + else -> throw ProcessingException(this@KotlinClassElement, "Unknown element: $ee") + } + } + } + + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt new file mode 100644 index 00000000000..bb5735799ad --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinConstructorElement.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.Modifier +import io.micronaut.context.annotation.ConfigurationInject +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinConstructorElement(method: KSFunctionDeclaration, + declaringType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + returnType: ClassElement +): ConstructorElement, KotlinMethodElement(method, declaringType, returnType, elementAnnotationMetadataFactory, visitorContext) { + + init { + if (method.closestClassDeclaration()?.modifiers?.contains(Modifier.DATA) == true && + declaringType.hasDeclaredStereotype(ConfigurationReader::class.java)) { + annotate(ConfigurationInject::class.java) + } + } + + override fun overrides(overridden: MethodElement): Boolean { + return false + } + + override fun hides(memberElement: MemberElement?): Boolean { + return false + } + + override fun getName() = "" + + override fun getReturnType(): ClassElement = declaringType + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt new file mode 100644 index 00000000000..8d261903dbf --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinElementFactory.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinElementFactory( + private val visitorContext: KotlinVisitorContext): ElementFactory { + + companion object { + val primitives = mapOf( + "kotlin.Boolean" to PrimitiveElement.BOOLEAN, + "kotlin.Char" to PrimitiveElement.CHAR, + "kotlin.Short" to PrimitiveElement.SHORT, + "kotlin.Int" to PrimitiveElement.INT, + "kotlin.Long" to PrimitiveElement.LONG, + "kotlin.Float" to PrimitiveElement.FLOAT, + "kotlin.Double" to PrimitiveElement.DOUBLE, + "kotlin.Byte" to PrimitiveElement.BYTE, + "kotlin.Unit" to PrimitiveElement.VOID + ) + val primitiveArrays = mapOf( + "kotlin.BooleanArray" to PrimitiveElement.BOOLEAN.toArray(), + "kotlin.CharArray" to PrimitiveElement.CHAR.toArray(), + "kotlin.ShortArray" to PrimitiveElement.SHORT.toArray(), + "kotlin.IntArray" to PrimitiveElement.INT.toArray(), + "kotlin.LongArray" to PrimitiveElement.LONG.toArray(), + "kotlin.FloatArray" to PrimitiveElement.FLOAT.toArray(), + "kotlin.DoubleArray" to PrimitiveElement.DOUBLE.toArray(), + "kotlin.ByteArray" to PrimitiveElement.BYTE.toArray(), + ) + } + + fun newClassElement( + type: KSType + ): ClassElement { + return newClassElement(type, visitorContext.elementAnnotationMetadataFactory) + } + + override fun newClassElement( + type: KSType, + annotationMetadataFactory: ElementAnnotationMetadataFactory + ): ClassElement { + return newClassElement( + type, + annotationMetadataFactory, + true + ) + } + + override fun newClassElement( + type: KSType, + annotationMetadataFactory: ElementAnnotationMetadataFactory, + resolvedGenerics: Map + ): ClassElement { + return newClassElement( + type, + annotationMetadataFactory, + true + ) + } + + fun newClassElement(annotated: KSAnnotated, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + allowPrimitive: Boolean): ClassElement { + val type = KotlinClassElement.getType(annotated, visitorContext) + val declaration = type.declaration + val qualifiedName = declaration.qualifiedName!!.asString() + val hasNoAnnotations = !annotated.annotations.iterator().hasNext() + var element = primitiveArrays[qualifiedName] + if (hasNoAnnotations && element != null) { + return element + } + if (qualifiedName == "kotlin.Array") { + val component = type.arguments[0].type!!.resolve() + val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) + return componentElement.toArray() + } else if (declaration is KSTypeParameter) { + return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) + } + if (allowPrimitive && !type.isMarkedNullable) { + element = primitives[qualifiedName] + if (hasNoAnnotations && element != null ) { + return element + } + } + return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) + } else { + KotlinClassElement(annotated, elementAnnotationMetadataFactory, visitorContext) + } + } + + fun newClassElement(type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + allowPrimitive: Boolean): ClassElement { + val declaration = type.declaration + val qualifiedName = declaration.qualifiedName!!.asString() + val hasNoAnnotations = !type.annotations.iterator().hasNext() + var element = primitiveArrays[qualifiedName] + if (hasNoAnnotations && element != null) { + return element + } + if (qualifiedName == "kotlin.Array") { + val component = type.arguments[0].type!!.resolve() + val componentElement = newClassElement(component, elementAnnotationMetadataFactory, false) + return componentElement.toArray() + } else if (declaration is KSTypeParameter) { + return KotlinGenericPlaceholderElement(declaration, elementAnnotationMetadataFactory, visitorContext) + } + if (allowPrimitive && !type.isMarkedNullable) { + element = primitives[qualifiedName] + if (hasNoAnnotations && element != null ) { + return element + } + } + return if (declaration is KSClassDeclaration && declaration.classKind == ClassKind.ENUM_CLASS) { + KotlinEnumElement(type, elementAnnotationMetadataFactory, visitorContext) + } else { + KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext) + } + } + + override fun newSourceClassElement( + type: KSType, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): ClassElement { + return newClassElement(type, elementAnnotationMetadataFactory) + } + + override fun newSourceMethodElement( + owningClass: ClassElement, + method: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return newMethodElement( + owningClass, method, elementAnnotationMetadataFactory + ) + } + + override fun newMethodElement( + declaringClass: ClassElement, + method: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): KotlinMethodElement { + val returnType = method.returnType!!.resolve() + + val returnTypeElement = newClassElement(returnType, elementAnnotationMetadataFactory) + + val kotlinMethodElement = KotlinMethodElement( + method, + declaringClass, + returnTypeElement, + elementAnnotationMetadataFactory, + visitorContext + ) + if (returnType.isMarkedNullable && !kotlinMethodElement.returnType.isPrimitive) { + kotlinMethodElement.annotate(AnnotationUtil.NULLABLE) + } + return kotlinMethodElement + } + + fun newMethodElement( + declaringClass: ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertyGetter, + type: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return KotlinMethodElement(propertyElement, method, declaringClass, type, elementAnnotationMetadataFactory, visitorContext) + } + + fun newMethodElement( + declaringClass: ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertySetter, + type: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): MethodElement { + return KotlinMethodElement( + type, + propertyElement, + method, + declaringClass, + elementAnnotationMetadataFactory, + visitorContext + ) + } + + override fun newConstructorElement( + owningClass: ClassElement, + constructor: KSFunctionDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): ConstructorElement { + return KotlinConstructorElement(constructor, owningClass, elementAnnotationMetadataFactory, visitorContext, owningClass) + } + + override fun newFieldElement( + owningClass: ClassElement, + field: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory + ): FieldElement { + return KotlinFieldElement(field, owningClass, elementAnnotationMetadataFactory, visitorContext) + } + + override fun newEnumConstantElement( + owningClass: ClassElement?, + enumConstant: KSPropertyDeclaration?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory? + ): EnumConstantElement { + TODO("Not yet implemented") + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt new file mode 100644 index 00000000000..7eac7e0ba80 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumConstructorElement.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement + +class KotlinEnumConstructorElement(private val classElement: ClassElement): MethodElement { + + override fun getName(): String = "valueOf" + + override fun isProtected() = false + + override fun isPublic() = true + + override fun getNativeType(): Any { + throw UnsupportedOperationException("No native type backing a kotlin enum static constructor") + } + + override fun isStatic(): Boolean = true + + override fun getDeclaringType(): ClassElement = classElement + + override fun getReturnType(): ClassElement = classElement + + override fun getParameters(): Array { + return arrayOf(ParameterElement.of(String::class.java, "s")) + } + + override fun withNewParameters(vararg newParameters: ParameterElement?): MethodElement { + throw UnsupportedOperationException("Cannot replace parameters of a kotlin enum static constructor") + } + + override fun withParameters(vararg newParameters: ParameterElement?): MethodElement { + throw UnsupportedOperationException("Cannot replace parameters of a kotlin enum static constructor") + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt new file mode 100644 index 00000000000..5ed1963d172 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinEnumElement.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import io.micronaut.inject.ast.EnumElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.* + +class KotlinEnumElement(private val type: KSType, elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, visitorContext: KotlinVisitorContext): + KotlinClassElement(type, elementAnnotationMetadataFactory, visitorContext), EnumElement { + + override fun values(): List { + return classDeclaration.declarations + .filterIsInstance() + .map { decl -> decl.simpleName.asString() } + .toList() + } + + override fun getDefaultConstructor(): Optional { + return Optional.empty() + } + + override fun copyThis(): KotlinEnumElement { + return KotlinEnumElement( + type, + annotationMetadataFactory, + visitorContext + ) + } + + override fun getPrimaryConstructor(): Optional { + return Optional.of(KotlinEnumConstructorElement(this)) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt new file mode 100644 index 00000000000..286873516ae --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinFieldElement.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.FieldElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinFieldElement(declaration: KSPropertyDeclaration, + private val declaringType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement(KSPropertyReference(declaration), elementAnnotationMetadataFactory, visitorContext), FieldElement { + + private val internalName = declaration.simpleName.asString() + private val internalType : ClassElement by lazy { + visitorContext.elementFactory.newClassElement(declaration.type.resolve()) + } + + private val internalGenericType : ClassElement by lazy { + resolveGeneric(declaration.parent, type, declaringType, visitorContext) + } + + override fun isFinal(): Boolean { + return declaration.setter == null + } + + override fun isReflectionRequired(): Boolean { + return true // all Kotlin fields are private + } + + override fun isReflectionRequired(callingType: ClassElement?): Boolean { + return true // all Kotlin fields are private + } + + override fun isPublic(): Boolean { + return if (hasDeclaredAnnotation(JvmField::class.java)) { + super.isPublic() + } else { + false // all Kotlin fields are private + } + } + + override fun getType(): ClassElement { + return internalType + } + + override fun getName(): String { + return internalName + } + + override fun getGenericType(): ClassElement { + return internalGenericType + } + + override fun isPrimitive(): Boolean { + return type.isPrimitive + } + + override fun isArray(): Boolean { + return type.isArray + } + + override fun getArrayDimensions(): Int { + return type.arrayDimensions + } + + override fun copyThis(): AbstractKotlinElement { + return KotlinFieldElement(declaration, declaringType, annotationMetadataFactory, visitorContext) + } + + override fun isPrivate(): Boolean = true + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): FieldElement { + return super.withAnnotationMetadata(annotationMetadata) as FieldElement + } + + override fun getDeclaringType() = declaringType + + override fun getModifiers(): MutableSet { + return super.getModifiers() + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt new file mode 100644 index 00000000000..355dadee52d --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinGenericPlaceholderElement.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.closestClassDeclaration +import com.google.devtools.ksp.symbol.KSTypeParameter +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.GenericPlaceholderElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.kotlin.processing.getBinaryName +import java.util.* +import java.util.function.Function + +class KotlinGenericPlaceholderElement( + private val parameter: KSTypeParameter, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + private val arrayDimensions: Int = 0 +) : KotlinClassElement(parameter, elementAnnotationMetadataFactory, visitorContext, arrayDimensions, true), ArrayableClassElement, GenericPlaceholderElement { + override fun copyThis(): KotlinGenericPlaceholderElement { + return KotlinGenericPlaceholderElement( + parameter, + annotationMetadataFactory, + visitorContext, + arrayDimensions + ) + } + + override fun getName(): String { + val bounds = parameter.bounds.firstOrNull() + if (bounds != null) { + return bounds.resolve().declaration.getBinaryName(visitorContext.resolver, visitorContext) + } + return "java.lang.Object" + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ClassElement { + return super.withAnnotationMetadata(annotationMetadata) as ClassElement + } + + override fun isArray(): Boolean = arrayDimensions > 0 + + override fun getArrayDimensions(): Int = arrayDimensions + + override fun withArrayDimensions(arrayDimensions: Int): ClassElement { + return KotlinGenericPlaceholderElement(parameter, annotationMetadataFactory, visitorContext, arrayDimensions) + } + + override fun getBounds(): MutableList { + val elementFactory = visitorContext.elementFactory + val resolved = parameter.bounds.map { + val argumentType = it.resolve() + elementFactory.newClassElement(argumentType, annotationMetadataFactory) + }.toMutableList() + return if (resolved.isEmpty()) { + mutableListOf(visitorContext.getClassElement(Object::class.java.name).get()) + } else { + resolved + } + } + + override fun getVariableName(): String { + return parameter.simpleName.asString() + } + + override fun getDeclaringElement(): Optional { + val classDeclaration = parameter.closestClassDeclaration() + return Optional.ofNullable(classDeclaration).map { + visitorContext.elementFactory.newClassElement( + classDeclaration!!.asStarProjectedType(), + visitorContext.elementAnnotationMetadataFactory) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as KotlinGenericPlaceholderElement + + if (parameter.simpleName.asString() != other.parameter.simpleName.asString()) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + parameter.simpleName.asString().hashCode() + return result + } + + override fun foldBoundGenericTypes(fold: Function?): ClassElement { + Objects.requireNonNull(fold, "Function argument cannot be null") + return fold!!.apply(this) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt new file mode 100644 index 00000000000..8c91c6da0b0 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinMethodElement.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.* +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.util.ArrayUtils +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.kotlin.processing.getVisibility +import io.micronaut.kotlin.processing.kspNode +import io.micronaut.kotlin.processing.unwrap +import java.util.* +import java.util.function.Supplier +import kotlin.jvm.Throws + +@OptIn(KspExperimental::class) +open class KotlinMethodElement: AbstractKotlinElement, MethodElement { + + private val name: String + private val owningType: ClassElement + private val internalDeclaringType: ClassElement by lazy { + var parent = declaration.parent + if (parent is KSPropertyDeclaration) { + parent = parent.parent + } + val owner = getOwningType() + if (parent is KSClassDeclaration) { + if (owner.name.equals(parent.qualifiedName)) { + owner + } else { + visitorContext.elementFactory.newClassElement( + parent.asStarProjectedType() + ) + } + } else { + owner + } + } + + private var parameterInit : Supplier> = Supplier { emptyList() } + private val parameters: List by lazy { + parameterInit.get() + } + private val returnType: ClassElement + private val abstract: Boolean + private val public: Boolean + private val private: Boolean + private val protected: Boolean + private val internal: Boolean + private val propertyElement : KotlinPropertyElement? + + constructor(propertyType : ClassElement, + propertyElement: KotlinPropertyElement, + method: KSPropertySetter, + owningType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : super(KSPropertySetterReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.propertyElement = propertyElement + this.owningType = owningType + this.returnType = PrimitiveElement.VOID + this.abstract = method.receiver.isAbstract() + val visibility = method.getVisibility() + this.public = visibility == Visibility.PUBLIC + this.private = visibility == Visibility.PRIVATE + this.protected = visibility == Visibility.PROTECTED + this.internal = visibility == Visibility.INTERNAL + this.parameterInit = Supplier { + val parameterElement = KotlinParameterElement( + propertyType, this, method.parameter, elementAnnotationMetadataFactory, visitorContext + ) + listOf(parameterElement) + } + } + + constructor( + propertyElement: KotlinPropertyElement, + method: KSPropertyGetter, + owningType: ClassElement, + returnType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + ) : super(KSPropertyGetterReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.propertyElement = propertyElement + this.owningType = owningType + this.parameterInit = Supplier { emptyList() } + this.returnType = returnType + this.abstract = method.receiver.isAbstract() + this.public = method.receiver.isPublic() + this.private = method.receiver.isPrivate() + this.protected = method.receiver.isProtected() + this.internal = method.receiver.isInternal() + } + + constructor(method: KSFunctionDeclaration, + owningType: ClassElement, + returnType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext + ) : super(KSFunctionReference(method), elementAnnotationMetadataFactory, visitorContext) { + this.name = visitorContext.resolver.getJvmName(method)!! + this.owningType = owningType + this.parameterInit = Supplier { + method.parameters.map { + val t = visitorContext.elementFactory.newClassElement( + it.type.resolve(), + elementAnnotationMetadataFactory) + KotlinParameterElement( + t, + this, + it, + elementAnnotationMetadataFactory, + visitorContext + ) + } + } + this.propertyElement = null + this.returnType = returnType + this.abstract = method.isAbstract + this.public = method.isPublic() + this.private = method.isPrivate() + this.protected = method.isProtected() + this.internal = method.isInternal() + } + + protected constructor(method: KSAnnotated, + name: String, + owningType: ClassElement, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + returnType: ClassElement, + parameters: List, + abstract: Boolean, + public: Boolean, + private: Boolean, + protected: Boolean, + internal: Boolean + ) : super(method, elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.owningType = owningType + this.parameterInit = Supplier { + parameters + } + this.propertyElement = null + this.returnType = returnType + this.abstract = abstract + this.public = public + this.private = private + this.protected = protected + this.internal = internal + } + + override fun getOwningType(): ClassElement { + return owningType + } + + override fun isSynthetic(): Boolean { + return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { + return true + } else { + if (declaration is KSFunctionDeclaration) { + return declaration.functionKind != FunctionKind.MEMBER && declaration.functionKind != FunctionKind.STATIC + } else { + return false + } + } + } + + override fun isFinal(): Boolean { + return if (declaration is KSPropertyGetter || declaration is KSPropertySetter) { + true + } else { + super.isFinal() + } + } + + override fun getModifiers(): MutableSet { + return super.getModifiers() + } + + override fun getDeclaredTypeVariables(): MutableList { + val nativeType = kspNode() + return if (nativeType is KSDeclaration) { + nativeType.typeParameters.map { + KotlinGenericPlaceholderElement(it, annotationMetadataFactory, visitorContext) + }.toMutableList() + } else { + super.getDeclaredTypeVariables() + } + } + + override fun isSuspend(): Boolean { + val nativeType = nativeType + return if (nativeType is KSModifierListOwner) { + nativeType.modifiers.contains(Modifier.SUSPEND) + } else { + false + } + } + + override fun getSuspendParameters(): Array { + val parameters = getParameters() + return if (isSuspend) { + val continuationParameter = visitorContext.getClassElement("kotlin.coroutines.Continuation") + .map { + var rt = genericReturnType + if (rt.isPrimitive && rt.name.equals("void")) { + rt = ClassElement.of(Unit::class.java) + } + val resolvedType = it.withTypeArguments(mapOf("T" to rt)) + ParameterElement.of( + resolvedType, + "continuation" + ) + }.orElse(null) + if (continuationParameter != null) { + + ArrayUtils.concat(parameters, continuationParameter) + } else { + parameters + } + } else { + parameters + } + } + + override fun overrides(overridden: MethodElement): Boolean { + val nativeType = kspNode() + val overriddenNativeType = overridden.kspNode() + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSFunctionDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } else if (nativeType is KSPropertySetter && overriddenNativeType is KSPropertySetter) { + return overriddenNativeType.receiver == nativeType.receiver.findOverridee() + } + return false + } + + override fun hides(memberElement: MemberElement?): Boolean { + // not sure how to implement this correctly for Kotlin + return false + } + + override fun withNewOwningType(owningType: ClassElement): MethodElement { + val newMethod = KotlinMethodElement( + declaration, + name, + owningType as KotlinClassElement, + annotationMetadataFactory, + visitorContext, + returnType, + parameters, + abstract, + public, + private, + protected, + internal + ) + copyValues(newMethod) + return newMethod + } + + override fun getName(): String { + return name + } + + override fun getDeclaringType(): ClassElement { + return internalDeclaringType + } + + override fun getReturnType(): ClassElement { + return returnType + } + + override fun getGenericReturnType(): ClassElement { + return if (this is ConstructorElement) { + returnType + } else { + resolveGeneric(declaration.parent, returnType, owningType, visitorContext) + } + } + + override fun getParameters(): Array { + return parameters.toTypedArray() + } + + override fun isAbstract(): Boolean = abstract + + override fun isPublic(): Boolean = public + + override fun isProtected(): Boolean = protected + override fun copyThis(): KotlinMethodElement { + if (declaration is KSPropertySetter) { + return KotlinMethodElement( + parameters[0].type, + propertyElement!!, + declaration.unwrap() as KSPropertySetter, + owningType, + annotationMetadataFactory, + visitorContext + ) + } else if (declaration is KSPropertyGetter) { + return KotlinMethodElement( + propertyElement!!, + declaration.unwrap() as KSPropertyGetter, + owningType, + returnType, + annotationMetadataFactory, + visitorContext + ) + } else if (declaration is KSFunctionDeclaration) { + return KotlinMethodElement( + declaration.unwrap() as KSFunctionDeclaration, + owningType, + returnType, + annotationMetadataFactory, + visitorContext + ) + } else { + + return KotlinMethodElement( + declaration, + name, + owningType, + annotationMetadataFactory, + visitorContext, + returnType, + parameters, + abstract, + public, + private, + protected, + internal + ) + } + } + + override fun isPrivate(): Boolean = private + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MethodElement { + return super.withAnnotationMetadata(annotationMetadata) as MethodElement + } + + override fun toString(): String { + return "$simpleName(" + parameters.joinToString(",") { + if (it.type.isGenericPlaceholder) { + (it.type as GenericPlaceholderElement).variableName + } else { + it.genericType.name + } + } + ")" + } + + override fun withParameters(vararg newParameters: ParameterElement): MethodElement { + return KotlinMethodElement(declaration, name, owningType, annotationMetadataFactory, visitorContext, returnType, newParameters.toList(), abstract, public, private, protected, internal) + } + + override fun getThrownTypes(): Array { + return stringValues(Throws::class.java, "exceptionClasses") + .flatMap { + val ce = visitorContext.getClassElement(it).orElse(null) + if (ce != null) { + listOf(ce) + } else { + emptyList() + } + }.toTypedArray() + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt new file mode 100644 index 00000000000..f53ab4e76b8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinParameterElement.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.symbol.KSValueParameter +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.ParameterElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory + +class KotlinParameterElement( + private val parameterType: ClassElement, + private val methodElement: KotlinMethodElement, + private val parameter: KSValueParameter, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext +) : AbstractKotlinElement(KSValueParameterReference(parameter), elementAnnotationMetadataFactory, visitorContext), ParameterElement { + private val internalName : String by lazy { + parameter.name!!.asString() + } + private val internalGenericType : ClassElement by lazy { + resolveGeneric( + methodElement.declaration.parent, + parameterType, + methodElement.owningType, + visitorContext + ) + } + + override fun isPrimitive(): Boolean { + return parameterType.isPrimitive + } + + override fun isArray(): Boolean { + return parameterType.isArray + } + + override fun copyThis(): AbstractKotlinElement { + return KotlinParameterElement( + parameterType, + methodElement, + parameter, + annotationMetadataFactory, + visitorContext + ) + } + + override fun getMethodElement(): MethodElement { + return methodElement + } + + override fun getName(): String { + return internalName + } + + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): ParameterElement { + return super.withAnnotationMetadata(annotationMetadata) as ParameterElement + } + + override fun getType(): ClassElement = parameterType + + override fun getGenericType(): ClassElement { + return internalGenericType + } + + override fun getArrayDimensions(): Int = parameterType.arrayDimensions + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt new file mode 100644 index 00000000000..15874b05664 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinPropertyElement.kt @@ -0,0 +1,525 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.isAbstract +import com.google.devtools.ksp.symbol.* +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationMetadataDelegate +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.AnnotationValueBuilder +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy +import io.micronaut.inject.ast.* +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate +import io.micronaut.kotlin.processing.kspNode +import java.util.* +import java.util.function.Consumer +import java.util.function.Predicate + +class KotlinPropertyElement: AbstractKotlinElement, PropertyElement { + + private val name: String + private val classElement: ClassElement + private val type: ClassElement + private val setter: Optional + private val getter: Optional + private val field: Optional + private val abstract: Boolean + private val exc: Boolean + private var annotationMetadata: MutableAnnotationMetadataDelegate<*>? = null + private val internalDeclaringType: ClassElement by lazy { + var parent = declaration.parent + if (parent is KSPropertyDeclaration) { + parent = parent.parent + } + val owner = getOwningType() + if (parent is KSClassDeclaration) { + if (owner.name.equals(parent.qualifiedName)) { + owner + } else { + visitorContext.elementFactory.newClassElement( + parent.asStarProjectedType() + ) + } + } else { + owner + } + } + + constructor(classElement: ClassElement, + type: ClassElement, + property: KSPropertyDeclaration, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(KSPropertyReference(property), elementAnnotationMetadataFactory, visitorContext) { + this.name = property.simpleName.asString() + this.exc = excluded + this.type = type + this.classElement = classElement + this.setter = Optional.ofNullable(property.setter) + .map { method -> + val modifiers = try { + method.modifiers + } catch (e: IllegalStateException) { + // KSP bug: IllegalStateException: unhandled visibility: invisible_fake + setOf(Modifier.INTERNAL) + } + return@map if (modifiers.contains(Modifier.PRIVATE)) { + null + } else { + visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + } + } + this.getter = Optional.ofNullable(property.getter) + .map { method -> + return@map visitorContext.elementFactory.newMethodElement(classElement, this, method, type, elementAnnotationMetadataFactory) + } + this.abstract = property.isAbstract() + if (property.hasBackingField) { + val newFieldElement = visitorContext.elementFactory.newFieldElement( + classElement, + property, + elementAnnotationMetadataFactory + ) + this.field = Optional.of(newFieldElement) + } else { + this.field = Optional.empty() + } + + val elements: MutableList = ArrayList(3) + setter.ifPresent { elements.add(it) } + getter.ifPresent { elements.add(it) } + field.ifPresent { elements.add(it) } + + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next() + } else { + AnnotationMetadataHierarchy( + true, + *elements.map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + constructor(classElement: ClassElement, + type: ClassElement, + name: String, + getter: KSFunctionDeclaration, + setter: KSFunctionDeclaration?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(getter, elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.type = type + this.exc = excluded + this.classElement = classElement + this.setter = Optional.ofNullable(setter) + .map { method -> + visitorContext.elementFactory.newMethodElement(classElement, method, elementAnnotationMetadataFactory) + } + this.getter = Optional.of(visitorContext.elementFactory.newMethodElement(classElement, getter, elementAnnotationMetadataFactory)) + this.abstract = getter.isAbstract || setter?.isAbstract == true + this.field = Optional.empty() + val elements: MutableList = ArrayList(3) + this.setter.ifPresent { elements.add(it) } + this.getter.ifPresent { elements.add(it) } + field.ifPresent { elements.add(it) } + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next() + } else { + AnnotationMetadataHierarchy( + true, + *elements.stream().map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toList().toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + + constructor(classElement: ClassElement, + type: ClassElement, + name: String, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement?, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + excluded : Boolean = false) : super(pickDeclaration(type, field, getter, setter), elementAnnotationMetadataFactory, visitorContext) { + this.name = name + this.type = type + this.classElement = classElement + this.setter = Optional.ofNullable(setter) + this.getter = Optional.ofNullable(getter) + this.abstract = getter?.isAbstract == true || setter?.isAbstract == true + this.field = Optional.ofNullable(field) + val elements: MutableList = ArrayList(3) + this.setter.ifPresent { elements.add(it) } + this.getter.ifPresent { elements.add(it) } + this.field.ifPresent { elements.add(it) } + this.exc = excluded + + // The instance AnnotationMetadata of each element can change after a modification + // Set annotation metadata as actual elements so the changes are reflected + val propertyAnnotationMetadata: AnnotationMetadata + propertyAnnotationMetadata = if (elements.size == 1) { + elements.iterator().next().declaredMetadata + } else { + AnnotationMetadataHierarchy( + true, + *elements.stream().map { e: MemberElement -> + if (e is MethodElement) { + return@map object : AnnotationMetadataDelegate { + override fun getAnnotationMetadata(): AnnotationMetadata { + // Exclude type metadata + return e.getAnnotationMetadata().declaredMetadata + } + } + } + e + }.toList().toTypedArray() + ) + } + this.annotationMetadata = object : MutableAnnotationMetadataDelegate { + override fun annotate(annotationValue: AnnotationValue): Element { + for (memberElement in elements) { + memberElement.annotate(annotationValue) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: String, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: Class): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate(annotationType: String): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType) + } + return this@KotlinPropertyElement + } + + override fun annotate( + annotationType: Class, + consumer: Consumer> + ): Element { + for (memberElement in elements) { + memberElement.annotate(annotationType, consumer) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotation(annotationType: String): Element { + for (memberElement in elements) { + memberElement.removeAnnotation(annotationType) + } + return this@KotlinPropertyElement + } + + override fun removeAnnotationIf(predicate: Predicate>): Element { + for (memberElement in elements) { + memberElement.removeAnnotationIf(predicate) + } + return this@KotlinPropertyElement + } + + override fun getAnnotationMetadata(): AnnotationMetadata { + return propertyAnnotationMetadata + } + } + } + + companion object Helper { + private fun pickDeclaration( + type: ClassElement, + field: FieldElement?, + getter: MethodElement?, + setter: MethodElement? + ): KSNode { + return if (field?.nativeType != null) { + field.nativeType as KSNode + } else if (getter?.nativeType != null) { + getter.nativeType as KSNode + } else if (setter?.nativeType != null) { + setter.nativeType as KSNode + } else { + type.nativeType as KSNode + } + } + } + + override fun overrides(overridden: PropertyElement?): Boolean { + if (overridden == null) { + return false + } else { + val nativeType = kspNode() + val overriddenNativeType = overridden.kspNode() + if (nativeType == overriddenNativeType) { + return false + } else if (nativeType is KSPropertyDeclaration) { + return overriddenNativeType == nativeType.findOverridee() + } + return false + } + } + + override fun isExcluded(): Boolean { + return this.exc + } + + override fun getGenericType(): ClassElement { + return resolveGeneric(declaration.parent, getType(), classElement, visitorContext) + } + + override fun getAnnotationMetadata(): MutableAnnotationMetadataDelegate<*> { + return this.annotationMetadata!! + } + + override fun getField(): Optional { + return this.field + } + + override fun getName(): String = name + override fun getModifiers(): MutableSet { + return super.getModifiers() + } + + override fun getType(): ClassElement = type + + override fun getDeclaringType(): ClassElement { + return internalDeclaringType + } + + override fun getOwningType(): ClassElement = classElement + + override fun getReadMethod(): Optional = getter + + override fun getWriteMethod(): Optional = setter + + override fun isReadOnly(): Boolean { + return !setter.isPresent || setter.get().isPrivate + } + + override fun copyThis(): AbstractKotlinElement { + if (nativeType is KSPropertyDeclaration) { + val property : KSPropertyDeclaration = nativeType as KSPropertyDeclaration + return KotlinPropertyElement( + classElement, + type, + property, + annotationMetadataFactory, + visitorContext, + exc + ) + } else { + val getter : KSFunctionDeclaration = nativeType as KSFunctionDeclaration + return KotlinPropertyElement( + classElement, + type, + name, + getter, + setter.map { it.nativeType as KSFunctionDeclaration }.orElse(null), + annotationMetadataFactory, + visitorContext, + exc + ) + } + } + + override fun isAbstract() = abstract + override fun withAnnotationMetadata(annotationMetadata: AnnotationMetadata): MemberElement { + return super.withAnnotationMetadata(annotationMetadata) as MemberElement + } + + override fun isPrimitive(): Boolean { + return type.isPrimitive + } + + override fun isArray(): Boolean { + return type.isArray + } + + override fun getArrayDimensions(): Int { + return type.arrayDimensions + } + + override fun isDeclaredNullable(): Boolean { + return type is KotlinClassElement && type.kotlinType.isMarkedNullable + } + + override fun isNullable(): Boolean { + return type is KotlinClassElement && type.kotlinType.isMarkedNullable + } + +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt new file mode 100644 index 00000000000..639236fe2d8 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinVisitorContext.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSNode +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.convert.value.MutableConvertibleValues +import io.micronaut.core.convert.value.MutableConvertibleValuesMap +import io.micronaut.core.util.StringUtils +import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.Element +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import io.micronaut.inject.visitor.VisitorContext +import io.micronaut.inject.writer.GeneratedFile +import io.micronaut.kotlin.processing.annotation.KotlinAnnotationMetadataBuilder +import io.micronaut.kotlin.processing.KotlinOutputVisitor +import io.micronaut.kotlin.processing.annotation.KotlinElementAnnotationMetadataFactory +import java.io.* +import java.net.URI +import java.nio.file.Files +import java.util.* +import java.util.function.BiConsumer + +@OptIn(KspExperimental::class) +open class KotlinVisitorContext(private val environment: SymbolProcessorEnvironment, + val resolver: Resolver) : VisitorContext { + + private val visitorAttributes: MutableConvertibleValues + private val elementFactory: KotlinElementFactory + private val outputVisitor = KotlinOutputVisitor(environment) + val annotationMetadataBuilder: KotlinAnnotationMetadataBuilder + private val elementAnnotationMetadataFactory: KotlinElementAnnotationMetadataFactory + + init { + visitorAttributes = MutableConvertibleValuesMap() + annotationMetadataBuilder = KotlinAnnotationMetadataBuilder(environment, resolver, this) + elementFactory = KotlinElementFactory(this) + elementAnnotationMetadataFactory = KotlinElementAnnotationMetadataFactory(false, annotationMetadataBuilder) + } + + override fun get(name: CharSequence?, conversionContext: ArgumentConversionContext?): Optional { + return visitorAttributes.get(name, conversionContext) + } + + override fun names(): MutableSet { + return visitorAttributes.names() + } + + override fun values(): MutableCollection { + return visitorAttributes.values() + } + + override fun put(key: CharSequence?, value: Any?): MutableConvertibleValues { + visitorAttributes.put(key, value) + return this + } + + override fun remove(key: CharSequence?): MutableConvertibleValues { + visitorAttributes.remove(key) + return this + } + + override fun clear(): MutableConvertibleValues { + visitorAttributes.clear() + return this + } + + override fun getClassElement(name: String): Optional { + var declaration = resolver.getClassDeclarationByName(name) + if (declaration == null) { + declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) + } + return Optional.ofNullable(declaration?.asStarProjectedType()) + .map(elementFactory::newClassElement) + } + + @OptIn(KspExperimental::class) + override fun getClassElements(aPackage: String, vararg stereotypes: String): Array { + return resolver.getDeclarationsFromPackage(aPackage) + .filterIsInstance() + .filter { declaration -> + declaration.annotations.any { ann -> + stereotypes.contains(KotlinAnnotationMetadataBuilder.getAnnotationTypeName(ann, this)) + } + } + .map { declaration -> + elementFactory.newClassElement(declaration.asStarProjectedType()) + } + .toList() + .toTypedArray() + } + + override fun getServiceEntries(): MutableMap> { + return outputVisitor.serviceEntries + } + + override fun visitClass(classname: String, vararg originatingElements: Element): OutputStream { + return outputVisitor.visitClass(classname, *originatingElements) + } + + override fun visitServiceDescriptor(type: String, classname: String) { + outputVisitor.visitServiceDescriptor(type, classname) + } + + override fun visitServiceDescriptor(type: String, classname: String, originatingElement: Element) { + outputVisitor.visitServiceDescriptor(type, classname, originatingElement) + } + + override fun visitMetaInfFile(path: String, vararg originatingElements: Element): Optional { + return outputVisitor.visitMetaInfFile(path, *originatingElements) + } + + override fun visitGeneratedFile(path: String?): Optional { + return outputVisitor.visitGeneratedFile(path) + } + + override fun finish() { + outputVisitor.finish() + } + + override fun getClassElement( + name: String, + annotationMetadataFactory: ElementAnnotationMetadataFactory + ): Optional { + var declaration = resolver.getClassDeclarationByName(name) + if (declaration == null) { + declaration = resolver.getClassDeclarationByName(name.replace('$', '.')) + } + return Optional.ofNullable(declaration?.asStarProjectedType()) + .map { elementFactory.newClassElement(it, annotationMetadataFactory) } + } + + override fun getElementFactory(): KotlinElementFactory = elementFactory + override fun getElementAnnotationMetadataFactory(): ElementAnnotationMetadataFactory { + return elementAnnotationMetadataFactory + } + + override fun getAnnotationMetadataBuilder(): AbstractAnnotationMetadataBuilder<*, *> { + return annotationMetadataBuilder + } + + override fun info(message: String, element: Element?) { + printMessage(message, environment.logger::info, element) + } + + fun info(message: String, element: KSNode?) { + printMessage(message, environment.logger::info, element) + } + + override fun info(message: String) { + printMessage(message, environment.logger::info, null as KSNode?) + } + + override fun fail(message: String, element: Element?) { + printMessage(message, environment.logger::error, element) + } + + fun fail(message: String, element: KSNode?) { + printMessage(message, environment.logger::error, element) + } + + override fun warn(message: String, element: Element?) { + printMessage(message, environment.logger::warn, element) + } + + fun warn(message: String, element: KSNode?) { + printMessage(message, environment.logger::warn, element) + } + + private fun printMessage(message: String, logger: BiConsumer, element: Element?) { + if (element is AbstractKotlinElement<*>) { + val el = element.nativeType + printMessage(message, logger, el) + } else { + printMessage(message, logger, null as KSNode?) + } + } + + private fun printMessage(message: String, logger: BiConsumer, element: KSNode?) { + if (StringUtils.isNotEmpty(message)) { + logger.accept(message, element) + } + } + + class KspGeneratedFile(private val outputStream: OutputStream, + private val path: String) : GeneratedFile { + + private val file = File(path) + + override fun toURI(): URI { + return file.toURI() + } + + override fun getName(): String { + return file.name + } + + override fun openInputStream(): InputStream { + return Files.newInputStream(file.toPath()) + } + + override fun openOutputStream(): OutputStream = outputStream + + override fun openReader(): Reader { + return file.reader() + } + + override fun getTextContent(): CharSequence { + return file.readText() + } + + override fun openWriter(): Writer { + return OutputStreamWriter(outputStream) + } + + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt new file mode 100644 index 00000000000..170201abe24 --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/KotlinWildcardElement.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.ast.ArrayableClassElement +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.WildcardElement +import io.micronaut.inject.ast.annotation.ElementAnnotationMetadataFactory +import java.util.function.Function + +class KotlinWildcardElement( + private val upperBounds: List, + private val lowerBounds: List, + elementAnnotationMetadataFactory: ElementAnnotationMetadataFactory, + visitorContext: KotlinVisitorContext, + arrayDimensions: Int = 0 +) : KotlinClassElement( + upperBounds[0]!!.nativeType, + elementAnnotationMetadataFactory, + visitorContext, + arrayDimensions, + false +), WildcardElement { + + override fun foldBoundGenericTypes(@NonNull fold: Function): ClassElement? { + val upperBounds: List = this.upperBounds + .map { ele -> + toKotlinClassElement( + ele?.foldBoundGenericTypes(fold) + ) + }.toList() + val lowerBounds: List = this.lowerBounds + .map { ele -> + toKotlinClassElement( + ele?.foldBoundGenericTypes(fold) + ) + }.toList() + return fold.apply( + if (upperBounds.contains(null) || lowerBounds.contains(null)) null else KotlinWildcardElement( + upperBounds, lowerBounds, elementAnnotationMetadataFactory, visitorContext, arrayDimensions + ) + ) + } + + override fun getUpperBounds(): MutableList { + val list = mutableListOf() + list.addAll(upperBounds) + return list + } + + override fun getLowerBounds(): MutableList { + val list = mutableListOf() + list.addAll(lowerBounds) + return list + } + + private fun toKotlinClassElement(element: ClassElement?): KotlinClassElement { + return if (element == null || element is KotlinClassElement) { + element as KotlinClassElement + } else { + if (element.isWildcard || element.isGenericPlaceholder) { + throw UnsupportedOperationException("Cannot convert wildcard / free type variable to JavaClassElement") + } else { + (visitorContext.getClassElement(element.name, elementAnnotationMetadataFactory) + .orElseThrow { + UnsupportedOperationException( + "Cannot convert ClassElement to JavaClassElement, class was not found on the visitor context" + ) + } as ArrayableClassElement) + .withArrayDimensions(element.arrayDimensions) + .withBoundGenericTypes(element.boundGenericTypes) as KotlinClassElement + } + } + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt new file mode 100644 index 00000000000..274955f379a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/LoadedVisitor.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.order.Ordered +import io.micronaut.core.reflect.GenericTypeUtils +import io.micronaut.inject.visitor.TypeElementVisitor +import java.util.* + +class LoadedVisitor(val visitor: TypeElementVisitor<*, *>, + val visitorContext: KotlinVisitorContext): Ordered { + + companion object { + const val ANY = "kotlin.Any" + } + + var classAnnotation: String = ANY + var elementAnnotation: String = ANY + + init { + val javaClass = visitor.javaClass + val resolver = visitorContext.resolver + val declaration = resolver.getClassDeclarationByName(javaClass.name) + val tevClassName = TypeElementVisitor::class.java.name + + if (declaration != null) { + val reference = declaration.superTypes + .map { it.resolve() } + .find { + it.declaration.qualifiedName?.asString() == tevClassName + }!! + classAnnotation = getType(reference.arguments[0].type!!.resolve(), visitor.classType) + elementAnnotation = getType(reference.arguments[1].type!!.resolve(), visitor.elementType) + } else { + val classes = GenericTypeUtils.resolveInterfaceTypeArguments( + javaClass, + TypeElementVisitor::class.java + ) + if (classes != null && classes.size == 2) { + val classGeneric = classes[0] + classAnnotation = if (classGeneric == Any::class.java) { + visitor.classType + } else { + classGeneric.name + } + val elementGeneric = classes[1] + elementAnnotation = if (elementGeneric == Any::class.java) { + visitor.elementType + } else { + elementGeneric.name + } + } else { + classAnnotation = Any::class.java.name + elementAnnotation = Any::class.java.name + } + } + if (classAnnotation == ANY) { + classAnnotation = Object::class.java.name + } + if (elementAnnotation == ANY) { + elementAnnotation = Object::class.java.name + } + } + + override fun getOrder(): Int { + return visitor.order + } + + private fun getType(type: KSType, default: String): String { + return if (!type.isError) { + val elementAnnotation = type.declaration.qualifiedName!!.asString() + if (elementAnnotation == ANY) { + default + } else { + elementAnnotation + } + } else { + //sigh + UUID.randomUUID().toString() + } + } + + fun matches(classDeclaration: KSClassDeclaration): Boolean { + if (classAnnotation == "java.lang.Object") { + return true + } + val annotationMetadata = visitorContext.annotationMetadataBuilder.buildDeclared(classDeclaration) + return annotationMetadata.hasStereotype(classAnnotation) + } + + fun matches(annotationMetadata: AnnotationMetadata): Boolean { + if (elementAnnotation == "java.lang.Object") { + return true + } + return annotationMetadata.hasStereotype(elementAnnotation) + } +} diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt new file mode 100644 index 00000000000..37dd9d0b36e --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessor.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.isConstructor +import com.google.devtools.ksp.isInternal +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.* +import com.google.devtools.ksp.visitor.KSTopDownVisitor +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Requires.Sdk +import io.micronaut.core.annotation.Generated +import io.micronaut.core.annotation.NonNull +import io.micronaut.core.order.OrderUtil +import io.micronaut.core.util.StringUtils +import io.micronaut.core.version.VersionUtils +import io.micronaut.inject.ast.* +import io.micronaut.inject.processing.ProcessingException +import io.micronaut.inject.visitor.TypeElementVisitor +import io.micronaut.inject.visitor.VisitorContext +import java.util.* + +open class TypeElementSymbolProcessor(private val environment: SymbolProcessorEnvironment): SymbolProcessor { + + private lateinit var loadedVisitors: MutableList + private lateinit var typeElementVisitors: Collection> + private lateinit var visitorContext: KotlinVisitorContext + + companion object { + private val SERVICE_LOADER = io.micronaut.core.io.service.SoftServiceLoader.load(TypeElementVisitor::class.java) + } + + open fun newClassElement( + visitorContext: KotlinVisitorContext, + classDeclaration: KSClassDeclaration + ) = visitorContext.elementFactory.newClassElement( + classDeclaration.asStarProjectedType(), + visitorContext.elementAnnotationMetadataFactory + ) + + override fun process(resolver: Resolver): List { + // set supported options as system properties to keep compatibility + // in particular for micronaut-openapi + environment.options.entries.stream() + .filter { (key) -> + key.startsWith( + VisitorContext.MICRONAUT_BASE_OPTION_NAME + ) + } + .forEach { (key, value) -> + System.setProperty( + key, + value + ) + } + + typeElementVisitors = findTypeElementVisitors() + loadedVisitors = ArrayList(typeElementVisitors.size) + visitorContext = KotlinVisitorContext(environment, resolver) + + start() + + if (loadedVisitors.isNotEmpty()) { + + + val elements = resolver.getAllFiles() + .flatMap { file: KSFile -> file.declarations } + .filterIsInstance() + .filter { declaration: KSClassDeclaration -> + declaration.annotations.none { ksAnnotation -> + ksAnnotation.shortName.getQualifier() == Generated::class.simpleName + } + } + .toList() + + if (elements.isNotEmpty()) { + + // The visitor X with a higher priority should process elements of A before + // the visitor Y which is processing elements of B but also using elements A + + // Micronaut Data use-case: EntityMapper with a higher priority needs to process entities first + // before RepositoryMapper is going to process repositories and read entities + for (loadedVisitor in loadedVisitors) { + for (typeElement in elements) { + if (!loadedVisitor.matches(typeElement)) { + continue + } + if (typeElement.classKind != ClassKind.ANNOTATION_CLASS) { + val className = typeElement.qualifiedName.toString() + try { + typeElement.accept(ElementVisitor(loadedVisitor, typeElement), className) + } catch (e: ProcessingException) { + val message = e.message + if (message != null) { + environment.logger.error(message, e.originatingElement as KSNode) + } else { + environment.logger.error("Unknown error processing element", e.originatingElement as KSNode) + val cause = e.cause + if (cause != null) { + environment.logger.exception(cause) + } else { + environment.logger.exception(e) + } + } + } + } + } + } + } + } + return emptyList() + } + + override fun finish() { + for (loadedVisitor in loadedVisitors) { + try { + loadedVisitor.visitor.finish(visitorContext) + } catch (e: Throwable) { + environment.logger.error("Error finalizing type visitor [${loadedVisitor.visitor}]: ${e.message}") + } + } + visitorContext.finish() + } + + override fun onError() { + + } + + private fun start() { + for (visitor in typeElementVisitors) { + try { + loadedVisitors.add( + LoadedVisitor( + visitor, + visitorContext + ) + ) + } catch (e: TypeNotPresentException) { + // ignored, means annotations referenced are not on the classpath + } catch (e: NoClassDefFoundError) { + } + + } + + OrderUtil.reverseSort(loadedVisitors) + + for (loadedVisitor in loadedVisitors) { + try { + loadedVisitor.visitor.start(visitorContext) + } catch (e: Throwable) { + environment.logger.error("Error initializing type visitor [${loadedVisitor.visitor}]: ${e.message}") + } + } + } + + @NonNull + private fun findTypeElementVisitors(): Collection> { + val typeElementVisitors: MutableMap> = HashMap(10) + for (definition in SERVICE_LOADER) { + if (definition.isPresent) { + val visitor: TypeElementVisitor<*, *>? = try { + definition.load() + } catch (e: Throwable) { + environment.logger.warn("TypeElementVisitor [" + definition.name + "] will be ignored due to loading error: " + e.message) + continue + } + if (visitor == null || !visitor.isEnabled) { + continue + } + val requires = visitor.javaClass.getAnnotation(Requires::class.java) + if (requires != null) { + val sdk: Sdk = requires.sdk + if (sdk == Sdk.MICRONAUT) { + val version: String = requires.version + if (StringUtils.isNotEmpty(version) && !VersionUtils.isAtLeastMicronautVersion(version)) { + try { + environment.logger.warn("TypeElementVisitor [" + definition.name + "] will be ignored because Micronaut version [" + VersionUtils.MICRONAUT_VERSION + "] must be at least " + version) + continue + } catch (e: IllegalArgumentException) { + // shouldn't happen, thrown when invalid version encountered + } + } + } + } + typeElementVisitors[definition.name] = visitor + } + } + return typeElementVisitors.values + } + + private inner class ElementVisitor(private val loadedVisitor: LoadedVisitor, + private val classDeclaration: KSClassDeclaration) : KSTopDownVisitor() { + + override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Any): Any { + if (classDeclaration.qualifiedName!!.asString() == "kotlin.Any") { + return data + } + if (classDeclaration.classKind == ClassKind.ENUM_ENTRY) { + return data + } + if (classDeclaration == this.classDeclaration) { + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(classDeclaration)) { + val classElement = newClassElement(visitorContext, classDeclaration) + + try { + loadedVisitor.visitor.visitClass(classElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(classElement, e.message) + } + + + classDeclaration.getAllFunctions() + .filter { it.isConstructor() && !it.isInternal() } + .forEach { + visitConstructor(classElement, it) + } + + visitMembers(classElement) + val innerClassQuery = + ElementQuery.ALL_INNER_CLASSES.onlyStatic().modifiers { it.contains(ElementModifier.PUBLIC) } + val innerClasses = classElement.getEnclosedElements(innerClassQuery) + innerClasses.forEach { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(it)) { + visitor.visitClass(it, visitorContext) + visitMembers(it) + } + } + } + } + return data + } + + private fun visitMembers(classElement: ClassElement) { + val properties = classElement.syntheticBeanProperties + for (property in properties) { + try { + visitNativeProperty(property) + } catch (e: Exception) { + throw ProcessingException(property, e.message, e) + } + } + val memberElements = classElement.getEnclosedElements(ElementQuery.ALL_FIELD_AND_METHODS) + for (memberElement in memberElements) { + when (memberElement) { + is FieldElement -> { + visitField(memberElement) + } + + is MethodElement -> { + visitMethod(memberElement) + } + } + } + } + + private fun visitMethod(memberElement: MethodElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(memberElement)) { + try { + visitor.visitMethod(memberElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(memberElement, e.message) + } + } + } + + private fun visitField(memberElement: FieldElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(memberElement)) { + try { + visitor.visitField(memberElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(memberElement, e.message) + } + } + } + + private fun visitConstructor(classElement: ClassElement, ctor: KSFunctionDeclaration) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + val ctorElement = visitorContext.elementFactory.newConstructorElement( + classElement, + ctor, + visitorContext.elementAnnotationMetadataFactory + ) + if (loadedVisitor.matches(ctorElement)) { + try { + visitor.visitConstructor(ctorElement, visitorContext) + } catch (e: Exception) { + throw ProcessingException(ctorElement, e.message) + } + } + } + + fun visitNativeProperty(propertyNode : PropertyElement) { + val visitor = loadedVisitor.visitor + val visitorContext = loadedVisitor.visitorContext + if (loadedVisitor.matches(propertyNode)) { + propertyNode.field.ifPresent { visitor.visitField(it, visitorContext)} + // visit synthetic getter/setter methods + propertyNode.writeMethod.ifPresent { visitor.visitMethod(it, visitorContext)} + propertyNode.readMethod.ifPresent{ visitor.visitMethod(it, visitorContext)} + } + } + + override fun defaultHandler(node: KSNode, data: Any): Any { + return data + } + + } +} + diff --git a/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt new file mode 100644 index 00000000000..769d745762a --- /dev/null +++ b/inject-kotlin/src/main/kotlin/io/micronaut/kotlin/processing/visitor/TypeElementSymbolProcessorProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2022 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.visitor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +open class TypeElementSymbolProcessorProvider: SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return TypeElementSymbolProcessor(environment) + } +} diff --git a/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000000..4a2ff0176c0 --- /dev/null +++ b/inject-kotlin/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1,2 @@ +io.micronaut.kotlin.processing.visitor.TypeElementSymbolProcessorProvider +io.micronaut.kotlin.processing.beans.BeanDefinitionProcessorProvider diff --git a/inject-kotlin/src/main/resources/notes.txt b/inject-kotlin/src/main/resources/notes.txt new file mode 100644 index 00000000000..064c96547ea --- /dev/null +++ b/inject-kotlin/src/main/resources/notes.txt @@ -0,0 +1,3 @@ +Differences: + +In Kotlin the enums have implicit properties name and ordinal that I'm not able to distinguish between defined properties. In Java only the defined properties are properties in the introspeciton diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy new file mode 100644 index 00000000000..d5184e9092b --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/adapter/MethodAdapterSpec.groovy @@ -0,0 +1,380 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.adapter + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import spock.lang.PendingFeature +import spock.lang.Specification + +import java.nio.charset.StandardCharsets + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class MethodAdapterSpec extends Specification { + + void 'test method adapter with failing requirements is not present'() { + given: + def context = buildContext(''' +package issue5640 + +import io.micronaut.aop.Adapter +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.nio.charset.StandardCharsets + +@Singleton +@Requires(property="not.present") +class AsciiParser { + @Parse + fun parseAsAscii(value: ByteArray): String { + return String(value, StandardCharsets.US_ASCII) + } +} + +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Adapter(Parser::class) +annotation class Parse + +interface Parser { + fun parse(value: ByteArray): String +} +''') + def adaptedType = context.classLoader.loadClass('issue5640.Parser') + + expect: + !context.containsBean(adaptedType) + context.getBeansOfType(adaptedType).isEmpty() + } + + void 'test method adapter with byte[] argument'() { + given: + def context = buildContext(''' +package issue5054 + +import io.micronaut.aop.Adapter +import jakarta.inject.Singleton +import java.nio.charset.StandardCharsets + +@Singleton +class AsciiParser { + @Parse + fun parseAsAscii(value: ByteArray): String { + return String(value, StandardCharsets.US_ASCII) + } +} + +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Adapter(Parser::class) +annotation class Parse + +interface Parser { + fun parse(value: ByteArray): String +} +''') + def adaptedType = context.classLoader.loadClass('issue5054.Parser') + def parser = context.getBean(adaptedType) + def result = parser.parse("test".getBytes(StandardCharsets.US_ASCII)) + + expect: + result == 'test' + } + + void "test method adapter inherits metadata"() { + when:"An adapter method is parsed that has requirements" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test + +import io.micronaut.aop.Adapter +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent + +@jakarta.inject.Singleton +@io.micronaut.context.annotation.Requires(property="foo.bar") +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + + } +} + +''') + then:"Then a bean is produced that is valid" + definition != null + definition.annotationMetadata.hasAnnotation(Requires) + definition.annotationMetadata.stringValue(Requires, "property").get() == 'foo.bar' + } + + void "test method adapter with around overloading"() { + given: + def context = buildContext(''' +package adapteroverloading + +import io.micronaut.context.event.* +import jakarta.inject.Singleton +import io.micronaut.runtime.event.annotation.* + +@Singleton +class Test { + var invoked = false + var shutdown = false + + @EventListener + fun receive(event: StartupEvent) { + invoked = true + } + + @EventListener + fun receive(event: ShutdownEvent) { + shutdown = true + } +} + +''') + + when: + def bean = getBean(context, 'adapteroverloading.Test') + + then: + bean.invoked + + when: + context.close() + + then: + bean.shutdown + } + + void "test method adapter with around advice"() { + given: + def context = buildContext(''' +package adapteraround + +import io.micronaut.context.event.StartupEvent +import io.micronaut.scheduling.annotation.Async +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture +import io.micronaut.runtime.event.annotation.* + +@Singleton +open class Test { + + var invoked = false + private set + + @EventListener + @Async + open fun onStartup(event: StartupEvent): CompletableFuture { + invoked = true + return CompletableFuture.completedFuture(invoked) + } +} + +''') + + def bean = getBean(context,'adapteraround.Test') + + expect: + bean.invoked + + cleanup: + context.close() + } + + void "test method adapter produces additional bean"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test; + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + + } +} + +''') + then:"Then a bean is produced that is valid" + definition != null + !(definition instanceof AdvisedBeanType) + ApplicationEventListener.isAssignableFrom(definition.getBeanType()) + !definition.getTypeArguments(ApplicationEventListener).isEmpty() + definition.getTypeArguments(ApplicationEventListener).get(0).type == StartupEvent + } + + void "test method adapter inherited from an interface produces additional bean"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$ApplicationEventListener$onStartup1$Intercepted','''\ +package test; + +import io.micronaut.aop.*; +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import io.micronaut.context.event.*; + +@jakarta.inject.Singleton +class Test: TestContract { + + override fun onStartup(event: StartupEvent) { + } +} + +interface TestContract { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) +} +''') + then:"Then a bean is produced that is valid" + definition != null + ApplicationEventListener.isAssignableFrom(definition.getBeanType()) + !definition.getTypeArguments(ApplicationEventListener).isEmpty() + definition.getTypeArguments(ApplicationEventListener).get(0).type == StartupEvent + } + + void "test method adapter honours type restraints - correct path"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$Foo$myMethod1$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(Foo::class) + fun myMethod(blah: String) { + + } +} + +interface Foo: java.util.function.Consumer +''') + then:"Then a bean is produced that is valid" + definition != null + ReflectionUtils.getAllInterfaces(definition.getBeanType()).find { it.name == 'test.Foo'} + !definition.getTypeArguments("test.Foo").isEmpty() + definition.getTypeArguments("test.Foo").get(0).type == String + } + + void "test method adapter honours type restraints - compilation error"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('test.Test$Foo$myMethod$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(Foo::class) + fun myMethod(blah: Integer) { + + } +} + +interface Foo: java.util.function.Consumer +''') + then:"An error occurs" + def e = thrown(RuntimeException) + e.message.contains 'Cannot adapt method [myMethod(java.lang.Integer)] to target method [accept(T)]. Type [java.lang.Integer] is not a subtype of type [java.lang.CharSequence] for argument at position 0' + } + + void "test method adapter wrong argument count"() { + when:"An adapter method is parsed" + buildBeanDefinition('test.Test$ApplicationEventListener$onStartup$Intercepted','''\ +package test + +import io.micronaut.aop.* +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@jakarta.inject.Singleton +class Test { + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent, stuff: Boolean) { + + } +} + +''') + then:"Then a bean is produced that is valid" + def e = thrown(RuntimeException) + e.message.contains("Cannot adapt method [onStartup(io.micronaut.context.event.StartupEvent,boolean)] to target method [onApplicationEvent(E)]. Argument lengths don't match.") + + } +/* + void "test method adapter argument order"() { + when:"An adapter method is parsed" + BeanDefinition definition = buildBeanDefinition('org.atinject.jakartatck.auto.events.EventListener$EventHandlerMultipleArguments$onEvent1$Intercepted','''\ +package org.atinject.jakartatck.auto.events; + +@jakarta.inject.Singleton +class EventListener { + + @EventHandler + public void onEvent(Metadata metadata, SomeEvent event) { + } + +} + +''') + then:"Then a bean is produced that is valid" + definition != null + EventHandlerMultipleArguments.isAssignableFrom(definition.getBeanType()) + definition.getTypeArguments(EventHandlerMultipleArguments).size() == 2 + definition.getTypeArguments(EventHandlerMultipleArguments).get(0).type == Metadata + definition.getTypeArguments(EventHandlerMultipleArguments).get(1).type == SomeEvent + } +*/ + + void "test adapter is invoked"() { + given: + ApplicationContext ctx = ApplicationContext.run("foo.bar":true) + + when: + Test t = ctx.getBean(Test) + + then: + t.invoked + + cleanup: + ctx.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy new file mode 100644 index 00000000000..612ee5df725 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AbstractClassIntroductionSpec.groovy @@ -0,0 +1,262 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AbstractClassIntroductionSpec extends Specification { + + void "test that a non-abstract method defined in class is not overridden by the introduction advise"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean { + abstract fun isAbstract(): String + + fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Foo { + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on the method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Foo { + @Stub + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on a super interface method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Bar { + @Stub + fun nonAbstract(): String + + fun another(): String +} + +interface Foo: Bar + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" + + override fun another(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.another() == 'good' + } + + void "test that a non-abstract method defined in class is and implemented from an interface not overridden by the introduction advise that also defines advice on the class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +interface Foo { + fun nonAbstract(): String +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + } + + void "test that a default method defined in a interface is not implemented by Introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +interface Foo { + fun nonAbstract(): String + + fun anotherNonAbstract(): String = "good" +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.anotherNonAbstract() == 'good' + } + + void "test that a default method overridden from parent interface is not implemented by Introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub + +interface Bar { + fun anotherNonAbstract(): String +} + +interface Foo: Bar { + fun nonAbstract(): String + + override fun anotherNonAbstract(): String = "good" +} + +@Stub +@jakarta.inject.Singleton +abstract class AbstractBean: Foo { + abstract fun isAbstract(): String + + override fun nonAbstract(): String = "good" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.isAbstract() == null + instance.nonAbstract() == 'good' + instance.anotherNonAbstract() == 'good' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy new file mode 100644 index 00000000000..7c29ef132a5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AnnotatedConstructorArgumentSpec.groovy @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationMetadata +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AnnotatedConstructorArgumentSpec extends Specification { + + void "test that constructor arguments propagate annotation metadata"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Value + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") val myValue: String) { + + open fun someMethod(someVal: String): String = "$myValue $someVal" + + internal open fun someMethodPackagePrivateMethod(someVal: String): String = "$myValue $someVal" +} +''') + + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.constructor.arguments.size() == 5 + beanDefinition.constructor.arguments[0].name == 'myValue' + beanDefinition.constructor.arguments[1].name == '$beanResolutionContext' + beanDefinition.constructor.arguments[2].name == '$beanContext' + beanDefinition.constructor.arguments[3].name == '$qualifier' + beanDefinition.constructor.arguments[4].name == '$interceptors' + beanDefinition.constructor.arguments[4] + .annotationMetadata + .getAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER) + .getAnnotations(AnnotationMetadata.VALUE_MEMBER, InterceptorBinding)[0] + .stringValue().get() == Mutating.name + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod("foo") == 'test changed' + instance.someMethodPackagePrivateMethod$main("foo") == 'test foo' + } + + void "test that constructor arguments propagate annotation metadata - method level AOP"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Value + +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") val myValue: String) { + + @Mutating("someVal") + open fun someMethod(someVal: String): String = "$myValue $someVal" + + @Mutating("someVal") + internal open fun someMethodPackagePrivateMethod(someVal: String): String = "$myValue $someVal" +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.constructor.arguments.size() == 5 + beanDefinition.constructor.arguments[0].name == 'myValue' + beanDefinition.constructor.arguments[1].name == '$beanResolutionContext' + beanDefinition.constructor.arguments[2].name == '$beanContext' + beanDefinition.constructor.arguments[3].name == '$qualifier' + beanDefinition.constructor.arguments[4].name == '$interceptors' + beanDefinition.constructor.arguments[4] + .annotationMetadata + .getAnnotation(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER) + .getAnnotations(AnnotationMetadata.VALUE_MEMBER, InterceptorBinding)[0] + .stringValue().get() == Mutating.name + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod("foo") == 'test changed' + instance.someMethodPackagePrivateMethod$main("foo") == 'test changed' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy new file mode 100644 index 00000000000..9e25c93d5df --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundCompileSpec.groovy @@ -0,0 +1,970 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.kotlin.processing.aop.simple.TestBinding +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.BeanDefinitionReference +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Issue +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AroundCompileSpec extends Specification { + + void 'test stereotype method level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.Around +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + val name : String = "test" + + @TestAnn2 + open fun test() { + + } + +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@TestAnn +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + + cleanup: + context.close() + } + + void 'test stereotype type level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.Around +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +@TestAnn2 +open class MyBean { + val name : String = "test" + open fun test() { + + } + +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.CLASS) +@TestAnn +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + + cleanup: + context.close() + } + + void 'test apply interceptor binder with annotation mapper'() { + given: + ApplicationContext context = buildContext(''' +package mapperbinding + +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) +annotation class TestAnn + + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context,'mapperbinding.MyBean') + def interceptor = getBean(context,'mapperbinding.TestInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + } + + void 'test apply interceptor binder with annotation mapper - plus members'() { + given: + ApplicationContext context = buildContext(''' +package mapperbindingmembers + +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + @TestAnn(num=1) + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS) +annotation class MyInterceptorBinding + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn(val num: Int) + +@Singleton +@TestAnn(num=1) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +@TestAnn(num=2) +class TestInterceptor2: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +''') + def instance = getBean(context, 'mapperbindingmembers.MyBean') + def interceptor = getBean(context, 'mapperbindingmembers.TestInterceptor') + def interceptor2 = getBean(context, 'mapperbindingmembers.TestInterceptor2') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !interceptor2.invoked + } + + void 'test method level interceptor matching'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + + } + + @TestAnn2 + open fun test2() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn2::class) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding2.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + when: + instance.test2() + + then: + anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test annotation with just interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean { + + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CLASS) +@InterceptorBinding +annotation class TestAnn + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding1.MyBean') + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + instance.test() + + expect:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + @PendingFeature(reason = "annotation defaults") + void 'test multiple interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package multiplebinding + +import io.micronaut.aop.* +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Singleton + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Deadly + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Fast + +@Retention +@InterceptorBinding(kind = InterceptorKind.AROUND) +annotation class Slow + +interface Missile { + fun fire() +} + +@Fast +@Deadly +@Singleton +open class FastAndDeadlyMissile: Missile { + override fun fire() { + } +} + +@Deadly +@Singleton +open class AnyDeadlyMissile: Missile { + override fun fire() { + } +} + +@Singleton +open class GuidedMissile: Missile { + @Slow + @Deadly + open fun lockAndFire() { + } + + @Fast + @Deadly + override fun fire() { + } +} + +@Slow +@Deadly +@Singleton +open class SlowMissile: Missile { + override fun fire() { + } +} + +@Fast +@Deadly +@Singleton +class MissileInterceptor: MethodInterceptor { + var intercepted = false + + override fun intercept(context: MethodInvocationContext): Any? { + intercepted = true + return context.proceed() + } +} + +@Slow +@Deadly +@Singleton +class LockInterceptor: MethodInterceptor { + var intercepted = false + + override fun intercept(context: MethodInvocationContext): Any? { + intercepted = true + return context.proceed() + } +} +''') + def missileInterceptor = getBean(context, 'multiplebinding.MissileInterceptor') + def lockInterceptor = getBean(context, 'multiplebinding.LockInterceptor') + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def guidedMissile = getBean(context, 'multiplebinding.GuidedMissile'); + guidedMissile.fire() + + then: + missileInterceptor.intercepted + !lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def fastAndDeadlyMissile = getBean(context, 'multiplebinding.FastAndDeadlyMissile'); + fastAndDeadlyMissile.fire() + + then: + missileInterceptor.intercepted + !lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def slowMissile = getBean(context, 'multiplebinding.SlowMissile'); + slowMissile.fire() + + then: + !missileInterceptor.intercepted + lockInterceptor.intercepted + + when: + missileInterceptor.intercepted = false + lockInterceptor.intercepted = false + def anyMissile = getBean(context, 'multiplebinding.AnyDeadlyMissile'); + anyMissile.fire() + + then: + missileInterceptor.intercepted + lockInterceptor.intercepted + + cleanup: + context.close() + } + + void 'test annotation with just interceptor binding - member binding'() { + given: + ApplicationContext context = buildContext(''' +package memberbinding + +import io.micronaut.aop.* +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Singleton + +@Singleton +@TestAnn(num=1, debug = false) +open class MyBean { + open fun test() { + } + + @TestAnn(num=2) // overrides binding on type + open fun test2() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@InterceptorBinding(bindMembers = true) +annotation class TestAnn(val num: Int, @get:NonBinding val debug: Boolean = false) + +@InterceptorBean(TestAnn::class) +@TestAnn(num = 1, debug = true) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn::class) +@TestAnn(num = 2) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'memberbinding.MyBean') + def interceptor = getBean(context, 'memberbinding.TestInterceptor') + def anotherInterceptor = getBean(context, 'memberbinding.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + when: + interceptor.invoked = false + instance.test2() + + then: + !interceptor.invoked + anotherInterceptor.invoked + + cleanup: + context.close() + } + + + void 'test annotation with just around'() { + given: + ApplicationContext context = buildContext(''' +package justaround + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean { + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CLASS) +@Around +annotation class TestAnn + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'justaround.MyBean') + def interceptor = getBean(context, 'justaround.TestInterceptor') + def anotherInterceptor = getBean(context, 'justaround.AnotherInterceptor') + instance.test() + + expect:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/5522') + void 'test Around annotation on private method fails'() { + when: + buildContext(''' +package around.priv.method + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + @TestAnn + private fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn +''') + + then: + Throwable t = thrown() + t.message.contains 'Method defines AOP advice but is declared final' + } + + void 'test byte[] return compile'() { + given: + ApplicationContext context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@jakarta.inject.Singleton +@Mutating("someVal") +open class MyBean { + + open fun test(someVal: ByteArray): ByteArray? { + return null + } +} +''') + def instance = getBean(context, 'test.MyBean') + + expect: + instance != null + + cleanup: + context.close() + } + + void 'compile simple AOP advice'() { + given: + BeanDefinition beanDefinition = buildInterceptedBeanDefinition('test.MyBean', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* + +@jakarta.inject.Singleton +@Mutating("someVal") +@TestBinding +open class MyBean { + open fun test() {} +} +''') + + BeanDefinitionReference ref = buildInterceptedBeanDefinitionReference('test.MyBean', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* + +@jakarta.inject.Singleton +@Mutating("someVal") +@TestBinding +open class MyBean { + open fun test() {} +} +''') + + def annotationMetadata = beanDefinition?.annotationMetadata + def values = annotationMetadata.getAnnotationValuesByType(InterceptorBinding) + + expect: + values.size() == 2 + values[0].stringValue().get() == Mutating.name + values[0].enumValue("kind", InterceptorKind).get() == InterceptorKind.AROUND + values[0].classValue("interceptorType").isPresent() + values[1].stringValue().get() == TestBinding.name + !values[1].classValue("interceptorType").isPresent() + values[1].enumValue("kind", InterceptorKind).get() == InterceptorKind.AROUND + beanDefinition != null + beanDefinition instanceof AdvisedBeanType + beanDefinition.interceptedType.name == 'test.MyBean' + ref in AdvisedBeanType + ref.interceptedType.name == 'test.MyBean' + } + + void 'test multiple annotations on a single method'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + @TestAnn2 + open fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class) +class TestInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@InterceptorBean(TestAnn2::class) +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding2.AnotherInterceptor') + + when: + instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked + anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test multiple annotations on an interceptor and method'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + @TestAnn2 + open fun test() { + + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class, TestAnn2::class) +class TestInterceptor: Interceptor { + var count = 0 + + override fun intercept(context: InvocationContext): Any? { + count++ + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then: + interceptor.count == 1 + + cleanup: + context.close() + } + + void 'test multiple annotations on an interceptor'() { + given: + ApplicationContext context = buildContext(''' +package annbinding2 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +open class MyBean { + + @TestAnn + open fun test() { + } + + @TestAnn2 + open fun test2() { + } + + @TestAnn + @TestAnn2 + open fun testBoth() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn + +@Retention +@Target(AnnotationTarget.FUNCTION) +@Around +annotation class TestAnn2 + +@InterceptorBean(TestAnn::class, TestAnn2::class) +class TestInterceptor: Interceptor { + var count = 0 + + override fun intercept(context: InvocationContext): Any? { + count++ + return context.proceed() + } +} +''') + def instance = getBean(context, 'annbinding2.MyBean') + def interceptor = getBean(context, 'annbinding2.TestInterceptor') + + when: + instance.test() + + then: + interceptor.count == 0 + + when: + instance.test2() + + then: + interceptor.count == 0 + + when: + instance.testBoth() + + then: + interceptor.count == 1 + + cleanup: + context.close() + } + + void "test validated on class with generics"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$BaseEntityService' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, """ +package test + +@io.micronaut.validation.Validated +open class BaseEntityService: BaseService() + +class BaseEntity + +abstract class BaseService: IBeanValidator { + override fun isValid(entity: T) = true +} + +interface IBeanValidator { + fun isValid(entity: T): Boolean +} +""") + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getTypeArguments('test.BaseService')[0].type.name == 'test.BaseEntity' + } + + void "test aop with generics"() { + ApplicationContext context = buildContext( ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.* +import jakarta.inject.Singleton + +@Singleton +open class Test { + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } +} +''', true) + def instance = getBean(context, 'test.Test') + + expect: + instance.testGenericsWithExtends("abc", 0) == "Name is changed" + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy new file mode 100644 index 00000000000..a28ca3cb6f6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/AroundConstructCompileSpec.groovy @@ -0,0 +1,746 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AroundConstructCompileSpec extends Specification { + + void 'test around construct with annotation mapper - plus members'() { + given: + ApplicationContext context = buildContext(''' +package aroundconstructmapperbindingmembers + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn2 +class MyBean @TestAnn(num=1) constructor() { + +} + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.ANNOTATION_CLASS) +annotation class MyInterceptorBinding + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn(val num: Int) + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.CLASS) +@MyInterceptorBinding +annotation class TestAnn2 + +@Singleton +@TestAnn(num=1) +class TestInterceptor: ConstructorInterceptor { + var invoked = false + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + return context.proceed() + } +} + +@Singleton +@TestAnn(num=2) +class TestInterceptor2: ConstructorInterceptor { + var invoked = false + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + return context.proceed() + } +} +''') + + + when: + def interceptor = getBean(context, 'aroundconstructmapperbindingmembers.TestInterceptor') + def interceptor2 = getBean(context, 'aroundconstructmapperbindingmembers.TestInterceptor2') + + then: + !interceptor.invoked + !interceptor2.invoked + + when: + def instance = getBean(context, 'aroundconstructmapperbindingmembers.MyBean') + + then:"the interceptor was invoked" + interceptor.invoked + !interceptor2.invoked + } + + void 'test around construct on type and constructor with proxy target + bind members'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND, bindMembers = true) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT, bindMembers = true) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND, bindMembers = true) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT, bindMembers = true) +@Around(proxyTarget = true) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + void 'test around construct on type and constructor with proxy target'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +@Around(proxyTarget = true) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + void 'test around construct on type and constructor'() { + given: + ApplicationContext context = buildContext(""" +package ctorbinding + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@FooClassBinding +@Singleton +open class Foo @FooCtorBinding constructor() { +} + +@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooCtorBinding + +@Target(AnnotationTarget.CLASS) +@Retention +@MustBeDocumented +@InterceptorBinding(kind = InterceptorKind.AROUND) +@InterceptorBinding(kind = InterceptorKind.AROUND_CONSTRUCT) +annotation class FooClassBinding + +@Singleton +@FooClassBinding +class Interceptor1: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} + +@Singleton +@FooCtorBinding +class Interceptor2: ConstructorInterceptor { + var intercepted = false + + override fun intercept(context: ConstructorInvocationContext): Any { + intercepted = true + return context.proceed() + } +} +""") + when: + def i1 = getBean(context, 'ctorbinding.Interceptor1') + def i2 = getBean(context, 'ctorbinding.Interceptor2') + + then: + !i1.intercepted + !i2.intercepted + + when: + def bean = getBean(context, 'ctorbinding.Foo') + + then: + i1.intercepted + i2.intercepted + + cleanup: + context.close() + } + + @Unroll + void 'test around construct with around interception - proxyTarget = #proxyTarget'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean(private val env: io.micronaut.context.env.Environment) { + + open fun test() { + } +} + +@io.micronaut.context.annotation.Factory +open class MyFactory { + + @TestAnn + @Singleton + open fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Around(proxyTarget=$proxyTarget) +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBean(TestAnn::class) +class TypeSpecificConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): MyBean { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def typeSpecificInterceptor = getBean(context, 'annbinding1.TypeSpecificConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + constructorInterceptor.invoked + typeSpecificInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + typeSpecificInterceptor.invoked = false + instance.test() + + then:"the methods interceptor are invoked" + instance instanceof Intercepted + interceptor.invoked + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + !typeSpecificInterceptor.invoked + + when:"A bean that is created from a factory is instantiated" + constructorInterceptor.invoked = false + interceptor.invoked = false + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"Constructor interceptors are invoked for the created instance" + constructorInterceptor.invoked + !typeSpecificInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other interceptors are not" + !interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + + where: + proxyTarget << [true, false] + } + + void 'test around construct without around interception'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +open class MyBean(private val env: io.micronaut.context.env.Environment) { + + open fun test() { + } +} + +@io.micronaut.context.annotation.Factory +open class MyFactory { + + @TestAnn + @Singleton + open fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + instance.test() + + then:"the methods interceptor are invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + + when:"A bean that is created from a factory is instantiated" + constructorInterceptor.invoked = false + interceptor.invoked = false + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"Constructor interceptors are invoked for the created instance" + !(factoryCreatedInstance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + and:"Other interceptors are not" + !interceptor.invoked + !anotherInterceptor.invoked + + cleanup: + context.close() + } + + void 'test around construct declared on constructor only'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +class MyBean @TestAnn constructor(env: io.micronaut.context.env.Environment) { + + fun test() { + } +} + +@Retention +@Target(AnnotationTarget.CONSTRUCTOR) +@AroundConstruct +@Around +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +""") + when: + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + + then: + !constructorInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + + cleanup: + context.close() + } + + void 'test around construct without around interception - interceptors from factory'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +class MyBean(env: io.micronaut.context.env.Environment) { + + fun test() { + } +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@AroundConstruct +annotation class TestAnn + +@Factory +class InterceptorFactory { + var aroundConstructInvoked = false + + @InterceptorBean(TestAnn::class) + fun aroundIntercept(): ConstructorInterceptor { + return ConstructorInterceptor { context -> + this.aroundConstructInvoked = true + context.proceed() + } + } +} +""") + when: + def factory = getBean(context, 'annbinding1.InterceptorFactory') + + then: + !factory.aroundConstructInvoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + !(instance instanceof Intercepted) + factory.aroundConstructInvoked + + cleanup: + context.close() + } + + void 'test around construct with introduction advice'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@Singleton +@TestAnn +abstract class MyBean(env: io.micronaut.context.env.Environment) { + abstract fun test(): String +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Introduction +@AroundConstruct +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestConstructInterceptor: ConstructorInterceptor { + var invoked = false + var parameters: Array? = null + + override fun intercept(context: ConstructorInvocationContext): Any { + invoked = true + parameters = context.parameterValues + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return "good" + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = false + + override fun intercept(context: InvocationContext): Any? { + invoked = true + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.TestConstructInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !constructorInterceptor.invoked + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A bean that features constructor injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The constructor interceptor is invoked" + instance instanceof Intercepted + constructorInterceptor.invoked + constructorInterceptor.parameters.size() == 1 + constructorInterceptor.parameters[0] instanceof Environment + + and:"Other non-constructor interceptors are not invoked" + !interceptor.invoked + !anotherInterceptor.invoked + + when:"A method with interception is invoked" + constructorInterceptor.invoked = false + def result = instance.test() + + then:"the methods interceptor are invoked" + interceptor.invoked + result == 'good' + !anotherInterceptor.invoked + + and:"The constructor interceptor is not" + !constructorInterceptor.invoked + + cleanup: + context.close() + } + +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy new file mode 100644 index 00000000000..623e6463137 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ExecutableFactoryMethodSpec.groovy @@ -0,0 +1,107 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.BeanDefinition +import reactor.core.publisher.Flux +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableFactoryMethodSpec extends Specification { + + void "test executing a default interface method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyFactory$MyClass0', ''' +package test + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +interface SomeInterface { + + fun goDog(): String + + fun go(): String { + return "go" + } +} + +@Factory +class MyFactory { + + @Singleton + @Executable + fun myClass(): MyClass { + return MyClass() + } +} + +class MyClass: SomeInterface { + + override fun goDog(): String{ + return "go" + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + Object instance = beanDefinition.class.classLoader.loadClass('test.MyClass').newInstance() + + then: + beanDefinition.findMethod("go").get().invoke(instance) == "go" + beanDefinition.findMethod("goDog").get().invoke(instance) == "go" + } + + void "test executable factory with multiple interface inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyFactory$MyClient0', """ +package test + +import reactor.core.publisher.Flux +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton +import org.reactivestreams.Publisher + +@Factory +class MyFactory { + + @Singleton + @Executable + fun myClient(): MyClient? { + return null + } +} + +interface HttpClient { + fun retrieve(): Publisher<*> +} +interface StreamingHttpClient: HttpClient { + fun stream(): Publisher +} +interface ReactorHttpClient: HttpClient { + override fun retrieve(): Flux<*> +} +interface ReactorStreamingHttpClient: StreamingHttpClient, ReactorHttpClient { + override fun stream(): Flux +} +interface MyClient: ReactorStreamingHttpClient { + fun blocking(): ByteArray +} +""") + + then: + noExceptionThrown() + beanDefinition != null + def retrieveMethod = beanDefinition.getRequiredMethod("retrieve") + def blockingMethod = beanDefinition.getRequiredMethod("blocking") + def streamMethod = beanDefinition.getRequiredMethod("stream") + retrieveMethod.returnType.type == Flux.class + streamMethod.returnType.type == Flux.class + retrieveMethod.returnType.typeParameters.length == 1 + retrieveMethod.returnType.typeParameters[0].type == Object.class + streamMethod.returnType.typeParameters[0].type == byte[].class + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy new file mode 100644 index 00000000000..57bde1f14b5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/FinalModifierSpec.groovy @@ -0,0 +1,243 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.compile + +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.aop.Intercepted +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class FinalModifierSpec extends Specification { + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2530') + void 'test final modifier on external class produced by factory'() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* +import com.fasterxml.jackson.databind.ObjectMapper + +@Factory +class MyBeanFactory { + + @Mutating("someVal") + @jakarta.inject.Singleton + @jakarta.inject.Named("myMapper") + fun myMapper(): ObjectMapper { + return ObjectMapper() + } + +} + +''') + then: + context.getBean(ObjectMapper, Qualifiers.byName("myMapper")) instanceof Intercepted + + cleanup: + context.close() + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited public method"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + fun getContext(): Any { + return Object() + } +} + +interface CountryRepository + +@jakarta.inject.Singleton +@Mutating("someVal") +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + open fun someMethod(): String { + return "test"; + } +} +''') + then:"Compilation passes" + definition != null + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited protected method"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + + protected fun getContext(): Any { + return Object() + } +} + +interface CountryRepository + +@jakarta.inject.Singleton +@Mutating("someVal") +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + open fun someMethod(): String { + return "test" + } +} +''') + then:"Compilation passes" + definition != null + } + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/2479') + void "test final modifier on inherited protected method - 2"() { + when: + def definition = buildBeanDefinition('test.CountryRepositoryImpl', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +abstract class BaseRepositoryImpl { + protected fun getContext(): Any { + return Object() + } +} + +interface CountryRepository { + @Mutating("someVal") + fun someMethod(): String +} + +@jakarta.inject.Singleton +open class CountryRepositoryImpl: BaseRepositoryImpl(), CountryRepository { + + override fun someMethod(): String { + return "test" + } +} +''') + then:"Compilation passes" + definition != null + } + + void "test final modifier on factory with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.MyBeanFactory', ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Factory +class MyBeanFactory { + + @Mutating("someVal") + @jakarta.inject.Singleton + fun myBean(): MyBean { + return MyBean() + } + +} + +class MyBean +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Cannot apply AOP advice to final class. Class must be made non-final to support proxying: test.MyBean' + } + + void "test final modifier on class with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Mutating("someVal") +@jakarta.inject.Singleton +class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + open fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Cannot apply AOP advice to final class. Class must be made non-final to support proxying: test.MyBean' + } + + void "test final modifier on method with AOP advice doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Public method inherits AOP advice but is declared final.' + } + + void "test final modifier on method with AOP advice on method doesn't compile"() { + when: + buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class MyBean(@Value("\\${foo.bar}") private val myValue: String) { + + @Mutating("someVal") + fun someMethod(): String { + return myValue + } +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains 'Method defines AOP advice but is declared final. Change the method to be non-final in order for AOP advice to be applied.' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy new file mode 100644 index 00000000000..0660f8b4977 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/GeneratedAnnotationSpec.groovy @@ -0,0 +1,54 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.writer.BeanDefinitionWriter +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class GeneratedAnnotationSpec extends Specification { + + @Issue('https://github.com/micronaut-projects/micronaut-core/issues/4127') + void 'test only 1 generated annotation is added'() { + when: + def bytes = getClassBytes('example.FooController' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package example + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.validation.Validated + +@Validated +@Controller("/") +open class FooController { + + @Get + open fun foo(): String { + return "" + } +} +''') + then: + bytes != null + + when: + ClassReader reader = new ClassReader(bytes) + int generatedAnnotations = 0 + reader.accept(new ClassVisitor(Opcodes.ASM5) { + @Override + AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + if (descriptor.contains("Generated")) { + generatedAnnotations++ + } + return super.visitAnnotation(descriptor, visible) + } + },ClassReader.SKIP_CODE) + + then:"Only one generated annotation is added" + generatedAnnotations == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy new file mode 100644 index 00000000000..369306c62a1 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/InheritedAnnotationMetadataSpec.groovy @@ -0,0 +1,157 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Blocking +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritedAnnotationMetadataSpec extends Specification { + + void "test that annotation metadata is inherited from overridden methods for introduction advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.Blocking +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Stub +@jakarta.inject.Singleton +interface MyBean: MyInterface { + override fun someMethod(): String +} + +interface MyInterface { + @Blocking + @Executable + fun someMethod(): String +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 1 + beanDefinition.executableMethods[0].hasAnnotation(Blocking) + !beanDefinition.executableMethods[0].hasDeclaredAnnotation(Blocking) + } + + void "test that annotation metadata is inherited from overridden methods for around advice"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Blocking + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean(@Value("\\${foo.bar}") private val myValue: String): MyInterface { + + override fun someMethod(): String { + return myValue + } +} + +interface MyInterface { + @Blocking + @Executable + fun someMethod(): String +} +''') + then: + beanDefinition != null + !beanDefinition.isAbstract() + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 1 + beanDefinition.executableMethods[0].hasAnnotation(Blocking) + + when: + def context = ApplicationContext.run('foo.bar':'test') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + instance.someMethod() == 'test' + } + + void "test that a bean definition is not created for an abstract class"() { + when: + ApplicationContext ctx = buildContext(''' +package test + +import io.micronaut.aop.* +import io.micronaut.context.annotation.* +import io.micronaut.core.annotation.* +import io.micronaut.core.order.Ordered +import jakarta.inject.Singleton + +interface ContractService { + + @SomeAnnot + fun interfaceServiceMethod() +} + +abstract class BaseService { + + @SomeAnnot + open fun baseServiceMethod() {} +} + +@SomeAnnot +abstract class BaseAnnotatedService + +@Singleton +open class Service: BaseService(), ContractService { + + @SomeAnnot + open fun serviceMethod() {} + + override fun interfaceServiceMethod() {} +} + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Around +@Type(SomeInterceptor::class) +annotation class SomeAnnot + +@Singleton +class SomeInterceptor: MethodInterceptor, Ordered { + + override fun intercept(context: MethodInvocationContext): Any? { + return context.proceed() + } +} +''') + then: + Class clazz = ctx.classLoader.loadClass("test.ContractService") + ctx.getBean(clazz) + + when: + ctx.classLoader.loadClass("test.\$BaseService" + BeanDefinitionWriter.CLASS_SUFFIX) + + then: + thrown(ClassNotFoundException) + + when: + ctx.classLoader.loadClass("test.\$BaseService" + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX) + + then: + thrown(ClassNotFoundException) + + when: + ctx.classLoader.loadClass("test.\$BaseAnnotatedService" + BeanDefinitionWriter.CLASS_SUFFIX) + + then: + thrown(ClassNotFoundException) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy new file mode 100644 index 00000000000..ecc10da9d20 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionAnnotationSpec.groovy @@ -0,0 +1,137 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.exceptions.UnimplementedAdviceException +import io.micronaut.context.BeanContext +import io.micronaut.inject.AdvisedBeanType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.kotlin.processing.aop.introduction.NotImplementedAdvice +import spock.lang.Specification + +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionAnnotationSpec extends Specification { + + void 'test unimplemented introduction advice'() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.NotImplemented + +@NotImplemented +interface MyBean { + fun test() +} +''') + def context = BeanContext.run() + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + when: + bean.test() + + then: + beanDefinition instanceof AdvisedBeanType + beanDefinition.interceptedType.name == 'test.MyBean' + thrown(UnimplementedAdviceException) + + cleanup: + context.close() + } + + void 'test unimplemented introduction advice on abstract class with concrete methods'() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.NotImplemented +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@NotImplemented +abstract class MyBean { + + abstract fun test() + + fun test2(): String { + return "good" + } + + @Mutating("arg") + open fun test3(arg: String): String { + return arg + } +} +''') + def context = BeanContext.run() + def bean = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + def notImplementedAdvice = context.getBean(NotImplementedAdvice) + + when: + bean.test() + + then: + thrown(UnimplementedAdviceException) + notImplementedAdvice.invoked + + when: + notImplementedAdvice.invoked = false + + then: + bean.test2() == 'good' + bean.test3() == 'changed' + !notImplementedAdvice.invoked + + cleanup: + context.close() + } + + void "test @Min annotation"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.Executable +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +interface MyInterface{ + @Executable + fun save(@NotBlank name: String, @Min(1L) age: Int) + + @Executable + fun saveTwo(@Min(1L) name: String) +} + + +@Stub +@jakarta.inject.Singleton +interface MyBean: MyInterface +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + + def saveMethod = beanDefinition.executableMethods.find {it.methodName == 'save'} + saveMethod != null + saveMethod.returnType.type == void.class + saveMethod.arguments[0].getAnnotationMetadata().hasAnnotation(NotBlank) + saveMethod.arguments[1].getAnnotationMetadata().hasAnnotation(Min) + saveMethod.arguments[1].getAnnotationMetadata().getValue(Min, Integer).get() == 1 + + + def saveTwoMethod = beanDefinition.executableMethods.find {it.methodName == 'saveTwo'} + saveTwoMethod != null + saveTwoMethod.methodName == 'saveTwo' + saveTwoMethod.returnType.type == void.class + saveTwoMethod.arguments[0].getAnnotationMetadata().hasAnnotation(Min) + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy new file mode 100644 index 00000000000..a8e8394bcdb --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionCompileSpec.groovy @@ -0,0 +1,110 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionCompileSpec extends Specification { + + void 'test coroutine repository'() { + given: + def context = KotlinCompiler.buildContext(''' +package test + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +import kotlinx.coroutines.flow.Flow + +class SomeEntity + +interface CoroutineCrudRepository { + + suspend fun save(entity: S): S + suspend fun update(entity: S): S + fun updateAll(entities: Iterable): Flow + fun saveAll(entities: Iterable): Flow + suspend fun findById(id: ID): E? + suspend fun existsById(id: ID): Boolean + fun findAll(): Flow + suspend fun count(): Long + suspend fun deleteById(id: ID): Int + suspend fun delete(entity: E): Int + suspend fun deleteAll(entities: Iterable): Int + suspend fun deleteAll(): Int +} + +@MyRepository +interface CustomRepository : CoroutineCrudRepository { + + // As of Kotlin version 1.7.20 and KAPT, this will generate JVM signature: "SomeEntity findById(long id, continuation)" + override suspend fun findById(id: Long): SomeEntity? + + suspend fun xyz(): String + + suspend fun abc(): String + + suspend fun count1(): String + + suspend fun count2(): String + +} + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class MyRepository +''') + def definition = getBeanDefinition(context, 'test.CustomRepository') + + expect: + definition != null + + cleanup: + context.close() + } + + void 'test apply introduction advise with interceptor binding'() { + given: + ApplicationContext context = buildContext(''' +package introductiontest + +import io.micronaut.aop.* +import jakarta.inject.Singleton + +@TestAnn +interface MyBean { + fun test(): Int +} + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Introduction +annotation class TestAnn + +@InterceptorBean(TestAnn::class) +class StubIntroduction: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any { + invoked++ + return 10 + } +} +''') + def instance = getBean(context, 'introductiontest.MyBean') + def interceptor = getBean(context, 'introductiontest.StubIntroduction') + + when: + def result = instance.test() + + then:"the interceptor was invoked" + instance instanceof Intercepted + interceptor.invoked == 1 + result == 10 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy new file mode 100644 index 00000000000..ffb47a937bd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionGenericTypesSpec.groovy @@ -0,0 +1,126 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.DefaultBeanContext +import io.micronaut.core.type.ReturnType +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionGenericTypesSpec extends Specification { + + void "test that generic return types are correct when implementing an interface with type arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.* +import java.net.URL + +interface MyInterface { + + fun getURL(): T + + fun getURLs(): List +} + + +@Stub +@jakarta.inject.Singleton +@Executable +interface MyBean: MyInterface + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + + beanDefinition.getRequiredMethod("getURL").targetMethod.returnType == URL + beanDefinition.getRequiredMethod("getURL").returnType.type == URL + beanDefinition.getRequiredMethod("getURLs").returnType.type == List + beanDefinition.getRequiredMethod("getURLs").returnType.asArgument().hasTypeVariables() + beanDefinition.getRequiredMethod("getURLs").returnType.asArgument().typeVariables['E'].type == URL + } + + void "test that generic return types are correct when implementing an interface with type arguments 2"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.context.annotation.* +import java.net.URL + +interface MyInterface { + + fun getPeopleSingle(): reactor.core.publisher.Mono> + + fun getPerson(): T + + fun getPeople(): List + + fun save(person: T) + + fun saveAll(person: List) + + fun getPeopleArray(): Array + + fun getPeopleListArray(): List> + + fun getPeopleMap(): Map +} + +@Stub +@jakarta.inject.Singleton +@Executable +interface MyBean: MyInterface + +open class Person + +class SubPerson: Person() + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + returnType(beanDefinition, "getPerson").type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeople").type == List + returnType(beanDefinition, "getPeople").asArgument().hasTypeVariables() + returnType(beanDefinition, "getPeople").asArgument().typeVariables['E'].type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeopleMap").typeVariables['K'].type.name == 'test.SubPerson' + returnType(beanDefinition, "getPeopleMap").typeVariables['V'].type == URL + returnType(beanDefinition, "getPeopleArray").type.isArray() + returnType(beanDefinition, "getPeopleArray").type.name.contains('test.SubPerson') + returnType(beanDefinition, "getPeopleListArray").type == List + returnType(beanDefinition, "getPeopleListArray").typeVariables['E'].type.isArray() + beanDefinition.findPossibleMethods("save").findFirst().get().targetMethod != null + beanDefinition.findPossibleMethods("getPerson").findFirst().get().targetMethod != null + def getPeopleSingle = returnType(beanDefinition, "getPeopleSingle") + getPeopleSingle.typeVariables['T'].type== List + getPeopleSingle.typeVariables['T'].typeVariables['E'].type.name == 'test.SubPerson' + + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + + then:"the methods are invocable" + instance.getPerson() == null + instance.getPeople() == null + instance.getPeopleArray() == null + instance.getPeopleSingle() == null + instance.save(null) == null + instance.saveAll([]) == null + } + + ReturnType returnType(BeanDefinition bd, String name) { + bd.findPossibleMethods(name).findFirst().get().returnType + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy new file mode 100644 index 00000000000..6eb9827cc0c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionInnerInterfaceSpec.groovy @@ -0,0 +1,39 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionInnerInterfaceSpec extends Specification { + + void 'test that an inner interface with introduction doesnt create advise for outer class'() { + given: + def clsName = 'inneritfce.Test' + def context = buildContext(''' +package inneritfce + +import jakarta.inject.Singleton +import io.micronaut.kotlin.processing.aop.introduction.Stub + +@Singleton +class Test { + + @Stub + interface InnerIntroduction +} +''') + when: + def bean = getBean(context, clsName) + + then:'outer bean is not AOP advice' + !(bean instanceof Intercepted) + + when: + context.classLoader.loadClass(clsName + BeanDefinitionVisitor.PROXY_SUFFIX) + + then:'proxy not generated for outer type' + thrown(ClassNotFoundException) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy new file mode 100644 index 00000000000..c9c8d5625df --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/IntroductionWithAroundSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionWithAroundSpec extends Specification { + + void "test that around advice is applied to introduction concrete methods"() { + when:"An introduction advice type is compiled that includes a concrete method that is annotated with around advice" + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.introduction.Stub +import io.micronaut.kotlin.processing.aop.simple.Mutating +import javax.validation.constraints.* +import jakarta.inject.Singleton + +@Stub +@Singleton +abstract class MyBean { + abstract fun save(@NotBlank name: String, @Min(1L) age: Int) + abstract fun saveTwo(@Min(1L) name: String) + + @Mutating("name") + open fun myConcrete(name: String): String { + return name + } +} + +''') + + then:"The around advice is applied to the concrete method" + beanDefinition != null + + when: + ApplicationContext context = ApplicationContext.run() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.myConcrete("test") == 'changed' + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy new file mode 100644 index 00000000000..953a113e907 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxySpec.groovy @@ -0,0 +1,168 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class LifeCycleWithProxySpec extends Specification { + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at class level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.env.Environment +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.conversionService != null + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } + + void "test that a simple AOP definition lifecycle hooks are invoked - annotation at method level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + + then: + instance.conversionService != null + instance.someMethod() == 'good' + instance.count == 1 + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy new file mode 100644 index 00000000000..7c5bfb505cf --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/LifeCycleWithProxyTargetSpec.groovy @@ -0,0 +1,147 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class LifeCycleWithProxyTargetSpec extends Specification { + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at class level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@Mutating("someVal") +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + when: + def context = ApplicationContext.builder(beanDefinition.class.classLoader).start() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then:"proxy post construct methods are not invoked" + instance.conversionService // injection works + instance.someMethod() == 'good' + instance.count == 0 + + and:"proxy target post construct methods are invoked" + instance.interceptedTarget().count == 1 + + cleanup: + context.close() + } + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level with hooks last"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + beanDefinition.postConstructMethods.size() == 1 + beanDefinition.preDestroyMethods.size() == 1 + + } + + void "test that a proxy target AOP definition lifecycle hooks are invoked - annotation at method level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyBean' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionWriter.PROXY_SUFFIX, ''' +package test; + +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import io.micronaut.core.convert.ConversionService + +@jakarta.inject.Singleton +open class MyBean { + + @jakarta.inject.Inject + lateinit var conversionService: ConversionService + + var count = 0 + + @jakarta.annotation.PostConstruct + fun created() { + count++ + } + + @jakarta.annotation.PreDestroy + fun destroyed() { + count-- + } + + @Mutating("someVal") + open fun someMethod(): String { + return "good" + } + +} +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy new file mode 100644 index 00000000000..47493de217f --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/PostConstructInterceptorCompileSpec.groovy @@ -0,0 +1,293 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import spock.lang.PendingFeature +import spock.lang.Specification +import spock.lang.Unroll +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class PostConstructInterceptorCompileSpec extends Specification { + + @Unroll + void 'test post construct with around interception - proxyTarget = #proxyTarget'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.* +import jakarta.annotation.PostConstruct + +@Singleton +@TestAnn +open class MyBean(env: io.micronaut.context.env.Environment) { + + @Inject lateinit var env: io.micronaut.context.env.Environment + + var invoked = 0 + + open fun test() { + } + + @PostConstruct + fun init() { + println("INVOKED POST CONSTRUCT") + invoked++ + } +} + +@io.micronaut.context.annotation.Factory +class MyFactory { + + @TestAnn + @Singleton + fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +open class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Around(proxyTarget=$proxyTarget) +@InterceptorBinding(kind=InterceptorKind.POST_CONSTRUCT) +@InterceptorBinding(kind=InterceptorKind.PRE_DESTROY) +annotation class TestAnn + + +@Singleton +@InterceptorBean(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.POST_CONSTRUCT) +class PostConstructTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.PRE_DESTROY) +class PreDestroyTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any? { + invoked++ + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.PostConstructTestInterceptor') + def destroyInterceptor = getBean(context, 'annbinding1.PreDestroyTestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !interceptor.invoked + !anotherInterceptor.invoked + !constructorInterceptor.invoked + + when:"A bean that featuring post construct injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The interceptors that apply to post construction are invoked" + (proxyTarget ? instance.interceptedTarget() : instance).invoked == 1 + interceptor.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 0 + + + when:"A method with interception is invoked" + instance.test() + + then:"the methods interceptor are invoked" + instance instanceof Intercepted + interceptor.invoked == 2 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + + + when:"A bean that is created from a factory is instantiated" + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"post construct interceptors are invoked for the created instance" + interceptor.invoked == 3 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + + when: + context.stop() + + then: + // TODO: Discuss why we are invoking destroy hooks for proxies + interceptor.invoked == proxyTarget ? 6 : 5 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + // TODO: Discuss why we are invoking destroy hooks for proxies + destroyInterceptor.invoked == proxyTarget ? 3 : 2 + + + where: + proxyTarget << [true, false] + } + + void 'test post construct & pre destroy without around interception'() { + given: + ApplicationContext context = buildContext(""" +package annbinding1 + +import io.micronaut.aop.* +import jakarta.inject.* +import jakarta.annotation.PostConstruct + +@Singleton +@TestAnn +open class MyBean(env: io.micronaut.context.env.Environment) { + + @Inject lateinit var env: io.micronaut.context.env.Environment + + var invoked = 0 + + open fun test() { + } + + @PostConstruct + fun init() { + println("INVOKED POST CONSTRUCT") + invoked++ + } +} + +@io.micronaut.context.annotation.Factory +class MyFactory { + + @TestAnn + @Singleton + fun test(env: io.micronaut.context.env.Environment): MyOtherBean { + return MyOtherBean() + } +} + +class MyOtherBean + +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@InterceptorBinding(kind=InterceptorKind.POST_CONSTRUCT) +@InterceptorBinding(kind=InterceptorKind.PRE_DESTROY) +annotation class TestAnn + +@Singleton +@InterceptorBean(TestAnn::class) +class TestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.POST_CONSTRUCT) +class PostConstructTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +@InterceptorBinding(value=TestAnn::class, kind=InterceptorKind.PRE_DESTROY) +class PreDestroyTestInterceptor: MethodInterceptor { + var invoked = 0 + + override fun intercept(context: MethodInvocationContext): Any? { + invoked++ + return context.proceed() + } +} + +@Singleton +class AnotherInterceptor: Interceptor { + var invoked = 0 + override fun intercept(context: InvocationContext): Any? { + invoked++ + return context.proceed() + } +} +""") + when: + def interceptor = getBean(context, 'annbinding1.TestInterceptor') + def constructorInterceptor = getBean(context, 'annbinding1.PostConstructTestInterceptor') + def destroyInterceptor = getBean(context, 'annbinding1.PreDestroyTestInterceptor') + def anotherInterceptor = getBean(context, 'annbinding1.AnotherInterceptor') + + then: + !interceptor.invoked + !anotherInterceptor.invoked + !constructorInterceptor.invoked + + when:"A bean that featuring post construct injection is instantiated" + def instance = getBean(context, 'annbinding1.MyBean') + + then:"The interceptors that apply to post construction are invoked" + interceptor.invoked == 1 + instance.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 0 + + + when:"A method with interception is invoked" + instance.test() + + then:"the methods interceptor are invoked" + interceptor.invoked == 1 + constructorInterceptor.invoked == 1 + anotherInterceptor.invoked == 0 + + + when:"A bean that is created from a factory is instantiated" + def factoryCreatedInstance = getBean(context, 'annbinding1.MyOtherBean') + + then:"post construct interceptors are invoked for the created instance" + interceptor.invoked == 2 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + + when: + context.stop() + + then: + interceptor.invoked == 4 + constructorInterceptor.invoked == 2 + anotherInterceptor.invoked == 0 + destroyInterceptor.invoked == 2 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy new file mode 100644 index 00000000000..2a60ee60047 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/compile/ValidatedNonBeanSpec.groovy @@ -0,0 +1,33 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ValidatedNonBeanSpec extends Specification { + + void "test a class with only a validation annotation is not a bean"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.DefaultContract", """ +package test + +import javax.validation.constraints.NotNull +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +class DefaultContract: Contract { + + override fun parseLong(@NotNull sequence: CharSequence): Long { + return 0L + } +} + +interface Contract { + fun parseLong(@NotNull sequence: CharSequence): Long +} + +""") + then: + beanDefinition == null + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy new file mode 100644 index 00000000000..e2978af1e36 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnConcreteClassFactorySpec.groovy @@ -0,0 +1,76 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class AdviceDefinedOnConcreteClassFactorySpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ConcreteClass foo = beanContext.getBean(ConcreteClass, Qualifiers.byName("another")) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ConcreteClass foo = beanContext.getBean(ConcreteClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy new file mode 100644 index 00000000000..a84ac473bb7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnFactorySpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import io.micronaut.inject.writer.BeanDefinitionWriter +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AdviceDefinedOnFactorySpec extends Specification { + + void "test advice defined at the class level of a factory"() { + when:"Advice is defined at the class level of the factory" + BeanDefinition beanDefinition = buildBeanDefinition('test.$MyFactory' + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +@Mutating("name") +open class MyFactory { + + @Bean + @Executable + open fun myBean(@Parameter name: String): String { + return name + } +} + +''') + then:"The methods of the factory have AOP advice applied, but not the created beans" + beanDefinition.executableMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy new file mode 100644 index 00000000000..a3146f90715 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/factory/AdviceDefinedOnInterfaceFactorySpec.groovy @@ -0,0 +1,121 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.core.reflect.ReflectionUtils +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import org.hibernate.SessionFactory +import spock.lang.Specification +import spock.lang.Unroll + +class AdviceDefinedOnInterfaceFactorySpec extends Specification { + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass, Qualifiers.byName("another")) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + + void "test session factory proxy"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + BeanDefinition beanDefinition = beanContext.findBeanDefinition(SessionFactory).get() + SessionFactory sessionFactory = beanContext.getBean(SessionFactory) + + // make sure all the public method are implemented + def clazz = sessionFactory.getClass() + int count = 1 // proxy methods + def interfaces = ReflectionUtils.getAllInterfaces(SessionFactory.class) + interfaces += SessionFactory.class + for(i in interfaces) { + for(m in i.declaredMethods) { + count++ + assert clazz.getDeclaredMethod(m.name, m.parameterTypes) + } + } + + then: + sessionFactory instanceof Intercepted + } + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + + when: + InterfaceClass foo = beanContext.getBean(InterfaceClass) + InterfaceClass another = beanContext.getBean(InterfaceClass, Qualifiers.byName("another")) + + then: + foo instanceof Intercepted + another instanceof Intercepted + // should not be a reflection based method + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy new file mode 100644 index 00000000000..6122a439ed9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/hotswap/ProxyHotswapSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.aop.hotswap + +import io.micronaut.aop.HotSwappableInterceptedProxy +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class ProxyHotswapSpec extends Specification { + + void "test AOP setup attributes"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + def newInstance = new HotswappableProxyingClass() + + when: + HotswappableProxyingClass foo = beanContext.getBean(HotswappableProxyingClass) + then: + foo instanceof HotSwappableInterceptedProxy + foo.interceptedTarget().getClass() == HotswappableProxyingClass + foo.test("test") == "Name is changed" + foo.test2("test") == "Name is test" + foo.interceptedTarget().invocationCount == 2 + + foo.swap(newInstance) + foo.interceptedTarget().invocationCount == 0 + foo.interceptedTarget() != foo + foo.interceptedTarget().is(newInstance) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..5230cf95b57 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/AbstractClassIntroductionAdviceSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll + +class AbstractClassIntroductionAdviceSpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + AbstractClass foo = beanContext.getBean(AbstractClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "changed" // test for single string arg + 'nonAbstract' | ['test'] | "changed" // test for single string arg + 'test' | ['test', 10] | "changed" // test for multiple args, one primitive + 'testGenericsFromType' | ['test', 10] | "changed" // test for multiple args, one primitive + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..b2b5a33b198 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionAdviceSpec.groovy @@ -0,0 +1,55 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InterfaceIntroductionAdviceSpec extends Specification { + + @Unroll + void "test AOP method invocation @Named bean for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceIntroductionClass foo = beanContext.getBean(InterfaceIntroductionClass) + + expect: + foo instanceof Intercepted + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "changed" // test for single string arg + 'test' | ['test', 10] | "changed" // test for multiple args, one primitive + 'testGenericsFromType' | ['test', 10] | "changed" // test for multiple args, one primitive + } + + void "test injecting an introduction advice with generics"() { + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InjectParentInterface foo = beanContext.getBean(InjectParentInterface) + + then: + noExceptionThrown() + + cleanup: + beanContext.close() + } + + void "test typeArgumentsMap are created for introduction advice"() { + def definition = buildBeanDefinition("test.Test\$Intercepted", """ +package test + +import io.micronaut.kotlin.processing.aop.introduction.* + +@Stub +interface Test: ParentInterface> +""") + + expect: + !definition.getTypeArguments(ParentInterface).isEmpty() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy new file mode 100644 index 00000000000..8578aba07f7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionAdviceWithNewInterfaceSpec.groovy @@ -0,0 +1,281 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionAdviceWithNewInterfaceSpec extends Specification { + + void "test configuration advice with Kotlin properties"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("test") +interface MyBean { + val foo : String +} + +''', true, ['test.foo':'test']) + def bean = getBean(context, 'test.MyBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test configuration advice with Kotlin properties inner classes"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("test") +interface MyBean { + val foo : String + + @ConfigurationProperties("more") + interface InnerBean { + val foo : String + } +} + +''', true, ['test.more.foo':'test']) + def bean = getBean(context, 'test.MyBean$InnerBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test introduction advice with Kotlin properties"() { + when: + def context = buildContext(''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@Stub("test") +interface MyBean { + val foo : String +} + +''', true) + def bean = getBean(context, 'test.MyBean') + + then: + bean.foo == 'test' + + cleanup: + context.close() + } + + void "test introduction advice with primitive generics"() { + when: + def context = buildContext( ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import javax.validation.constraints.NotNull + +@RepoDef +interface MyRepo : DeleteByIdCrudRepo { + + override fun deleteById(@NotNull id: Int) +} + + +''', true) + + def bean = + getBean(context, 'test.MyRepo') + then: + bean != null + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on concrete classes"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +open class MyBean { + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + beanDefinition.findMethod("getFoo").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on abstract classes"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +abstract class MyBean { + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 2 + beanDefinition.findMethod("getFoo").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + } + + void "test that it is possible for @Introduction advice to implement additional interfaces on interfaces"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +interface MyBean { + + @Executable + fun getBar(): String + + @Executable + fun getFoo() = "good" +} + +''') + then: + !beanDefinition.isAbstract() + beanDefinition != null + ApplicationEventListener.class.isAssignableFrom(beanDefinition.beanType) + beanDefinition.injectedFields.size() == 0 + beanDefinition.executableMethods.size() == 3 + beanDefinition.findMethod("getBar").isPresent() + beanDefinition.findMethod("onApplicationEvent", Object).isPresent() + + when: + def context = BeanContext.run() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + ListenerAdviceInterceptor listenerAdviceInterceptor= context.getBean(ListenerAdviceInterceptor) + listenerAdviceInterceptor.recievedMessages.clear() + + then:"the methods are invocable" + listenerAdviceInterceptor.recievedMessages.isEmpty() + instance.getFoo() == "good" + instance.getBar() == null + instance.onApplicationEvent(new Object()) == null + !listenerAdviceInterceptor.recievedMessages.isEmpty() + listenerAdviceInterceptor.recievedMessages.size() == 1 + + cleanup: + context.close() + } + + void "test an interface with non overriding but subclass return type method"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@Stub +@jakarta.inject.Singleton +interface MyBean: GenericInterface, SpecificInterface + +open class Generic + +class Specific: Generic() + +interface GenericInterface { + fun getObject(): Generic +} + +interface SpecificInterface { + fun getObject(): Specific +} +''') + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = new DefaultBeanContext() + context.start() + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + //I ended up going this route because actually calling the methods here would be relying on + //having the target interface in the bytecode of the test + instance.$proxyMethods.length == 2 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..1368f7a1ce2 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/IntroductionOnConcreteClassSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class IntroductionOnConcreteClassSpec extends Specification { + + @Shared @AutoCleanup ApplicationContext applicationContext = ApplicationContext.run() + + void "test introduction of new interface on concrete class"() { + when: + ConcreteClass cc = applicationContext.getBean(ConcreteClass) + def listenerAdviceInterceptor = applicationContext.getBean(ListenerAdviceInterceptor) + + then: + cc instanceof ApplicationEventListener + + when: + def event = new StartupEvent(applicationContext) + cc.onApplicationEvent(event) + + then: + listenerAdviceInterceptor.recievedMessages.contains(event) + + cleanup: + listenerAdviceInterceptor.recievedMessages.clear() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..5afef055eef --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MappedIntroductionOnConcreteClassSpec.groovy @@ -0,0 +1,43 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class MappedIntroductionOnConcreteClassSpec extends Specification { + + void "test mapped introduction of new interface on concrete class"() { + given: + ApplicationContext applicationContext = buildContext(''' +package test + +import jakarta.inject.Singleton + +@io.micronaut.kotlin.processing.aop.introduction.ListenerAdviceMarker +@Singleton +open class MyBeanWithMappedIntroduction +''') + applicationContext.registerSingleton(new ListenerAdviceInterceptor()) + + when: + def beanClass = applicationContext.classLoader.loadClass('test.MyBeanWithMappedIntroduction') + def cc = applicationContext.getBean(beanClass) + def listenerAdviceInterceptor = applicationContext.getBean(ListenerAdviceInterceptor) + + then: + cc instanceof ApplicationEventListener + + when: + def event = new StartupEvent(applicationContext) + cc.onApplicationEvent(event) + + then: + listenerAdviceInterceptor.recievedMessages.contains(event) + + cleanup: + listenerAdviceInterceptor.recievedMessages.clear() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy new file mode 100644 index 00000000000..45b95ebdf0a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroductionSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +import java.util.stream.Collectors + +class MyRepoIntroductionSpec extends Specification { + + void "test generated introduction methods"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(MyRepo) + def interceptorDeclaredMethods = Arrays.stream(bean.getClass().getMethods()).filter(m -> m.getDeclaringClass() == bean.getClass()).collect(Collectors.toList()) + def repoDeclaredMethods = Arrays.stream(MyRepo.class.getMethods()).filter(m -> m.getDeclaringClass() == MyRepo.class).collect(Collectors.toList()) + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + repoDeclaredMethods.size() == 3 + interceptorDeclaredMethods.size() == 4 + bean.getClass().getName().contains("Intercepted") + myRepoIntroducer.executableMethods.isEmpty() + + when: + bean.aBefore() + bean.xAfter() + bean.findAll() + + then: + myRepoIntroducer.executableMethods.size() == 3 + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "aBefore" } + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "xAfter" } + myRepoIntroducer.executableMethods.contains repoDeclaredMethods.find { method -> method.name == "findAll" && method.returnType == List.class } + + cleanup: + applicationContext.close() + } + + void "test interface overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(CustomCrudRepo) + def beanDef = applicationContext.getBeanDefinition(CustomCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + CrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test interface abstract overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(AbstractCustomCrudRepo) + def beanDef = applicationContext.getBeanDefinition(AbstractCustomCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + CrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test abstract overridden method"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(AbstractCustomAbstractCrudRepo) + def beanDef = applicationContext.getBeanDefinition(AbstractCustomAbstractCrudRepo) + def findByIdMethods = beanDef.getExecutableMethods().findAll(m -> m.getName() == "findById") + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + + then: + myRepoIntroducer.executableMethods.size() == 0 + findByIdMethods.size() == 1 + findByIdMethods[0].hasAnnotation(Marker) + + when: + bean.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + when: + AbstractCrudRepo crudRepo = bean + crudRepo.findById(111) + + then: + myRepoIntroducer.executableMethods.size() == 1 + + cleanup: + applicationContext.close() + } + + void "test overridden void methods"() { + when: + def applicationContext = ApplicationContext.run() + def bean = applicationContext.getBean(MyRepo2) + def myRepoIntroducer = applicationContext.getBean(MyRepoIntroducer) + bean.deleteById(1) + + then: + myRepoIntroducer.executableMethods.size() == 1 + myRepoIntroducer.executableMethods.clear() + + cleanup: + applicationContext.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy new file mode 100644 index 00000000000..f51963ab956 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroductionAdviceSpec.groovy @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.context.ApplicationContext +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class DelegatingIntroductionAdviceSpec extends Specification { + + @Shared @AutoCleanup ApplicationContext context = ApplicationContext.run() + + void "test that delegation advice works"() { + given: + DelegatingIntroduced delegating = (DelegatingIntroduced)context.getBean(Delegating) + + expect: + delegating.test2() == 'good' + delegating.test() == 'good' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy new file mode 100644 index 00000000000..306c19be59e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionInnerInterfaceSpec.groovy @@ -0,0 +1,50 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionInnerInterfaceSpec extends Specification { + + void "test an inner class passed to @Introduction(interfaces = "() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.MyBean" + BeanDefinitionVisitor.PROXY_SUFFIX, """ +package test + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type +import jakarta.inject.Singleton + +@Around +@Type(io.micronaut.kotlin.processing.aop.introduction.with_around.ObservableInterceptor::class) +@Introduction(interfaces = [ObservableUI.Inner::class]) +@Retention +annotation class ObservableUI { + + interface Inner { + fun hello(): String + } +} + +@Singleton +@ObservableUI +open class MyBean +""") + + then: + noExceptionThrown() + beanDefinition != null + + when: + def context = ApplicationContext.run() + def instance = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + instance.hello() == "World" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy new file mode 100644 index 00000000000..724310bc232 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/introduction/with_around/IntroductionWithAroundOnConcreteClassSpec.groovy @@ -0,0 +1,135 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.inject.ExecutableMethod +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class IntroductionWithAroundOnConcreteClassSpec extends Specification { + + @Shared + @AutoCleanup + ApplicationContext applicationContext = ApplicationContext.run() + + void "test introduction with around compile"() { + given: + def context = buildContext(''' +package aroundwithintro + +import io.micronaut.kotlin.processing.aop.introduction.with_around.ProxyIntroductionAndAroundOneAnnotation + +@ProxyIntroductionAndAroundOneAnnotation +open class Test +''', true) + + expect: + getBean(context, 'aroundwithintro.Test') + + cleanup: + context.close() + } + + @Unroll + void "test introduction with around for #clazz"(Class clazz) { + when: + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + def proxyTargetBeanDefinition = applicationContext.findProxyTargetBeanDefinition(clazz, null).orElse(beanDefinition) + def bean = applicationContext.getBean(beanDefinition) + + then: + bean instanceof CustomProxy + ((CustomProxy) bean).isProxy() + bean.getId() == 1 + bean.getName() == null + + when: + bean = beanDefinition != proxyTargetBeanDefinition ? applicationContext.getProxyTargetBean(clazz, null) : applicationContext.getBean(clazz, null) + + then: + bean instanceof CustomProxy + ((CustomProxy) bean).isProxy() + bean.getId() == 1 + bean.getName() == null + + and: + beanDefinition.getExecutableMethods().size() == 5 + proxyTargetBeanDefinition.getExecutableMethods().size() == 5 + + where: + clazz << [MyBean1, MyBean2, MyBean3, MyBean4, MyBean5] + } + + void "test introspected preset"(Class clazz) { + when: + def introspection = BeanIntrospection.getIntrospection(clazz) + + then: + introspection + + where: + clazz << [MyBean4, MyBean5, MyBean6] + } + + void "test executable methods count for introduction with executable"() { + when: + def clazz = MyBean7.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 5 + } + + void "test executable methods count for around with executable"() { + when: + def clazz = MyBean8.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 4 + + when: + MyBean8 myBean8 = applicationContext.getBean(clazz) + + then: + myBean8.getId() == 1L + } + + void "test a multidimensional array property"() { + when: + def clazz = MyBean9.class + def beanDefinition = applicationContext.getBeanDefinition(clazz, null) + + then: + beanDefinition.getExecutableMethods().size() == 5 + beanDefinition.findMethod("getMultidim").get().getReturnType().asArgument().getType() == String[][].class + beanDefinition.findMethod("setMultidim", String[][].class).isPresent() + beanDefinition.findMethod("getPrimitiveMultidim").get().getReturnType().asArgument().getType() == int[][].class + beanDefinition.findMethod("setPrimitiveMultidim", int[][].class).isPresent() + + when: + MyBean9 bean = applicationContext.getBean(beanDefinition) + ExecutableMethod getMultiDim = beanDefinition.findMethod('getMultidim').get() + ExecutableMethod setMultiDim = beanDefinition.findMethod('setMultidim', String[][].class).get() + ExecutableMethod getPrimitiveMultidim = beanDefinition.findMethod('getPrimitiveMultidim').get() + ExecutableMethod setPrimitiveMultidim = beanDefinition.findMethod('setPrimitiveMultidim', int[][].class).get() + + then: + getMultiDim.invoke(bean) == null + getPrimitiveMultidim.invoke(bean) == null + + when: + setMultiDim.invoke(bean, new Object[] { new String[][] { new String[] { "test" }, new String[] { "abc" } } }) + setPrimitiveMultidim.invoke(bean, new Object[] { new int[][] { new int[] { 1 }, new int[] { 2 }} }) + + then: + bean.getMultidim()[0][0] == "test" + bean.getPrimitiveMultidim()[0][0] == 1 + getMultiDim.invoke(bean)[0][0] == "test" + getPrimitiveMultidim.invoke(bean)[0][0] == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..ffb4b6433cc --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceMethodLevelAopSpec.groovy @@ -0,0 +1,67 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class InterfaceMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InterfaceClass foo = beanContext.getBean(InterfaceClass) + + + then: + foo instanceof Intercepted + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy new file mode 100644 index 00000000000..05bcf0e241a --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelSpec.groovy @@ -0,0 +1,61 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class InterfaceTypeLevelSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + InterfaceTypeLevel foo = beanContext.getBean(InterfaceTypeLevel) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: + InterfaceTypeLevel foo = beanContext.getBean(InterfaceTypeLevel) + + + then: + foo instanceof Intercepted + foo.test("test") == "Name is changed" + + cleanup: + beanContext.close() + + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy new file mode 100644 index 00000000000..ff5c08fcb50 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/named/NamedAopAdviceSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.aop.Intercepted +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.PendingFeature +import spock.lang.Specification + +class NamedAopAdviceSpec extends Specification { + + void "test that named beans that have AOP advice applied lookup the correct target named bean - primary included"() { + given: + def context = ApplicationContext.run( + 'aop.test.named.default': 0, + 'aop.test.named.one': 1, + 'aop.test.named.two': 2, + ) + + expect: + context.getBean(NamedInterface) instanceof Intercepted + context.getBean(NamedInterface).doStuff() == 'default' + context.getBean(NamedInterface, Qualifiers.byName("one")).doStuff() == 'one' + context.getBean(NamedInterface, Qualifiers.byName("two")).doStuff() == 'two' + context.getBeansOfType(NamedInterface).size() == 3 + context.getBeansOfType(NamedInterface).every({ it instanceof Intercepted }) + + cleanup: + context.close() + } + + void "test that named beans that have AOP advice applied lookup the correct target named bean - no primary"() { + given: + def context = ApplicationContext.run( + 'aop.test.named.one': 1, + 'aop.test.named.two': 2, + ) + + expect: + context.getBean(NamedInterface, Qualifiers.byName("one")).doStuff() == 'one' + context.getBean(NamedInterface, Qualifiers.byName("two")).doStuff() == 'two' + context.getBeansOfType(NamedInterface).size() == 2 + context.getBeansOfType(NamedInterface).every({ it instanceof Intercepted }) + + cleanup: + context.close() + } + + void "test manually named beans with AOP advice"() { + given: + def context = ApplicationContext.run() + + expect: + context.getBean(OtherInterface, Qualifiers.byName("first")).doStuff() == 'first' + context.getBean(OtherInterface, Qualifiers.byName("second")).doStuff() == 'second' + context.getBeansOfType(OtherInterface).size() == 2 + context.getBeansOfType(OtherInterface).every({ it instanceof Intercepted }) + context.getBean(OtherBean).first.doStuff() == "first" + context.getBean(OtherBean).second.doStuff() == "second" + + cleanup: + context.close() + } + + void "test named bean relying on non iterable config"() { + given: + def context = ApplicationContext.run(['other.interfaces.third': 'third']) + + expect: + context.getBean(OtherInterface, Qualifiers.byName("third")).doStuff() == 'third' + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..7be21bee147 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingMethodLevelAopSpec.groovy @@ -0,0 +1,80 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.InterceptedProxy +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import spock.lang.Specification +import spock.lang.Unroll + +class ProxyingMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + ProxyingClass foo = beanContext.getBean(ProxyingClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + foo.lifeCycleCount == 0 + foo instanceof InterceptedProxy + foo.interceptedTarget().lifeCycleCount == 1 + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: "the bean definition is obtained" + BeanDefinition beanDefinition = beanContext.findBeanDefinition(ProxyingClass).get() + + then: + beanDefinition.findMethod("test", String).isPresent() + // should not be a reflection based method + !beanDefinition.findMethod("test", String).get().getClass().getName().contains("Reflection") + + when: + ProxyingClass foo = beanContext.getBean(ProxyingClass) + + + then: + foo instanceof InterceptedProxy + beanContext.findExecutableMethod(ProxyingClass, "test", String).isPresent() + // should not be a reflection based method + !beanContext.findExecutableMethod(ProxyingClass, "test", String).get().getClass().getName().contains("Reflection") + foo.test("test") == "Name is changed" + + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy new file mode 100644 index 00000000000..af37ca6615b --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassMethodLevelAopSpec.groovy @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Intercepted +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class SimpleClassMethodLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + SimpleClass foo = beanContext.getBean(SimpleClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == result + foo.postConstructInvoked + + where: + method | args | result + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + } + + + void "test AOP setup"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + when: "the bean definition is obtained" + BeanDefinition beanDefinition = beanContext.findBeanDefinition(SimpleClass).get() + + then: + beanDefinition.findMethod("test", String).isPresent() + // should not be a reflection based method + !beanDefinition.findMethod("test", String).get().getClass().getName().contains("Reflection") + + when: + SimpleClass foo = beanContext.getBean(SimpleClass) + + + then: + foo instanceof Intercepted + beanContext.findExecutableMethod(SimpleClass, "test", String).isPresent() + // should not be a reflection based method + !beanContext.findExecutableMethod(SimpleClass, "test", String).get().getClass().getName().contains("Reflection") + foo.test("test") == "Name is changed" + } + + void "test modifying the interceptor parameters is not supported"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + SimpleClass foo = beanContext.getBean(SimpleClass) + + when: "the interceptor is called" + foo.invalidInterceptor() + + then: + thrown(UnsupportedOperationException) + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy new file mode 100644 index 00000000000..5526d4635b6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/aop/simple/SimpleClassTypeLevelAopSpec.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class SimpleClassTypeLevelAopSpec extends Specification { + + @Unroll + void "test AOP method invocation for method #method"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + AnotherClass foo = beanContext.getBean(AnotherClass) + + expect: + args.isEmpty() ? foo."$method"() : foo."$method"(*args) == expected + + where: + method | args | expected + 'test' | ['test'] | "Name is changed" // test for single string arg + 'test' | [10] | "Age is 20" // test for single primitive + 'test' | ['test', 10] | "Name is changed and age is 10" // test for multiple args, one primitive + 'test' | [] | "noargs" // test for no args + 'testVoid' | ['test'] | null // test for void return type + 'testVoid' | ['test', 10] | null // test for void return type + 'testBoolean' | ['test'] | true // test for boolean return type + 'testBoolean' | ['test', 10] | true // test for boolean return type + 'testInt' | ['test'] | 1 // test for int return type + 'testInt' | ['test', 10] | 20 // test for int return type + 'testShort' | ['test'] | 1 // test for short return type + 'testShort' | ['test', 10] | 20 // test for short return type + 'testChar' | ['test'] | 1 // test for char return type + 'testChar' | ['test', 10] | 20 // test for char return type + 'testByte' | ['test'] | 1 // test for byte return type + 'testByte' | ['test', 10] | 20 // test for byte return type + 'testFloat' | ['test'] | 1 // test for float return type + 'testFloat' | ['test', 10] | 20 // test for float return type + 'testDouble' | ['test'] | 1 // test for double return type + 'testDouble' | ['test', 10] | 20 // test for double return type + 'testByteArray' | ['test', 'test'.bytes] | 'test'.bytes // test for byte array + 'testGenericsWithExtends' | ['test', 10] | 'Name is changed' // test for generics + 'testGenericsFromType' | ['test', 10] | 'Name is changed' // test for generics + 'testListWithWildCardIn' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') // test for generics + 'testListWithWildCardOut' | ['test', new CovariantClass<>()] | new CovariantClass<>('changed') + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy new file mode 100644 index 00000000000..fd5f6090f50 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/AbstractBeanSpec.groovy @@ -0,0 +1,59 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.inject.BeanDefinition +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class AbstractBeanSpec extends Specification { + + void "test bean definitions are created for classes with only a qualifier"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@jakarta.inject.Named("a") +class Bean +''') + then: + beanDefinition != null + !beanDefinition.isSingleton() + } + + void "test abstract classes with only a qualifier do not generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@jakarta.inject.Named("a") +abstract class Bean +''') + then: + beanDefinition == null + } + + void "test classes with only AOP advice generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test + +@io.micronaut.validation.Validated +open class Bean +''') + then: + beanDefinition != null + } + + void "test abstract classes with only AOP advice do not generate bean definitions"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Bean', ''' +package test; + +@io.micronaut.validation.Validated +abstract class Bean +''') + then: + beanDefinition == null + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy new file mode 100644 index 00000000000..4a530725f3e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanDefinitionSpec.groovy @@ -0,0 +1,849 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.Order +import io.micronaut.core.bind.annotation.Bindable +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.HttpMethodMapping +import io.micronaut.http.client.annotation.Client +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.qualifiers.Qualifiers +import io.micronaut.inject.writer.BeanDefinitionVisitor +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class BeanDefinitionSpec extends Specification { + + void "test jvm field"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.JvmFieldTest', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class JvmFieldTest { + @JvmField + @Inject + var f : F? = null +} + +@Singleton +class F +''') + + expect: + definition.injectedMethods.size() == 0 + definition.injectedFields.size() == 1 + } + + @PendingFeature(reason = "difficult to achieve with current design without a significant rewrite or how native properties are handled") + void "test injection order for inheritance"() { + given: + def context = KotlinCompiler.buildContext(''' +package inherit + +import jakarta.inject.* +import jakarta.inject.Inject + +@Singleton +class Child : Parent() { + + var parentMethodInjectBeforeChildMethod : Boolean = false + var parentMethodInjectBeforeChildField : Boolean = false + var childFieldInjectedBeforeChildMethod : Boolean = false + + @Inject + var childProp : Other? = Other() + set(value) { + if (parentProp != null && parentMethod != null) { + parentMethodInjectBeforeChildField = true + } + } + lateinit var childMethod : Other + + @Inject + fun antherMethod(other : Other) { + if (parentProp != null && parentMethod != null) { + parentMethodInjectBeforeChildMethod = true + } + if (childProp != null) { + childFieldInjectedBeforeChildMethod = true + } + childMethod = other + } +} + +open class Parent { + var parentPropInjectedBeforeParentMethod : Boolean = false + + @Inject + lateinit var parentProp : Other + lateinit var parentMethod : Other + + @Inject + fun someMethod(other : Other) { + if (parentProp != null) { + parentPropInjectedBeforeParentMethod = true + } + parentMethod = other + } +} + +@Singleton +class Other +''') + def bean = KotlinCompiler.getBean(context, 'inherit.Child') + + expect:"The parent property was injected before the parent method" + bean.parentPropInjectedBeforeParentMethod + + and:"All injection points of the parent were injected before the child method" + bean.parentInjectBeforeChildMethod + + and:"All injection points of the parent were injected before the child field" + bean.parentInjectBeforeChildField + + and:"The child property was injected before the child method" + bean.childFieldInjectedBeforeChildMethod + + cleanup: + context.close() + } + + void "test suspend function with executable"() { + given: + def definition = buildBeanDefinition('test.SuspendTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class SuspendTest { + @Executable + suspend fun test() { + TODO() + } +} + +@Singleton +class A +''') + expect: + definition != null + definition.executableMethods.size() == 1 + } + + void "test @Inject on set of Kotlin property"() { + given: + def definition = buildBeanDefinition('test.SetterInjectBean', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class SetterInjectBean { + internal var _a: A? = null + internal var a: A + get() = _a!! + @Inject set(value) { _a = value; } +} + +@Singleton +class A +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + } + + void "test requires validation adds bean introspection"() { + given: + def definition = buildBeanDefinition('test.EngineConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import javax.validation.constraints.Min +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") +class EngineConfig { + + @Min(1L) + var cylinders: Int = 0 + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //<1> + var sensors: Map? = null +} +''') + expect: + definition.hasAnnotation(Introspected) + } + + void "test repeated annotations - auto unwrap"() { + given: + def definition = buildBeanDefinition('test.RepeatedTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.Headers +import jakarta.inject.Singleton + +@Singleton +@Headers( + Header(name="Foo"), + Header(name="Bar") +) + +class RepeatedTest { + @Executable + @Headers( + Header(name="Baz"), + Header(name="Stuff") + ) + fun test() : String { + return "Ok" + } +} +''') + expect: + definition.getRequiredMethod("test").getAnnotationValuesByType(Header).size() == 4 + definition.getRequiredMethod("test").getAnnotationNamesByStereotype(Bindable).size() == 2 + } + + void "test repeated annotations"() { + given: + def definition = buildBeanDefinition('test.RepeatedTest', ''' +package test + +import io.micronaut.context.annotation.Executable +import io.micronaut.http.annotation.Header +import jakarta.inject.Singleton + +@Singleton +@Header(name="Foo") +@Header(name="Bar") +class RepeatedTest { + @Executable + @Header(name="Baz") + @Header(name="Stuff") + fun test() : String { + return "Ok" + } +} +''') + expect: + definition.getRequiredMethod("test").getAnnotationValuesByType(Header).size() == 4 + } + + void "test annotation defaults"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.TestClient' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface TestClient { + @Post + fun save(str : String) : String +} +''') + expect: + definition.getRequiredMethod("save", String) + .getAnnotation(HttpMethodMapping) + .getRequiredValue(String) == '/' + } + + void "test annotation defaults - inherited"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.TestClient' + BeanDefinitionVisitor.PROXY_SUFFIX, ''' +package test + +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface TestClient : TestOperations { + override fun save(str : String) : String +} + +interface TestOperations { + @Post + fun save(str : String) : String +} +''') + expect: + definition.getRequiredMethod("save", String) + .getAnnotation(HttpMethodMapping) + .getRequiredValue(String) == '/' + } + + void "test @Inject internal var"() { + given: + def context = KotlinCompiler.buildContext(''' +package test + +import io.micronaut.context.event.ApplicationEventPublisher +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class SampleEventEmitterBean { + + @Inject + internal var eventPublisher: ApplicationEventPublisher? = null + + fun publishSampleEvent() { + eventPublisher!!.publishEvent(SampleEvent()) + } + +} + +class SampleEvent + +''') + + def bean = getBean(context, 'test.SampleEventEmitterBean') + def definition = KotlinCompiler.getBeanDefinition(context, 'test.SampleEventEmitterBean') + expect: + definition.injectedFields.size() == 0 + definition.injectedMethods.size() == 1 + + bean.eventPublisher + + cleanup: + context.close() + } + void "test @Property targeting field"() { + given: + def context = buildContext(''' +package test + +import io.micronaut.context.annotation.Property + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class Engine { + + @field:Property(name = "my.engine.cylinders") // <1> + protected var cylinders: Int = 0 // <2> + + @set:Inject + @setparam:Property(name = "my.engine.manufacturer") // <3> + var manufacturer: String? = null + + @Inject + @Property(name = "my.engine.manufacturer") // <3> + var manufacturer2: String? = null + + @Property(name = "my.engine.manufacturer") // <3> + var manufacturer3: String? = null + + fun cylinders(): Int { + return cylinders + } +} +''', false, ['my.engine.cylinders': 8, 'my.engine.manufacturer':'Ford']) + def definition = getBeanDefinition(context, 'test.Engine') + def bean = getBean(context, 'test.Engine') + + expect:"field targeting injects fields" + definition.injectedMethods.size() == 4 + definition.injectedFields.size() == 0 + bean.cylinders() == 8 + bean.manufacturer == 'Ford' + bean.manufacturer2 == 'Ford' + bean.manufacturer3 == 'Ford' + } + + void "test non-binding qualifier"() { + given: + def definition = KotlinCompiler.buildBeanDefinition('test.V8Engine', ''' +package test + +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Qualifier +import jakarta.inject.Singleton +import kotlin.annotation.Retention + +@Cylinders(value = 8, description = "test") +@Singleton +class V8Engine + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Cylinders( + val value: Int, + @get:NonBinding // <2> + val description: String = "" +) +''') + expect:"the non-binding member is not there" + definition.declaredQualifier.qualifierAnn.memberNames == ["value"] as Set + } + + void "test property annotation on properties and targeting params"() { + given: + def context = KotlinCompiler.buildContext(''' +package test +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.MapFormat +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class BeanWithProperty { + + @set:Inject + @setparam:Property(name="app.string") + var stringParam:String ?= null + + @set:Inject + @setparam:Property(name="app.map") + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParam:Map ?= null + + @Property(name="app.string") + var stringParamTwo:String ?= null + + @Property(name="app.map") + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParamTwo:Map ?= null +} +''', false, ["app.string": "Hello", "app.map.yyy.xxx": 2, "app.map.yyy.yyy": 3]) + + def bean = KotlinCompiler.getBean(context, 'test.BeanWithProperty') + + expect: + bean.stringParam == 'Hello' + + cleanup: + context.close() + } + + void "test annotations targeting field on properties"() { + given: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class TestBean { + @Inject @field:Named("one") lateinit var otherBean: OtherBean +} + +@Singleton +@Named("one") +class OtherBean +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + definition.injectedMethods[0].annotationMetadata.hasAnnotation(AnnotationUtil.NAMED) + definition.injectedMethods[0].arguments[0].annotationMetadata.hasAnnotation(AnnotationUtil.NAMED) + } + + void "test annotations targeting field on properties - client"() { + given: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class TestBean { + @Inject @field:Client("/test") lateinit var client: HttpClient +} + +''') + expect: + definition != null + definition.injectedMethods.size() == 1 + definition.injectedMethods[0].annotationMetadata.hasAnnotation(Client) + definition.injectedMethods[0].arguments[0].annotationMetadata.hasAnnotation(Client) + } + + void "test controller with constructor arguments"() { + given: + def definition = buildBeanDefinition('controller.TestController', ''' +package controller + +import io.micronaut.context.annotation.* +import io.micronaut.http.annotation.Controller +import jakarta.inject.* +import jakarta.inject.Singleton + +@Controller +class TestController(var bar : Bar) + +@Singleton +class Bar +''') + expect: + definition != null + } + + void "test limit the exposed bean types"() { + given: + def definition = buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test: Runnable { + + override fun run() { + } +} + +''') + expect: + definition.exposedTypes == [Runnable] as Set + } + + void "test limit the exposed bean types - reference"() { + given: + def reference = buildBeanDefinitionReference('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test: Runnable { + + override fun run(){ + } +} + +''') + expect: + reference.exposedTypes == [Runnable] as Set + } + + void "test fail compilation on invalid exposed bean type"() { + when: + buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [Runnable::class]) +class Test +''') + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [java.lang.Runnable] that is not implemented by the bean type") + } + + void "test exposed types on factory with AOP"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.kotlin.processing.aop.Logged +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [X::class]) + @Logged + fun method(): Y { + return Y() + } +} + +interface X + +open class Y: X +''') + + then: + noExceptionThrown() + } + + void "test fail compilation on exposed subclass of bean type"() { + when: + buildBeanDefinition('limittypes.Test', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Bean(typed = [X::class]) +open class Test + +class X : Test() +''') + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.X] that is not implemented by the bean type") + } + + void "test fail compilation on exposed subclass of bean type with factory"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [X::class, Y::class]) + fun method(): X { + return Y() + } +} + +interface X + +class Y: X +''') + + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.Y] that is not implemented by the bean type") + } + + void "test exposed bean types with factory invalid type"() { + when: + buildBeanDefinition('limittypes.Test$Method0', ''' +package limittypes + +import io.micronaut.context.annotation.* +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean(typed = [Z::class]) + fun method(): X { + return Y() + } +} + +interface Z +interface X +class Y: X +''') + + then: + def e = thrown(RuntimeException) + e.message.contains("Bean defines an exposed type [limittypes.Z] that is not implemented by the bean type") + } + + void 'test order annotation'() { + given: + def definition = buildBeanDefinition('test.TestOrder', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.context.annotation.* +import jakarta.inject.* + +@Singleton +@Order(value = 10) +class TestOrder +''') + expect: + + definition.intValue(Order).getAsInt() == 10 + } + + void 'test qualifier for named only'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test + +@jakarta.inject.Named("foo") +class Test +''') + expect: + definition.getDeclaredQualifier() == Qualifiers.byName("foo") + } + + void 'test no qualifier / only scope'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test; + +@jakarta.inject.Singleton +class Test +''') + expect: + definition.getDeclaredQualifier() == null + } + + void 'test named via alias'() { + given: + def definition = buildBeanDefinition('aliastest.Test', ''' +package aliastest + +import io.micronaut.context.annotation.* + +@MockBean(named="foo") +class Test + +@Bean +annotation class MockBean( + + @get:Aliases(AliasFor(annotation = Replaces::class, member = "named"), AliasFor(annotation = jakarta.inject.Named::class, member = "value")) + val named: String = "" +) +''') + expect: + definition.getDeclaredQualifier() == Qualifiers.byName("foo") + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == AnnotationUtil.NAMED + } + + void 'test qualifier annotation'() { + given: + def definition = buildBeanDefinition('test.Test', ''' +package test + +import io.micronaut.context.annotation.* + +@MyQualifier +class Test + +@jakarta.inject.Qualifier +annotation class MyQualifier ( + + @get:Aliases(AliasFor(annotation = Replaces::class, member = "named"), AliasFor(annotation = jakarta.inject.Named::class, member = "value")) + val named: String = "" +) +''') + + expect: + definition.getDeclaredQualifier() == Qualifiers.byAnnotation(definition.getAnnotationMetadata(), "test.MyQualifier") + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == "test.MyQualifier" + } + + /* @Issue("https://github.com/micronaut-projects/micronaut-core/issues/5001") + void "test building a bean with generics that dont have a type"() { + when: + def definition = buildBeanDefinition('test.NumberThingManager', ''' +package test; + +import jakarta.inject.Singleton + +interface Thing + +interface NumberThing> extends Thing {} + +class AbstractThingManager> {} + +@Singleton +public class NumberThingManager extends AbstractThingManager> {} +''') + + then: + noExceptionThrown() + definition != null + definition.getTypeArguments("test.AbstractThingManager")[0].getTypeVariables().get("T").getType() == Number.class + }*/ + + void "test a bean definition in a package with uppercase letters"() { + when: + def definition = buildBeanDefinition('test.A', 'TestBean', ''' +package test.A + +@jakarta.inject.Singleton +class TestBean +''') + then: + noExceptionThrown() + definition != null + } + + void "test a bean definition inner static class"() { + when: + def definition = buildBeanDefinition('test.TestBean$TestBeanInner', ''' +package test + +class TestBean { + + @jakarta.inject.Singleton + class TestBeanInner { + + } +} +''') + then: + noExceptionThrown() + definition != null + } + + void "test a bean definition is not created for inner class"() { + when: + def definition = buildBeanDefinition('test.TestBean$TestBeanInner', ''' +package test + +class TestBean { + + @jakarta.inject.Singleton + inner class TestBeanInner { + + } +} +''') + then: + noExceptionThrown() + definition == null + } + + void "test nullable constructor arg"() { + when: + def definition = buildBeanDefinition('test.TestBean', ''' +package test + +@jakarta.inject.Singleton +class TestBean(private val other: Other?) { +} + +class Other +''') + then: + noExceptionThrown() + definition.constructor.arguments[0].isDeclaredNullable() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy new file mode 100644 index 00000000000..e10491c1775 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/BeanRegistrationSpec.groovy @@ -0,0 +1,70 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.context.BeanRegistration +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class BeanRegistrationSpec extends Specification { + + void 'test inject bean registrations'() { + given: + def className = 'beanreg.Test' + def context = buildContext( ''' +package beanreg + +import jakarta.inject.Singleton +import jakarta.inject.Inject +import jakarta.inject.Named +import io.micronaut.context.BeanRegistration + +@Singleton +class Test(val registrations: Collection>, val primaryBean: BeanRegistration) { + + @Inject + lateinit var fieldRegistrations: Collection> + + @Inject + lateinit var fieldArrayRegistrations: Array> + + @Inject + lateinit var methodRegistrations: List> + + @Named("two") + @Inject + lateinit var secondaryBean: BeanRegistration +} + +interface Foo + +@Singleton +@io.micronaut.context.annotation.Primary +class Foo1: Foo + +@Singleton +@Named("two") +class Foo2: Foo +''') + + def bean = getBean(context, className) + + Collection registrations = bean.registrations + Collection fieldRegistrations = bean.fieldRegistrations + Collection methodRegistrations = bean.methodRegistrations + Collection fieldArrayRegistrations = bean.fieldArrayRegistrations.toList() + + expect: + bean.primaryBean.bean.getClass().name == 'beanreg.Foo1' + bean.secondaryBean.bean.getClass().name == 'beanreg.Foo2' + registrations.size() == 2 + fieldRegistrations.size() == 2 + fieldRegistrations == registrations + fieldRegistrations as List == methodRegistrations + fieldRegistrations as List == fieldArrayRegistrations + registrations.any { it.bean.getClass().name == 'beanreg.Foo1'} + registrations.any { it.bean.getClass().name == 'beanreg.Foo2'} + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy new file mode 100644 index 00000000000..96c19189832 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/SingletonSpec.groovy @@ -0,0 +1,111 @@ +package io.micronaut.kotlin.processing.beans + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.BeanDefinition + +import spock.lang.Specification + +class SingletonSpec extends AbstractKotlinCompilerSpec { + + void "test simple singleton bean"() { + when: + def context = buildContext(""" +package test + +import jakarta.inject.Singleton + +@Singleton +class Test +""") + + then: + noExceptionThrown() + + when: + Class test = context.classLoader.loadClass("test.Test") + context.getBean(test) + + then: + noExceptionThrown() + } + + void "test singleton bean from a factory property"() { + when: + def context = buildContext(""" +package test + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton + +@Factory +class Test { + + @Singleton + @Bean + val one = Foo("one") + +} + +class Foo(val name: String) +""") + + then: + noExceptionThrown() + Class foo = context.classLoader.loadClass("test.Foo") + context.getBean(foo).getName() == "one" + } + + void "test singleton bean from a factory method"() { + when: + def context = buildContext(""" +package test + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton + +@Factory +class Test { + @Singleton + fun one() = Foo("one") +} + +class Foo(val name: String) +""") + + then: + noExceptionThrown() + Class foo = context.classLoader.loadClass("test.Foo") + context.getBean(foo).getName() == "one" + } + + void "test singleton abstract class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AbstractBean', ''' +package test + +import jakarta.inject.Singleton + +@Singleton +abstract class AbstractBean { + +} +''') + then: + beanDefinition.isAbstract() + } + + void "test that using @Singleton on an enum results in a compilation error"() { + when: + buildBeanDefinition('test.Test','''\ +package test + +@jakarta.inject.Singleton +enum class Test +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Enum types cannot be defined as beans') + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy new file mode 100644 index 00000000000..830bb485411 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/aliasfor/AliasForQualifierSpec.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.aliasfor + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition + +import spock.lang.Specification + +class AliasForQualifierSpec extends Specification { + + void "test that when an alias is created for a named qualifier the stereotypes are correct"() { + given: + BeanDefinition definition = KotlinCompiler.buildBeanDefinition('test.Test$MyFunc0','''\ +package test + +import io.micronaut.context.annotation.Factory +import io.micronaut.kotlin.processing.beans.aliasfor.TestAnnotation + +@Factory +class Test { + + @TestAnnotation("foo") + fun myFunc(): (String) -> Int { + return { str -> 10 } + } +} + +''') + expect: + definition != null + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).isPresent() + definition.getAnnotationNameByStereotype(AnnotationUtil.QUALIFIER).get() == AnnotationUtil.NAMED + definition.getValue(AnnotationUtil.NAMED, String).get() == 'foo' + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy new file mode 100644 index 00000000000..3bddb37b104 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/collect/InjectCollectionBeanSpec.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import io.micronaut.context.BeanContext +import spock.lang.Specification + +class InjectCollectionBeanSpec extends Specification { + + void "test resolve collection bean"() { + given: + def ctx = BeanContext.run() + + expect: + ctx.getBean(ThingThatNeedsMySetOfStrings).strings.size() == 1 + ctx.getBean(ThingThatNeedsMySetOfStrings).strings == ctx.getBean(ThingThatNeedsMySetOfStrings).otherStrings + } + + void "test resolve iterable bean"() { + when: + def ctx = BeanContext.run() + ctx.getBean(MyIterable) + ctx.getBean(ThingThatNeedsMyIterable) + + then: + noExceptionThrown() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy new file mode 100644 index 00000000000..436be5635d5 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigPropertiesInnerClassSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class ConfigPropertiesInnerClassSpec extends Specification { + + void "test configuration properties binding with inner class"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ]] + )) + + applicationContext.start() + + MyConfigInner config = applicationContext.getBean(MyConfigInner) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy new file mode 100644 index 00000000000..7e1356f3184 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesFactorySpec.groovy @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesFactorySpec extends Specification { + + void "test replacing a configuration properties via a factory"() { + ApplicationContext ctx = ApplicationContext.run(["spec.name": ConfigurationPropertiesFactorySpec.simpleName]) + + expect: + ctx.getBean(Neo4jProperties).uri.getHost() == "google.com" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..3729b3460fd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/ConfigurationPropertiesSpec.groovy @@ -0,0 +1,148 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import spock.lang.Specification + +class ConfigurationPropertiesSpec extends Specification { + + void "test submap with generics binding"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':10, + 'foo.bar.map.key1.key2.property2.property':10 + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test submap with generics binding and conversion"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':'10', + 'foo.bar.map.key1.key2.property2.property':'10' + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ], + 'foo.bar.port':'8080', + 'foo.bar.max-size':'1MB', + 'foo.bar.another-size':'1MB', + 'foo.bar.anotherPort':'9090', + 'foo.bar.intList':"1,2,3", + 'foo.bar.stringList':"1,2", + 'foo.bar.flags.one':'1', + 'foo.bar.flags.two':'2', + 'foo.bar.urlList':"http://test.com, http://test2.com", + 'foo.bar.urlList2':["http://test.com", "http://test2.com"], + 'foo.bar.url':'http://test.com'] + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + config.port == 8080 + config.maxSize == 1048576 + config.anotherPort == 9090 + config.intList == [1,2,3] + config.flags == [one:1, two:2] + config.urlList == [new URL('http://test.com'),new URL('http://test2.com')] + config.urlList2 == [new URL('http://test.com'),new URL('http://test2.com')] + config.stringList == ["1", "2"] + config.emptyList == null + config.url.get() == new URL('http://test.com') + !config.anotherUrl.isPresent() + config.defaultPort == 9999 + config.defaultValue == 9999 + } + + void "test configuration inner class properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.inner.enabled':'true', + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.inner.enabled + } + + void "test binding to a map setter"() { + ApplicationContext context = ApplicationContext.run(CollectionUtils.mapOf("map.setter.yyy.zzz", 3, "map.setter.yyy.xxx", 2, "map.setter.yyy.yyy", 3)) + MapProperties config = context.getBean(MapProperties.class) + + expect: + config.setter.containsKey('yyy') + + cleanup: + context.close() + } + + void "test camelCase vs kebab_case"() { + ApplicationContext context1 = ApplicationContext.run("rec1") + ApplicationContext context2 = ApplicationContext.run("rec2") + + RecConf config1 = context1.getBean(RecConf.class) + RecConf config2 = context2.getBean(RecConf.class) + + expect: + config1 == config2 + + cleanup: + context1.close() + context2.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy new file mode 100644 index 00000000000..6d07513f669 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ConfigurationPropertiesInheritanceSpec.groovy @@ -0,0 +1,197 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class ConfigurationPropertiesInheritanceSpec extends Specification { + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.port':'8080', + 'foo.bar.host':'localhost', + 'foo.bar.baz.stuff': 'test'] + )) + + applicationContext.start() + + ChildConfig config = applicationContext.getBean(ChildConfig) + MyConfig parent = applicationContext.getBean(MyConfig) + + expect: +// parent.is(config) + parent.host == 'localhost' + parent.port == 8080 + config.port == 8080 + config.host == 'localhost' + config.stuff == 'test' + + cleanup: + applicationContext.stop() + } + + void "test configuration properties binding extending POJO"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.baz.otherProperty':'x', + 'foo.baz.onlySetter':'y', + 'foo.baz.port': 55] + )) + + applicationContext.start() + + MyOtherConfig config = applicationContext.getBean(MyOtherConfig) + + expect: + config.port == 55 + config.otherProperty == 'x' + config.onlySetter == 'y' + + cleanup: + applicationContext.stop() + } + + void "test EachProperty inner ConfigurationProperties with setter"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams.cubs.wins': 5, + 'teams.cubs.manager.age': 40, + 'teams.mets.wins': 6 + ]) + + when: + ParentEachProps cubs = context.getBean(ParentEachProps, Qualifiers.byName("cubs")) + + then: + cubs.wins == 5 + cubs.manager.age == 40 + + when: + ParentEachProps.ManagerProps cubsManager = context.getBean(ParentEachProps.ManagerProps, Qualifiers.byName("cubs")) + + then: "The instance is the same" + cubsManager.is(cubs.manager) + + when: + ParentEachProps mets = context.getBean(ParentEachProps, Qualifiers.byName("mets")) + + then: + mets.wins == 6 + mets.manager == null + + and: + !context.findBean(ParentEachProps.ManagerProps, Qualifiers.byName("mets")).isPresent() + context.getBeansOfType(ParentEachProps).size() == 2 + context.getBeansOfType(ParentEachProps.ManagerProps).size() == 1 + } + + void "test EachProperty inner ConfigurationProperties with constructor"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams.cubs.wins': 5, + 'teams.cubs.manager.age': 40, + 'teams.mets.wins': 6 + ]) + + when: + ParentEachPropsCtor cubs = context.getBean(ParentEachPropsCtor, Qualifiers.byName("cubs")) + + then: + cubs.wins == 5 + cubs.manager.age == 40 + cubs.name == "cubs" + + when: + ParentEachPropsCtor.ManagerProps cubsManager = context.getBean(ParentEachPropsCtor.ManagerProps, Qualifiers.byName("cubs")) + + then: "The instance is the same" + cubsManager.is(cubs.manager) + cubsManager.name == "cubs" + + when: + ParentEachPropsCtor mets = context.getBean(ParentEachPropsCtor, Qualifiers.byName("mets")) + + then: + mets.wins == 6 + mets.manager == null + mets.name == "mets" + + and: + !context.findBean(ParentEachPropsCtor.ManagerProps, Qualifiers.byName("mets")).isPresent() + context.getBeansOfType(ParentEachPropsCtor).size() == 2 + context.getBeansOfType(ParentEachPropsCtor.ManagerProps).size() == 1 + } + + void "test EachProperty array inner ConfigurationProperties with setter"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams': [['wins': 5, 'manager': ['age': 40]], ['wins': 6]] + ]) + + when: + Collection teams = context.getBeansOfType(ParentArrayEachProps) + + then: + teams[0].wins == 5 + teams[0].manager.age == 40 + teams[1].wins == 6 + teams[1].manager == null + + when: + Collection managers = context.getBeansOfType(ParentArrayEachProps.ManagerProps) + + then: "The instance is the same" + managers.size() == 1 + managers[0].is(teams[0].manager) + } + + void "test EachProperty array inner ConfigurationProperties with constructor"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'teams': [['wins': 5, 'manager': ['age': 40]], ['wins': 6]] + + ]) + + when: + Collection teams = context.getBeansOfType(ParentArrayEachPropsCtor) + + then: + teams[0].wins == 5 + teams[0].manager.age == 40 + teams[1].wins == 6 + teams[1].manager == null + + when: + Collection managers = context.getBeansOfType(ParentArrayEachPropsCtor.ManagerProps) + + then: "The instance is the same" + managers.size() == 1 + managers[0].is(teams[0].manager) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy new file mode 100644 index 00000000000..2ea6a1db23e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableBeanSpec.groovy @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.inject.BeanDefinition +import spock.lang.Issue +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableBeanSpec extends Specification { + + void "test executable method return types"() { + given: + BeanDefinition definition = buildBeanDefinition('test.ExecutableBean1','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class ExecutableBean1 { + + fun round(num: Float): Int { + return num.roundToInt() + } +} +''') + expect: + definition != null + definition.findMethod("round", float.class).get().returnType.type == int.class + } + + @Issue('#2789') + void "test don't generate executable methods for inherited protected or package private methods"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +@jakarta.inject.Singleton +@Executable +class MyBean: Parent() { + + fun round(num: Float): Int { + return num.roundToInt() + } +} + +open class Parent { + protected fun protectedMethod() { + } + + internal fun packagePrivateMethod() { + } + + private fun privateMethod() { + } +} +''') + expect: + definition != null + !definition.findMethod("privateMethod").isPresent() + !definition.findMethod("packagePrivateMethod").isPresent() + !definition.findMethod("protectedMethod").isPresent() + } + + void "bean definition should not be created for class with only executable methods"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.context.annotation.Executable +import kotlin.math.roundToInt + +class MyBean { + + @Executable + fun round(num: Float): Int { + return num.roundToInt() + } +} + +''') + + expect: + definition == null + } + + void "test multiple executable annotations on a method"() { + given: + BeanDefinition definition = buildBeanDefinition('test.MyBean','''\ +package test + +import io.micronaut.kotlin.processing.beans.executable.RepeatableExecutable + +@jakarta.inject.Singleton +class MyBean { + + @RepeatableExecutable("a") + @RepeatableExecutable("b") + fun run() { + + } +} +''') + expect: + definition != null + definition.findMethod("run").isPresent() + } +} + diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy new file mode 100644 index 00000000000..29c9976bca2 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/ExecutableSpec.groovy @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.ExecutionHandle +import io.micronaut.inject.MethodExecutionHandle +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ExecutableSpec extends Specification { + + void "test executable compile spec"() { + given:"A bean that defines no explicit scope" + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyBean', ''' +package test + +import io.micronaut.context.annotation.Executable + +@Executable +class MyBean { + + fun methodOne(@jakarta.inject.Named("foo") one: String): String { + return "good" + } + + fun methodTwo(one: String, two: String): String { + return "good" + } + + fun methodZero(): String { + return "good" + } +} + + +''') + then:"the default scope is singleton" + beanDefinition.executableMethods.size() == 3 + beanDefinition.executableMethods[0].methodName == 'methodOne' + beanDefinition.executableMethods[0].getArguments()[0].getAnnotationMetadata().stringValue(AnnotationUtil.NAMED).get() == 'foo' + } + + void "test executable metadata"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test").start() + + when: + Optional method = applicationContext.findExecutionHandle(BookController, "show", Long) + ExecutableMethod executableMethod = applicationContext.findBeanDefinition(BookController).get().findMethod("show", Long).get() + + then: + method.isPresent() + + when: + MethodExecutionHandle executionHandle = method.get() + + then: + executionHandle.returnType.type == String + executionHandle.invoke(1L) == "1 - The Stand" + + when: + executionHandle.invoke("bad") + + then: + def e = thrown(IllegalArgumentException) + e.message == 'Invalid type [java.lang.String] for argument [Long id] of method: String show(Long id)' + + } + + void "test executable responses"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test").start() + + expect: + applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).isPresent() + ExecutionHandle method = applicationContext.findExecutionHandle(BookController, methodName, argTypes as Class[]).get() + method.invoke(args as Object[]) == result + + + where: + methodName | argTypes | args | result + "show" | [Long] | [1L] | "1 - The Stand" + "showArray" | [Long[].class] | [[1L] as Long[]] | "1 - The Stand" + "showPrimitive" | [long.class] | [1L as long] | "1 - The Stand" + "showPrimitiveArray" | [long[].class] | [[1L] as long[]] | "1 - The Stand" + "showVoidReturn" | [Iterable.class] | [['test']] | null + "showPrimitiveReturn" | [int[].class] | [[1] as int[]] | 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy new file mode 100644 index 00000000000..0899c96b11c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/executable/inheritance/InheritedExecutableSpec.groovy @@ -0,0 +1,149 @@ +package io.micronaut.kotlin.processing.beans.executable.inheritance + +import io.micronaut.inject.BeanDefinition +import spock.lang.PendingFeature +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritedExecutableSpec extends Specification { + + void "test extending an abstract class with an executable method"() { + given: + BeanDefinition definition = buildBeanDefinition("test.GenericController", """ +package test + +import io.micronaut.context.annotation.Executable + +abstract class GenericController { + + abstract fun getPath(): String + + @Executable + fun save(entity: T): String { + return "parent" + } +} + +""") + expect: + definition == null + } + + void "test the same method isn't written twice"() { + BeanDefinition definition = buildBeanDefinition("test.StatusController", """ +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton + +@Executable +@Singleton +class StatusController: GenericController() { + + override fun getPath(): String { + return "/statuses" + } + + override fun save(entity: String): String { + return "child" + } + +} + +abstract class GenericController { + + abstract fun getPath(): String + + @Executable + open fun save(entity: T): String { + return "parent" + } + + @Executable + open fun save(): String { + return "parent" + } +} + +""") + expect: + definition != null + definition.getExecutableMethods().any { it.methodName == "getPath" } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes == [String] as Class[] } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes.length == 0 } + definition.getExecutableMethods().size() == 3 + } + + void "test with multiple generics"() { + BeanDefinition definition = buildBeanDefinition("test.StatusController",""" +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton +import java.io.Serializable + +abstract class GenericController { + + @Executable + fun save(entity: T): T { + return entity + } + + @Executable + fun find(id: ID): T? { + return null + } + + abstract fun create(id: ID): T +} + +@Executable +@Singleton +class StatusController: GenericController() { + + override fun create(id: Int): String { + return id.toString() + } +} +""") + expect: + definition != null + definition.getExecutableMethods().any { it.methodName == "create" && it.argumentTypes == [int] as Class[] } + definition.getExecutableMethods().any { it.methodName == "save" && it.argumentTypes == [String] as Class[] } + definition.getExecutableMethods().any { it.methodName == "find" && it.argumentTypes == [int] as Class[] } + definition.getExecutableMethods().size() == 3 + } + + void "test multiple inheritance"() { + BeanDefinition definition = buildBeanDefinition("test.Z", """ +package test + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Singleton + +interface X { + + @Executable + fun test() +} + +abstract class Y : X { + + override fun test(){ + } + +} + +@Singleton +class Z : Y() { + + override fun test(){ + } +} +""") + expect: + definition != null + definition.executableMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy new file mode 100644 index 00000000000..af8bf4e599c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanannotation/PrototypeAnnotationSpec.groovy @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.beans.factory.beanannotation + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import spock.lang.Specification + +class PrototypeAnnotationSpec extends Specification{ + + void "test @bean annotation makes a class available as a bean"() { + given: + BeanContext beanContext = new DefaultBeanContext().start() + + expect: + beanContext.getBean(A) != beanContext.getBean(A) // prototype by default + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy new file mode 100644 index 00000000000..0ba4b59f7fa --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/factory/beanproperty/FactoryBeanFieldSpec.groovy @@ -0,0 +1,367 @@ +package io.micronaut.kotlin.processing.beans.factory.beanproperty + +import spock.lang.Specification +import spock.lang.Unroll + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class FactoryBeanFieldSpec extends Specification { + + void "test fail compilation for AOP advice for primitive array type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +class PrimitiveFactory { + + @Bean + @Named("totals") + @Mutating("test") + val totals: Array = arrayOf(10) +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Cannot apply AOP advice to arrays") + } + + void "test fail compilation for AOP advice to primitive type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +import io.micronaut.kotlin.processing.aop.simple.Mutating + +@Factory +class PrimitiveFactory { + + @Bean + @Named("total") + @Mutating("test") + val totals = 10 +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Cannot apply AOP advice to primitive beans") + } + + void "test fail compilation when defining preDestroy for primitive type from field"() { + when: + buildBeanDefinition('primitive.fields.factory.errors.PrimitiveFactory',""" +package primitive.fields.factory.errors + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named + +@Factory +class PrimitiveFactory { + + @Bean(preDestroy="close") + @Named("total") + val totals = 10 +} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("Using 'preDestroy' is not allowed on primitive type beans") + } + + @Unroll + void "test produce bean for primitive #primitiveType array type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + + @Bean + @Named("totals") + val totals: ${primitiveType}Array = ${primitiveType.toLowerCase()}ArrayOf(10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')}) +} + +@Singleton +class MyBean(@Named("totals") val totals: ${primitiveType}Array) { + + @Inject + @Named("totals") + lateinit var totalsFromProperty: ${primitiveType}Array + + var totalsFromMethod: ${primitiveType}Array? = null + + @Inject + fun setTotals(@Named("totals") totals: ${primitiveType}Array) { + this.totalsFromMethod = totals + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.totals[0] == 10 + bean.totalsFromProperty[0] == 10 + bean.totalsFromMethod[0] == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + @Unroll + void "test produce bean for primitive #primitiveType matrix array type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + @Bean + @Named("totals") + val totals: Array<${primitiveType}Array> = arrayOf(${primitiveType.toLowerCase()}ArrayOf(10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')})) +} + +@Singleton +class MyBean(@Named("totals") val totals: Array<${primitiveType}Array>) { + + @Inject + @Named("totals") + lateinit var totalsFromProperty: Array<${primitiveType}Array> + + var totalsFromMethod: Array<${primitiveType}Array>? = null + + @Inject + fun setTotals(@Named("totals") totals: Array<${primitiveType}Array>) { + this.totalsFromMethod = totals + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.totals[0][0] == 10 + bean.totalsFromProperty[0][0] == 10 + bean.totalsFromMethod[0][0] == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + @Unroll + void "test produce bean for primitive #primitiveType type from field"() { + given: + def context = buildContext(""" +package primitive.fields.factory + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class PrimitiveFactory { + @Bean + @Named("total") + val total: $primitiveType = 10${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')} +} + +@Singleton +class MyBean(@Named("total") val total: ${primitiveType}) { + + @Inject + @Named("total") + var totalFromProperty: ${primitiveType} = 0${primitiveType == 'Double' ? '.0' : (primitiveType == 'Float' ? 'F': '')} + + var totalFromMethod: ${primitiveType}? = null + + @Inject + fun setTotals(@Named("total") total: ${primitiveType}) { + this.totalFromMethod = total + } +} +""") + + def bean = getBean(context, 'primitive.fields.factory.MyBean') + + expect: + bean.total == 10 + bean.totalFromProperty == 10 + bean.totalFromMethod == 10 + + where: + primitiveType << ['Int', 'Short', 'Long', 'Double', 'Float', 'Byte'] + } + + /* + void "test a factory bean can be supplied from a field"() { + given: + ApplicationContext context = buildContext('''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import io.micronaut.inject.factory.enummethod.TestEnum; +import jakarta.inject.*; +import java.util.Locale; +import jakarta.inject.Singleton; + +@Factory +class TestFactory$TestField { + + @Singleton + @Bean + @io.micronaut.context.annotation.Primary + Foo one = new Foo("one"); + + // final fields are implicitly singleton + @Bean + @Named("two") + final Foo two = new Foo("two"); + + // non-final fields are prototype + @Bean + @Named("three") + Foo three = new Foo("three"); + + @SomeMeta + @Bean + Foo four = new Foo("four"); + + @Bean + @Mutating + Bar bar = new Bar(); +} + +class Bar { + public String test(String test) { + return test; + } +} + +class Foo { + final String name; + Foo(String name) { + this.name = name; + } +} + +@Retention(RUNTIME) +@Singleton +@Named("four") +@AroundConstruct +@interface SomeMeta { +} + +@Retention(RUNTIME) +@Singleton +@Around +@interface Mutating { +} + +@Singleton +@InterceptorBean(SomeMeta.class) +class TestConstructInterceptor implements ConstructorInterceptor { + boolean invoked = false; + Object[] parameters; + + @Override + public Object intercept(ConstructorInvocationContext context) { + invoked = true; + parameters = context.getParameterValues(); + return context.proceed(); + } +} + +@InterceptorBean(Mutating.class) +class TestInterceptor implements MethodInterceptor { + @Override public Object intercept(MethodInvocationContext context) { + final Object[] parameterValues = context.getParameterValues(); + parameterValues[0] = parameterValues[0].toString().toUpperCase(Locale.ENGLISH); + System.out.println(parameterValues[0]); + return context.proceed(); + } +} +''') + + def barBean = getBean(context, 'test.Bar') + + expect: + barBean.test("good") == 'GOOD' // proxied + getBean(context, "test.Foo").name == 'one' + getBean(context, "test.Foo", Qualifiers.byName("two")).name == 'two' + getBean(context, "test.Foo", Qualifiers.byName("two")).is( + getBean(context, "test.Foo", Qualifiers.byName("two")) + ) + getBean(context, "test.Foo", Qualifiers.byName("three")).is( + getBean(context, "test.Foo", Qualifiers.byName("three")) + ) + getBean(context, 'test.TestConstructInterceptor').invoked == false + getBean(context, "test.Foo", Qualifiers.byName("four")) // around construct + getBean(context, 'test.TestConstructInterceptor').invoked == true + + cleanup: + context.close() + } +*/ + @Unroll + void 'test fail compilation on invalid modifier #modifier'() { + when: + def ctx = buildContext( """ +package invalidmod + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Bean + +@Factory +class TestFactory { + + @Bean + $modifier val test: Test = Test() +} + +class Test +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("cannot be ") + e.message.contains(modifier) + + where: + modifier << ['private'] + + + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy new file mode 100644 index 00000000000..0e406063996 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/beans/inheritance/InheritanceSingletonSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.kotlin.processing.beans.inheritance + +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InheritanceSingletonSpec extends Specification { + + void "test getBeansOfType returns the same instance"() { + def ctx = buildContext(""" +package test + +import jakarta.inject.Singleton + +@Singleton +class BankService: AbstractService() + +abstract class AbstractService: ServiceContract + +interface ServiceContract +""") + + + when: + def bankService = ctx.getBean(ctx.classLoader.loadClass("test.BankService")) + def otherBankService = ctx.getBeansOfType(ctx.classLoader.loadClass("test.ServiceContract"), Qualifiers.byTypeArgumentsClosest(String))[0] + + then: + bankService.is(otherBankService) + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy new file mode 100644 index 00000000000..2e0a606c0b7 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/ast/ClassElementSpec.groovy @@ -0,0 +1,314 @@ +package io.micronaut.kotlin.processing.inject.ast + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.ast.ClassElement +import io.micronaut.inject.ast.ConstructorElement +import io.micronaut.inject.ast.ElementModifier +import io.micronaut.inject.ast.ElementQuery +import io.micronaut.inject.ast.MethodElement +import io.micronaut.inject.ast.PropertyElement +import spock.lang.PendingFeature + +class ClassElementSpec extends AbstractKotlinCompilerSpec { + + void "test class element"() { + expect: + buildClassElement('ast.test.Test', ''' +package ast.test + +import java.lang.IllegalStateException +import kotlin.jvm.Throws + + +class Test( + val publicConstructorReadOnly : String, + private val privateConstructorReadOnly : String, + protected val protectedConstructorReadOnly : Boolean +) : Parent(), One, Two { + + val publicReadOnlyProp : Boolean = true + protected val protectedReadOnlyProp : Boolean? = true + private val privateReadOnlyProp : Boolean? = true + var publicReadWriteProp : Boolean = true + protected var protectedReadWriteProp : String? = "ok" + private var privateReadWriteProp : String = "ok" + private var conventionProp : String = "ok" + + private fun privateFunc(name : String) : String { + return "ok" + } + + open fun openFunc(name : String) : String { + return "ok" + } + + protected fun protectedFunc(name : String) : String { + return "ok" + } + + @Throws(IllegalStateException::class) + override fun publicFunc(name : String) : String { + return "ok" + } + + suspend fun suspendFunc(name : String) : String { + return "ok" + } + + fun getConventionProp() : String { + return conventionProp + } + + fun setConventionProp(name : String) { + this.conventionProp = name + } + + + companion object Helper { + fun publicStatic() : String { + return "ok" + } + + private fun privateStatic() : String { + return "ok" + } + } + + inner class InnerClass1 + + class InnerClass2 +} + +open class Parent : Three { + open fun publicFunc(name : String) : String { + return "ok" + } + + fun parentFunc() : Boolean { + return true + } + + companion object ParentHelper { + fun publicStatic() : String { + return "ok" + } + } +} + +interface One +interface Two +interface Three +''') { ClassElement classElement -> + List constructorElements = classElement.getEnclosedElements(ElementQuery.CONSTRUCTORS) + List allInnerClasses = classElement.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES) + List declaredInnerClasses = classElement.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES.onlyDeclared()) + List propertyElements = classElement.getBeanProperties() + List syntheticProperties = classElement.getSyntheticBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + List declaredMethodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.onlyDeclared()) + List includeOverridden = classElement.getEnclosedElements(ElementQuery.ALL_METHODS.includeOverriddenMethods()) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map declaredMethodMap = declaredMethodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + Map synthPropMap = syntheticProperties.collectEntries { + [it.name, it] + } + Map declaredInnerMap = declaredInnerClasses.collectEntries { + [it.simpleName, it] + } + Map innerMap = allInnerClasses.collectEntries { + [it.simpleName, it] + } + + def overridden = includeOverridden.find { it.declaringType.simpleName == 'Parent' && it.name == 'publicFunc' } + + assert classElement != null + assert classElement.interfaces*.simpleName as Set == ['One', "Two"] as Set + assert methodElements != null + assert !classElement.isAbstract() + assert classElement.name == 'ast.test.Test' + assert !classElement.isPrivate() + assert classElement.isPublic() + assert classElement.modifiers == [ElementModifier.FINAL, ElementModifier.PUBLIC] as Set + assert constructorElements.size() == 1 + assert constructorElements[0].parameters.size() == 3 + assert classElement.superType.isPresent() + assert classElement.superType.get().simpleName == 'Parent' + assert !classElement.superType.get().getSuperType().isPresent() + assert propertyElements.size() == 7 + assert propMap.size() == 7 + assert synthPropMap.size() == 6 + assert methodElements.size() == 8 + assert includeOverridden.size() == 9 + assert declaredMethodElements.size() == 7 + assert propMap.keySet() == ['conventionProp', 'publicReadOnlyProp', 'protectedReadOnlyProp', 'publicReadWriteProp', 'protectedReadWriteProp', 'publicConstructorReadOnly', 'protectedConstructorReadOnly'] as Set + assert synthPropMap.keySet() == ['publicReadOnlyProp', 'protectedReadOnlyProp', 'publicReadWriteProp', 'protectedReadWriteProp', 'publicConstructorReadOnly', 'protectedConstructorReadOnly'] as Set + // inner classes + assert allInnerClasses.size() == 4 + assert declaredInnerClasses.size() == 3 + assert !declaredInnerMap['Test$InnerClass1'].isStatic() + assert declaredInnerMap['Test$InnerClass2'].isStatic() + assert declaredInnerMap['Test$InnerClass1'].isPublic() + assert declaredInnerMap['Test$InnerClass2'].isPublic() + + // read-only public + assert propMap['publicReadOnlyProp'].isReadOnly() + assert !propMap['publicReadOnlyProp'].isWriteOnly() + assert propMap['publicReadOnlyProp'].isPublic() + assert propMap['publicReadOnlyProp'].readMethod.isPresent() + assert propMap['publicReadOnlyProp'].readMethod.get().isSynthetic() + assert !propMap['publicReadOnlyProp'].writeMethod.isPresent() + // read/write public property + assert !propMap['publicReadWriteProp'].isReadOnly() + assert !propMap['publicReadWriteProp'].isWriteOnly() + assert propMap['publicReadWriteProp'].isPublic() + assert propMap['publicReadWriteProp'].readMethod.isPresent() + assert propMap['publicReadWriteProp'].readMethod.get().isSynthetic() + assert propMap['publicReadWriteProp'].writeMethod.isPresent() + assert propMap['publicReadWriteProp'].writeMethod.get().isSynthetic() + // convention prop + assert !propMap['conventionProp'].isReadOnly() + assert !propMap['conventionProp'].isWriteOnly() + assert propMap['conventionProp'].isPublic() + assert propMap['conventionProp'].readMethod.isPresent() + assert !propMap['conventionProp'].readMethod.get().isSynthetic() + assert propMap['conventionProp'].writeMethod.isPresent() + assert !propMap['conventionProp'].writeMethod.get().isSynthetic() + + // methods + assert methodMap.keySet() == ['publicFunc', 'parentFunc', 'openFunc', 'privateFunc', 'protectedFunc', 'suspendFunc', 'getConventionProp', 'setConventionProp'] as Set + assert declaredMethodMap.keySet() == ['publicFunc', 'openFunc', 'privateFunc', 'protectedFunc', 'suspendFunc', 'getConventionProp', 'setConventionProp'] as Set + assert methodMap['suspendFunc'].isSuspend() + assert methodMap['suspendFunc'].returnType.name == String.name + assert methodMap['suspendFunc'].parameters.size() == 1 + assert methodMap['suspendFunc'].suspendParameters.size() == 2 + assert !methodMap['openFunc'].isFinal() + assert !methodMap['publicFunc'].isPackagePrivate() + assert !methodMap['publicFunc'].isPrivate() + assert !methodMap['publicFunc'].isStatic() + assert !methodMap['publicFunc'].isReflectionRequired() + assert methodMap['publicFunc'].hasParameters() + assert methodMap['publicFunc'].thrownTypes.size() == 1 + assert methodMap['publicFunc'].thrownTypes[0].name == IllegalStateException.name + assert methodMap['publicFunc'].isPublic() + assert methodMap['publicFunc'].owningType.name == 'ast.test.Test' + assert methodMap['publicFunc'].declaringType.name == 'ast.test.Test' + assert !methodMap['publicFunc'].isFinal() // should be final? But apparently not + assert overridden != null + assert methodMap['publicFunc'].overrides(overridden) + } + } + + void "test class element generics"() { + expect: + buildClassElement('ast.test.Test', ''' +package ast.test + +/** +* Class docs +* +* @param constructorProp construct prop +*/ +class Test( + val constructorProp : String) : Parent(constructorProp), One { + /** + * Property doc + */ + val publicReadOnlyProp : Boolean = true + override val size: Int = 10 + override fun get(index: Int): String { + return "ok" + } + + open fun openFunc(name : String) : String { + return "ok" + } + + /** + * Method doc + * @param name Param name + */ + override fun publicFunc(name : String) : String { + return "ok" + } +} + +open abstract class Parent(val parentConstructorProp : T) : AbstractMutableList() { + + var parentProp : T = parentConstructorProp + private var conventionProp : T = parentConstructorProp + + fun getConventionProp() : T { + return conventionProp + } + override fun add(index: Int, element: T){ + TODO("Not yet implemented") + } + override fun removeAt(index: Int): T{ + TODO("Not yet implemented") + } + override fun set(index: Int, element: T): T{ + TODO("Not yet implemented") + } + fun setConventionProp(name : T) { + this.conventionProp = name + } + + open fun publicFunc(name : T) : T { + TODO("not yet implemented") + } + + fun parentFunc(name : T) : T { + TODO("not yet implemented") + } + + suspend fun suspendFunc(name : T) : T { + TODO("not yet implemented") + } +} + +interface One +interface Two +interface Three +''') { ClassElement classElement -> + List constructorElements = classElement.getEnclosedElements(ElementQuery.CONSTRUCTORS) + List propertyElements = classElement.getBeanProperties() + List syntheticProperties = classElement.getSyntheticBeanProperties() + List methodElements = classElement.getEnclosedElements(ElementQuery.ALL_METHODS) + Map methodMap = methodElements.collectEntries { + [it.name, it] + } + Map propMap = propertyElements.collectEntries { + [it.name, it] + } + + assert classElement.documentation.isPresent() + assert methodMap['add'].parameters[1].genericType.simpleName == 'String' + assert methodMap['add'].parameters[1].type.simpleName == 'CharSequence' + assert methodMap['iterator'].returnType.firstTypeArgument.get().simpleName == 'Object' + assert methodMap['iterator'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + assert methodMap['stream'].returnType.firstTypeArgument.get().simpleName == 'Object' + assert methodMap['stream'].genericReturnType.firstTypeArgument.get().simpleName == 'String' + assert propMap['conventionProp'].type.simpleName == 'String' + assert propMap['conventionProp'].genericType.simpleName == 'String' + assert propMap['conventionProp'].genericType.simpleName == 'String' + assert propMap['conventionProp'].readMethod.get().returnType.simpleName == 'CharSequence' + assert propMap['conventionProp'].readMethod.get().genericReturnType.simpleName == 'String' + assert propMap['conventionProp'].writeMethod.get().parameters[0].type.simpleName == 'CharSequence' + assert propMap['conventionProp'].writeMethod.get().parameters[0].genericType.simpleName == 'String' + assert propMap['parentConstructorProp'].type.simpleName == 'CharSequence' + assert propMap['parentConstructorProp'].genericType.simpleName == 'String' + assert methodMap['publicFunc'].documentation.isPresent() + assert methodMap['parentFunc'].returnType.simpleName == 'CharSequence' + assert methodMap['parentFunc'].genericReturnType.simpleName == 'String' + assert methodMap['parentFunc'].parameters[0].type.simpleName == 'CharSequence' + assert methodMap['parentFunc'].parameters[0].genericType.simpleName == 'String' + } + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy new file mode 100644 index 00000000000..b791218e659 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesInnerClassSpec.groovy @@ -0,0 +1,29 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class ConfigPropertiesInnerClassSpec extends Specification { + + void "test configuration properties binding with inner class"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ]] + )) + + applicationContext.start() + + MyConfigInner config = applicationContext.getBean(MyConfigInner) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy new file mode 100644 index 00000000000..b734347fa11 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigPropertiesParseSpec.groovy @@ -0,0 +1,1111 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.annotation.processing.test.KotlinCompiler +import io.micronaut.context.ApplicationContext +import io.micronaut.context.BeanContext +import io.micronaut.context.annotation.ConfigurationReader +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.ReadableBytes +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.MethodInjectionPoint +import io.micronaut.kotlin.processing.inject.configuration.Engine +import spock.lang.Specification + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ConfigPropertiesParseSpec extends Specification { + + void "test data classes that are configuration properties inject values"() { + given: + + def config = ['foo.bar.host': 'test', 'foo.bar.baz.stuff': "good"] + def context = buildContext(''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +data class DataConfigTest(val host : String, val child: ChildConfig ) { + @ConfigurationProperties("baz") + data class ChildConfig(var stuff: String) +} +''', false, config) + + def bean = getBean(context, 'test.DataConfigTest') + + expect: + bean.host == 'test' + bean.child.stuff == 'good' + + cleanup: + context.close() + } + + void "test inner class paths - pojo inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* +import java.time.Duration + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + open class ChildConfig: ParentConfig() { + protected var stuff: String? = null + } +} + +open class ParentConfig { + var foo: String? = null +} +''') + then: + beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' + beanDefinition.injectedMethods.size() == 2 + + def setStuff = beanDefinition.injectedMethods.find { it.name == 'setStuff'} + setStuff.getAnnotationMetadata().hasAnnotation(Property) + setStuff.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + setStuff.name == 'setStuff' + + + def setFooMethod = beanDefinition.injectedMethods.find { it.name == 'setFoo'} + setFooMethod.getAnnotationMetadata().hasAnnotation(Property) + setFooMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.foo' + setFooMethod.name == 'setFoo' + } + + void "test inner class paths - fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + + var host: String? = null + + @ConfigurationProperties("baz") + open class ChildConfig { + protected var stuff: String? = null + } +} +''') + then: + beanDefinition.synthesize(ConfigurationReader).prefix() == 'foo.bar.baz' + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - one level"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + + void "test inner class paths - two levels"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + + @ConfigurationProperties("more") + class MoreConfig { + var stuff: String? = null + } + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - with parent inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig: ParentConfig() { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} + +@ConfigurationProperties("parent") +open class ParentConfig +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test setters with two arguments are not injected"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig { + + private var host: String = "localhost" + + fun getHost() = host + + fun setHost(host: String, port: Int) { + this.host = host + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 0 + } + + void "test setters with two arguments from abstract parent are not injected"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +abstract class MyConfig { + private var host: String = "localhost" + + fun getHost() = host + + fun setHost(host: String, port: Int) { + this.host = host + } +} + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inheritance with setters"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +open class MyConfig { + protected var port: Int = 0 + var host: String? = null +} + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 3 + + def stuffMethod = beanDefinition.injectedMethods.find { it.name == 'setStuff'} + stuffMethod.name == 'setStuff' + stuffMethod.getAnnotationMetadata().hasAnnotation(Property) + stuffMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.stuff' + + def setPortMethod = beanDefinition.injectedMethods.find { it.name == 'setPort'} + setPortMethod.name == 'setPort' + setPortMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.port' + + def setHostMethod = beanDefinition.injectedMethods.find { it.name == 'setHost'} + setHostMethod.getAnnotationMetadata().hasAnnotation(Property) + setHostMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.host' + setHostMethod.name == 'setHost' + + } + + void "test annotation on property"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +package test + +import io.micronaut.core.convert.format.* +import io.micronaut.context.annotation.* + +@ConfigurationProperties("http.client") +class HttpClientConfiguration { + @ReadableBytes + var maxContentLength: Int = 1024 * 1024 * 10 // 10MB +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo") +open class MyProperties { + protected var fieldTest: String = "unconfigured" + private val privateFinal = true + protected val protectedFinal = true + private var anotherField: Boolean = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} +''') + then: + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } + beanDefinition.injectedMethods.find {it.name == 'setSetterTest' } + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test configuration properties inheritance from non-configuration properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo") +open class MyProperties: Parent() { + protected var fieldTest: String = "unconfigured" + private val privateFinal = true + protected val protectedFinal = true + private var anotherField: Boolean = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} + +open class Parent { + private var parentField: String? = null + + fun setParentTest(s: String) { + this.parentField = s + } + + fun getParentTest() = parentField +} +''') + then: + beanDefinition.injectedMethods.size() == 3 + + def fieldTest = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} + fieldTest.getAnnotationMetadata().hasAnnotation(Property) + fieldTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' + fieldTest.name == 'setFieldTest' + + def setterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} + setterTest.getAnnotationMetadata().hasAnnotation(Property) + setterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' + setterTest.name == 'setSetterTest' + + def parentTest = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} + parentTest.name == 'setParentTest' + parentTest.getAnnotationMetadata().hasAnnotation(Property) + parentTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' + + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test boolean fields starting with is[A-Z] map to set methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("micronaut.issuer.FooConfigurationProperties", """ +package micronaut.issuer + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +class FooConfigurationProperties { + + private var issuer: String? = null + private var isEnabled = false + + fun setIssuer(issuer: String) { + this.issuer = issuer + } + + //isEnabled field maps to setEnabled method + fun setEnabled(enabled: Boolean) { + this.isEnabled = enabled + } +} +""") + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].name == "setIssuer" + beanDefinition.injectedMethods[1].name == "setEnabled" + } + + void "test includes on fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test includes on methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", includes = ["publicMethod", "parentPublicMethod"]) +class MyProperties: Parent() { + + fun setPublicMethod(value: String) {} + fun setAnotherPublicMethod(value: String) {} +} + +open class Parent { + fun setParentPublicMethod(value: String) {} + fun setAnotherParentPublicMethod(value: String) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } + beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } + } + + void "test excludes on fields"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + } + + void "test excludes on methods"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicMethod", "anotherParentPublicMethod"]) +class MyProperties: Parent() { + + fun setPublicMethod(value: String) {} + fun setAnotherPublicMethod(value: String) {} +} + +open class Parent { + fun setParentPublicMethod(value: String) {} + fun setAnotherParentPublicMethod(value: String) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setParentPublicMethod" } + beanDefinition.injectedMethods.find { it.name == "setPublicMethod" } + } + + void "test excludes on configuration builder"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.inject.configuration.Engine + +@ConfigurationProperties(value = "foo", excludes = ["engine", "engine2"]) +class MyProperties: Parent() { + + @ConfigurationBuilder(prefixes = ["with"]) + val engine: Engine.Builder = Engine.builder() + + @ConfigurationBuilder(configurationPrefix = "two", prefixes = ["with"]) + var engine2: Engine.Builder = Engine.builder() +} + +open class Parent { + fun setEngine(engine: Engine.Builder) {} +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.isEmpty() + beanDefinition.injectedFields.isEmpty() + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'foo.manufacturer':'Subaru', + 'foo.two.manufacturer':'Subaru' + ) + def bean = factory.instantiate(applicationContext) + + then: + ((Engine.Builder) bean.engine).build().manufacturer == 'Subaru' + ((Engine.Builder) bean.getEngine2()).build().manufacturer == 'Subaru' + } + + void "test name is correct with inner classes of non config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +package test + +import io.micronaut.context.annotation.* + +class Test { + + @ConfigurationProperties("test") + class TestNestedConfig { + var vall: String? = null + } +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.vall" + } + + void "test property names with numbers"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("aws") +class AwsConfig { + + var disableEc2Metadata: String? = null + var disableEcMetadata: String? = null + var disableEc2instanceMetadata: String? = null +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" + beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" + beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" + } + + void "test inner interface EachProperty list = true"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child$Intercepted', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +import jakarta.inject.Inject + +@ConfigurationProperties("parent") +class Parent @Inject constructor(val children: List) { + + @EachProperty(value = "children", list = true) + interface Child { + fun getPropA(): String + fun getPropB(): String + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" + beanDefinition.getRequiredMethod("getPropA").getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" + } + + void "test config props with post construct first in file"() { + given: + BeanContext context = buildContext(""" +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import jakarta.annotation.PostConstruct + +@ConfigurationProperties("app.entity") +class EntityProperties { + + @PostConstruct + fun init() { + println("prop = " + prop) + } + + var prop: String? = null +} +""") + + when: + context.getBean(context.classLoader.loadClass("test.EntityProperties")) + + then: + noExceptionThrown() + } + + void "test inner class paths - two levels"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$MoreConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + + @ConfigurationProperties("more") + class MoreConfig { + var stuff: String? = null + } + } +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'foo.bar.baz.more.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test inner class paths - with parent inheritance"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfig: ParentConfig() { + var host: String? = null + + @ConfigurationProperties("baz") + class ChildConfig { + var stuff: String? = null + } +} + +@ConfigurationProperties("parent") +open class ParentConfig +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].getAnnotationMetadata().hasAnnotation(Property) + beanDefinition.injectedMethods[0].getAnnotationMetadata().synthesize(Property).name() == 'parent.foo.bar.baz.stuff' + beanDefinition.injectedMethods[0].name == 'setStuff' + } + + void "test annotation on setters arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.HttpClientConfiguration', ''' +package test + +import io.micronaut.core.convert.format.ReadableBytes +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("http.client") +class HttpClientConfiguration { + + @ReadableBytes + var maxContentLength: Int = 1024 * 1024 * 10 + +} +''') + then: + beanDefinition.injectedFields.size() == 0 + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods[0].arguments[0].synthesize(ReadableBytes) + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +open class MyProperties { + open var fieldTest: String = "unconfigured" + private val privateFinal = true + open val protectedFinal = true + private val anotherField = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} +''') + then: + beanDefinition != null + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == 'setFieldTest' } + beanDefinition.injectedMethods.find { it.name == 'setSetterTest' } + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + } + + void "test configuration properties inheritance from non-configuration properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo") +class MyProperties: Parent() { + + open var fieldTest: String = "unconfigured" + private val privateFinal = true + open val protectedFinal = true + private val anotherField = false + private var internalField = "unconfigured" + + fun setSetterTest(s: String) { + this.internalField = s + } + + fun getSetter() = internalField +} + +open class Parent { + var parentTest: String?= null +} +''') + then: + beanDefinition.injectedMethods.size() == 3 + + def setFieldMethod = beanDefinition.injectedMethods.find { it.name == 'setFieldTest'} + setFieldMethod.name == 'setFieldTest' + setFieldMethod.getAnnotationMetadata().hasAnnotation(Property) + setFieldMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.field-test' + + + def setParentMethod = beanDefinition.injectedMethods.find { it.name == 'setParentTest'} + setParentMethod.name == 'setParentTest' + setParentMethod.getAnnotationMetadata().hasAnnotation(Property) + setParentMethod.getAnnotationMetadata().synthesize(Property).name() == 'foo.parent-test' + + + def setSetterTest = beanDefinition.injectedMethods.find { it.name == 'setSetterTest'} + setSetterTest.name == 'setSetterTest' + setSetterTest.getAnnotationMetadata().hasAnnotation(Property) + setSetterTest.getAnnotationMetadata().synthesize(Property).name() == 'foo.setter-test' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.builder().start() + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "unconfigured" + bean.@fieldTest == "unconfigured" + bean.parentTest == null + + when: + applicationContext.environment.addPropertySource( + "test", + ['foo.setterTest' :'foo', + 'foo.fieldTest' :'bar', + 'foo.parentTest': 'baz'] + ) + bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.setter == "foo" + bean.@fieldTest == "bar" + bean.parentTest == "baz" + } + + void "test includes on properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "foo", includes = ["publicField", "parentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test excludes on properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "foo", excludes = ["anotherPublicField", "anotherParentPublicField"]) +class MyProperties: Parent() { + var publicField: String? = null + var anotherPublicField: String? = null +} + +open class Parent { + var parentPublicField: String? = null + var anotherParentPublicField: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find { it.name == "setPublicField" } + beanDefinition.injectedMethods.find { it.name == "setParentPublicField" } + } + + void "test name is correct with inner classes of non config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.Test\$TestNestedConfig", ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +class Test { + + @ConfigurationProperties("test") + class TestNestedConfig { + var x: String? = null + } + +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].annotationMetadata.getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "test.x" + } + + void "test property names with numbers"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.AwsConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("aws") +class AwsConfig { + + var disableEc2Metadata: String? = null + var disableEcMetadata: String? = null + var disableEc2instanceMetadata: String? = null +} +''') + + then: + noExceptionThrown() + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2-metadata" + beanDefinition.injectedMethods[1].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec-metadata" + beanDefinition.injectedMethods[2].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "aws.disable-ec2instance-metadata" + } + + void "test inner class EachProperty list = true"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Parent$Child', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +import jakarta.inject.Inject + +@ConfigurationProperties("parent") +class Parent(val children: List) { + + @EachProperty(value = "children", list = true) + class Child { + var propA: String? = null + var propB: String? = null + } +} +''') + + then: + noExceptionThrown() + beanDefinition != null + beanDefinition.getAnnotationMetadata().stringValue(ConfigurationReader.class, "prefix").get() == "parent.children[*]" + beanDefinition.injectedMethods[0].getAnnotationMetadata().getAnnotationValuesByType(Property.class).get(0).stringValue("name").get() == "parent.children[*].prop-a" + } + + void "test config props with post construct first in file"() { + given: + BeanContext context = buildContext(""" +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import jakarta.annotation.PostConstruct + +@ConfigurationProperties("app.entity") +class EntityProperties { + + @PostConstruct + fun init() { + println("prop = \$prop") + } + + var prop: String? = null +} +""") + + when: + getBean(context, "test.EntityProperties") + + then: + noExceptionThrown() + } + + void "test configuration properties inheriting config props class"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties(value = "child") +class MyProperties: Parent() { + var childProp: String? = null +} + +@ConfigurationProperties(value = "parent") +open class Parent { + var prop: String? = null +} +''') + then: + noExceptionThrown() + beanDefinition.injectedMethods.size() == 2 + + def setChildProp = beanDefinition.injectedMethods.find { it.name == 'setChildProp'} + setChildProp.name == "setChildProp" + setChildProp.annotationMetadata.stringValue(Property, "name").get() == "parent.child.child-prop" + + def setProp = beanDefinition.injectedMethods.find { it.name == 'setProp'} + setProp.name == "setProp" + setProp.annotationMetadata.stringValue(Property, "name").get() == "parent.prop" + } + + void "test inner each bean internal constructor"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition("test.ParentEachPropsCtor\$ManagerProps", """ +package test + +import io.micronaut.context.annotation.* + +@EachProperty("teams") +class ParentEachPropsCtor internal constructor( + @Parameter val name: String, + val manager: ManagerProps? +) { + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter val name: String) { + var age: Int? = null + } +} +""") + + then: + noExceptionThrown() + beanDefinition != null + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy new file mode 100644 index 00000000000..b20ef8d800e --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesBuilderSpec.groovy @@ -0,0 +1,780 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import org.neo4j.driver.v1.Config +import spock.lang.PendingFeature +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ConfigurationPropertiesBuilderSpec extends Specification { + + void "test configuration builder on method"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build") + var test: Test? = null +} + +class Test private constructor() { + + var foo: String? = null + + companion object { + @JvmStatic + fun build(): Test { + return Test() + } + } +} +''') + + when:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean.test.foo == 'good' + } + + void "test configuration builder with includes"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test; + +import io.micronaut.context.annotation.*; + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build", includes=["foo"]) + var test: Test? = null +} + +class Test private constructor() { + + var foo: String? = null + var bar: String? = null + + companion object { + @JvmStatic + fun build(): Test { + return Test() + } + } +} +''') + + when:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar':'bad' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean.test.foo == 'good' + bean.test.bar == null + } + + void "test catch and log NoSuchMethodError for when underlying builder changes"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder + var test = Test() +} + +class Test { + fun setFoo(s: String) { + throw NoSuchMethodError("setFoo") + } +} +''') + + expect:"The bean was built and a warning was logged" + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + ) + factory.instantiate(applicationContext) + } + + void "test with setters that return void"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder + var test = Test() +} + +class Test { + var foo: String? = null + var bar: Int = 0 + @Deprecated("message") + var baz: Long? = null +} +''') + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar': '10', + 'test.baz':'20' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.test != null + + when: + def test = bean.test + + then: + test.foo == 'good' + test.bar == 10 + test.baz == null //deprecated properties not settable + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.encryptionLevel':'none', + 'neo4j.test.leakedSessionsLogging':true, + 'neo4j.test.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + configurationPrefix="options" + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + value="options" + ) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value using @AccessorsStyle"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.annotation.AccessorsStyle; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + allowZeroArgs = true, + value = "options" + ) + @AccessorsStyle(writePrefixes = ["with"]) + var options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test builder method long and TimeUnit arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + protected var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + var options: Config.ConfigBuilder = Config.build() + +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '6s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 6000 + } + + void "test using a builder that is marked final"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '17s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 17000 + } + + void "test with setter methods that return this"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyProperties', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("test") +class MyProperties { + + @ConfigurationBuilder(factoryMethod="build") + var test: Test? = null +} + +class Test private constructor() { + + private var foo: String? = null + + fun getFoo() = foo + + fun setFoo(foo: String): Test { + this.foo = foo + return this + } + + private var bar: Int = 0 + + fun getBar() = bar + + fun setBar(bar: Int): Test { + this.bar = bar + return this + } + + private var baz: Long? = null + + fun getBaz() = baz + + @Deprecated("do not use") + fun setBaz(baz: Long): Test { + this.baz = baz + return this + } + + companion object { + @JvmStatic fun build(): Test { + return Test() + } + } +} +''') + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'test.foo':'good', + 'test.bar': '10', + 'test.baz':'20' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.test != null + + when: + def test = bean.test + + then: + test.foo == 'good' + test.bar == 10 + test.baz == null //deprecated properties not settable + } + + void "test different inject types for config properties"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.encryptionLevel':'none', + 'neo4j.test.leakedSessionsLogging':true, + 'neo4j.test.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + configurationPrefix="options" + ) + val options: Config.ConfigBuilder = Config.build() +} +''') + then: + beanDefinition.injectedMethods.size() == 1 + beanDefinition.injectedMethods.first().name == 'setUri$main' + + when: + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test; + +import io.micronaut.context.annotation.*; +import org.neo4j.driver.v1.*; + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true, + value="options" + ) + val options: Config.ConfigBuilder = Config.build() + + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test specifying a configuration prefix with value using @AccessorsStyle"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.core.annotation.AccessorsStyle +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + allowZeroArgs = true, + value = "options" + ) + @AccessorsStyle(writePrefixes = ["with"]) + val options: Config.ConfigBuilder = Config.build() +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.options.encryptionLevel':'none', + 'neo4j.test.options.leakedSessionsLogging':true, + 'neo4j.test.options.maxIdleSessions':2 + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.maxIdleConnectionPoolSize() == 2 + config.encrypted() == true // deprecated properties are ignored + config.logLeakedSessions() + } + + void "test builder method long and TimeUnit arguments"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.Neo4jProperties', ''' +package test + +import io.micronaut.context.annotation.* +import org.neo4j.driver.v1.* + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + internal var uri: java.net.URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() + +} +''') + InstantiatableBeanDefinition factory = beanDefinition + ApplicationContext applicationContext = ApplicationContext.run( + 'neo4j.test.connectionLivenessCheckTimeout': '6s' + ) + def bean = factory.instantiate(applicationContext) + + then: + bean != null + bean.options != null + + when: + Config config = bean.options.toConfig() + + then: + config.idleTimeBeforeConnectionTest() == 6000 + } + + void "test configuration builder that are interfaces"() { + given: + ApplicationContext ctx = buildContext(''' +package test + +import io.micronaut.context.annotation.* +import io.micronaut.kotlin.processing.beans.configproperties.AnnWithClass + +@ConfigurationProperties("pool") +class PoolConfig { + + @ConfigurationBuilder(prefixes = [""]) + var builder: ConnectionPool.Builder = DefaultConnectionPool.builder() + +} + +interface ConnectionPool { + + interface Builder { + fun maxConcurrency(maxConcurrency: Int?): Builder + fun foo(foo: Foo): Builder + fun build(): ConnectionPool + } + + fun getMaxConcurrency(): Int? +} + +class DefaultConnectionPool(private val maxConcurrency: Int?): ConnectionPool { + + companion object { + @JvmStatic + fun builder(): ConnectionPool.Builder { + return DefaultBuilder() + } + } + + override fun getMaxConcurrency(): Int? = maxConcurrency + + private class DefaultBuilder: ConnectionPool.Builder { + + private var maxConcurrency: Int? = null + + override fun maxConcurrency(maxConcurrency: Int?): ConnectionPool.Builder{ + this.maxConcurrency = maxConcurrency + return this + } + + override fun foo(foo: Foo): ConnectionPool.Builder { + return this + } + + override fun build(): ConnectionPool{ + return DefaultConnectionPool(maxConcurrency) + } + } +} + +@AnnWithClass(String::class) +interface Foo +''') + ctx.getEnvironment().addPropertySource(PropertySource.of(["pool.max-concurrency": 123])) + + when: + Class testProps = ctx.classLoader.loadClass("test.PoolConfig") + def testPropBean = ctx.getBean(testProps) + + then: + noExceptionThrown() + testPropBean.builder.build().getMaxConcurrency() == 123 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy new file mode 100644 index 00000000000..12ddc6d3654 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesFactorySpec.groovy @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesFactorySpec extends Specification { + + void "test replacing a configuration properties via a factory"() { + ApplicationContext ctx = ApplicationContext.run(["spec.name": ConfigurationPropertiesFactorySpec.simpleName]) + + expect: + ctx.getBean(Neo4jProperties).uri.getHost() == "google.com" + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..6931bc80712 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesSpec.groovy @@ -0,0 +1,133 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import spock.lang.Specification + +class ConfigurationPropertiesSpec extends Specification { + + void "test submap with generics binding"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':10, + 'foo.bar.map.key1.key2.property2.property':10 + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test submap with generics binding and conversion"() { + given: + ApplicationContext ctx = ApplicationContext.run( + 'foo.bar.map.key1.key2.property':'10', + 'foo.bar.map.key1.key2.property2.property':'10' + ) + + expect: + ctx.getBean(MyConfig).map.containsKey('key1') + ctx.getBean(MyConfig).map.get("key1") instanceof Map + ctx.getBean(MyConfig).map.get("key1").get("key2") instanceof MyConfig.Value + ctx.getBean(MyConfig).map.get("key1").get("key2").property == 10 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2 + ctx.getBean(MyConfig).map.get("key1").get("key2").property2.property == 10 + + cleanup: + ctx.close() + } + + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.innerVals': [ + ['expire-unsigned-seconds': 123], ['expireUnsignedSeconds': 600] + ], + 'foo.bar.port':'8080', + 'foo.bar.max-size':'1MB', + 'foo.bar.another-size':'1MB', + 'foo.bar.anotherPort':'9090', + 'foo.bar.intList':"1,2,3", + 'foo.bar.stringList':"1,2", + 'foo.bar.flags.one':'1', + 'foo.bar.flags.two':'2', + 'foo.bar.urlList':"http://test.com, http://test2.com", + 'foo.bar.urlList2':["http://test.com", "http://test2.com"], + 'foo.bar.url':'http://test.com'] + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.innerVals.size() == 2 + config.innerVals[0].expireUnsignedSeconds == 123 + config.innerVals[1].expireUnsignedSeconds == 600 + config.port == 8080 + config.maxSize == 1048576 + config.anotherPort == 9090 + config.intList == [1,2,3] + config.flags == [one:1, two:2] + config.urlList == [new URL('http://test.com'),new URL('http://test2.com')] + config.urlList2 == [new URL('http://test.com'),new URL('http://test2.com')] + config.stringList == ["1", "2"] + config.emptyList == null + config.url.get() == new URL('http://test.com') + !config.anotherUrl.isPresent() + config.defaultPort == 9999 + config.defaultValue == 9999 + } + + void "test configuration inner class properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.inner.enabled':'true', + )) + + applicationContext.start() + + MyConfig config = applicationContext.getBean(MyConfig) + + expect: + config.inner.enabled + } + + void "test binding to a map property"() { + ApplicationContext context = ApplicationContext.run(CollectionUtils.mapOf("map.property.yyy.zzz", 3, "map.property.yyy.xxx", 2, "map.property.yyy.yyy", 3)) + MapProperties config = context.getBean(MapProperties.class) + + expect: + config.property.containsKey('yyy') + + cleanup: + context.close() + } + + void "test camelCase vs kebab_case"() { + ApplicationContext context1 = ApplicationContext.run("rec1") + ApplicationContext context2 = ApplicationContext.run("rec2") + + RecConf config1 = context1.getBean(RecConf.class) + RecConf config2 = context2.getBean(RecConf.class) + + expect: + config1 == config2 + + cleanup: + context1.close() + context2.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy new file mode 100644 index 00000000000..a275d33d696 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ConfigurationPropertiesWithRawMapSpec.groovy @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import spock.lang.Specification + +class ConfigurationPropertiesWithRawMapSpec extends Specification { + + void 'test that injected raw properties are correct'() { + given: + ApplicationContext context = ApplicationContext.run( + 'jpa.properties.hibernate.fooBar':'good', + 'jpa.properties.hibernate.CAP':'whatever' + ) + + expect:"when using StringConvention.RAW the map is injected as is" + context.getBean(MyHibernateConfig) + .properties == ['hibernate.fooBar':'good', 'hibernate.CAP': 'whatever'] + + and:"When not using StringConvention.RAW then you get the normalized versions" + context.getBean(MyHibernateConfig2) + .properties == ['hibernate.foo-bar':'good', 'hibernate.cap': 'whatever'] + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..36fb65b58c6 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ImmutableConfigurationPropertiesSpec.groovy @@ -0,0 +1,164 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultBeanResolutionContext +import io.micronaut.context.annotation.Property +import io.micronaut.core.naming.Named +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import spock.lang.Specification + +import javax.validation.Constraint + +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ImmutableConfigurationPropertiesSpec extends Specification { + + void 'test interface immutable properties'() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('interfaceprops.MyConfig$Intercepted', ''' +package interfaceprops + +import io.micronaut.context.annotation.EachProperty + +@EachProperty("foo.bar") +interface MyConfig { + + @javax.validation.constraints.NotBlank + fun getHost(): String + + fun getPort(): Int +} + + +''') + then: + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost").synthesize(Property).name() == 'foo.bar.*.host' + beanDefinition.getRequiredMethod("getPort").synthesize(Property).name() == 'foo.bar.*.port' + } + + void "test parse immutable configuration properties"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) + +''') + def arguments = beanDefinition.constructor.arguments + then: + beanDefinition instanceof ValidatedBeanDefinition + arguments.length == 2 + arguments[0].synthesize(Property) + .name() == 'foo.bar.host' + arguments[1].synthesize(Property) + .name() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + void "test parse immutable configuration properties - child config"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) { + + @ConfigurationProperties("baz") + class ChildConfig @ConfigurationInject constructor(val stuff: String) +} + +''') + def arguments = beanDefinition.constructor.arguments + then: + arguments.length == 1 + arguments[0].synthesize(Property) + .name() == 'foo.bar.baz.stuff' + + when: + def context = ApplicationContext.run('foo.bar.baz.stuff': 'test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.stuff == 'test' + + cleanup: + context.close() + + } + + void "test parse immutable configuration properties - each property"() { + + when: + ApplicationContext context = buildContext( ''' +package test; + +import io.micronaut.context.annotation.*; +import java.time.Duration; + +@EachProperty("foo.bar") +class MyConfig @ConfigurationInject constructor(@javax.validation.constraints.NotBlank val host: String, val serverPort: Int) +''', false, ['foo.bar.one.host': 'test', 'foo.bar.one.server-port': '9999']) + def config = getBean(context, 'test.MyConfig') + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + + void "test parse immutable configuration properties - init method"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test; + +import io.micronaut.context.annotation.*; +import java.time.Duration; + +@ConfigurationProperties("foo.bar") +class MyConfig { + var host: String? = null + private set + var serverPort: Int = 0 + private set + + @ConfigurationInject + fun init(host: String, serverPort: Int) { + this.host = host + this.serverPort = serverPort + } +} +''') + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..bc475ab685c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/InterfaceConfigurationPropertiesSpec.groovy @@ -0,0 +1,280 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Property +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import io.micronaut.runtime.context.env.ConfigurationAdvice +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class InterfaceConfigurationPropertiesSpec extends Specification { + + + void "test simple interface config props"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + fun getHost(): String? + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} +''') + then: + beanDefinition.getAnnotationMetadata().getAnnotationType(ConfigurationAdvice.class.getName()).isPresent() + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + } + + void "test optional interface config props"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL +import java.util.Optional + +@ConfigurationProperties("foo.bar") +@Executable +interface MyConfig { + + fun getHost(): String? + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Optional + + @io.micronaut.core.bind.annotation.Bindable(defaultValue = "http://default") + fun getURL(): Optional +} + +''') + then: + beanDefinition.getAnnotationMetadata().getAnnotationType(ConfigurationAdvice.class.getName()).isPresent() + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + beanDefinition.getRequiredMethod("getURL") + .stringValue(Property, "name").get() == 'foo.bar.url' + + when: + def context = ApplicationContext.run() + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == null + config.serverPort == Optional.empty() + config.URL == Optional.of(new URL("http://default")) + + when: + def context2 = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999', 'foo.bar.url': 'http://test') + def config2 = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context2) + + then: + config2.host == 'test' + config2.serverPort == Optional.of(9999) + config2.URL == Optional.of(new URL("http://test")) + + cleanup: + context.close() + context2.close() + } + + void "test inheritance interface config props"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test; + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("bar") +interface MyConfig: ParentConfig { + + @Executable + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} + +@ConfigurationProperties("foo") +interface ParentConfig { + + @Executable + @javax.validation.constraints.NotBlank + fun getHost(): String? +} + +''') + then: + beanDefinition instanceof ValidatedBeanDefinition + beanDefinition.getRequiredMethod("getHost") + .stringValue(Property, "name").get() == 'foo.bar.host' + beanDefinition.getRequiredMethod("getServerPort") + .stringValue(Property, "name").get() == 'foo.bar.server-port' + + when: + def context = ApplicationContext.run('foo.bar.host': 'test', 'foo.bar.server-port': '9999') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.host == 'test' + config.serverPort == 9999 + + cleanup: + context.close() + + } + + void "test nested interface config props"() { + + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$ChildConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @Executable + @javax.validation.constraints.NotBlank + fun getHost(): String? + + @Executable + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int + + @ConfigurationProperties("child") + interface ChildConfig { + @Executable + fun getURL(): URL? + } +} +''') + then: + beanDefinition instanceof BeanDefinition + beanDefinition.getRequiredMethod("getURL") + .stringValue(Property, "name").get() == 'foo.bar.child.url' + + when: + def context = ApplicationContext.run('foo.bar.child.url': 'http://test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + + then: + config.URL == new URL("http://test") + + cleanup: + context.close() + } + + void "test nested interface config props - get child"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* +import java.net.URL + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + @Executable + fun getHost(): String + + @javax.validation.constraints.Min(10L) + @Executable + fun getServerPort(): Int + + @Executable + fun getChild(): ChildConfig + + @ConfigurationProperties("child") + interface ChildConfig { + @Executable + fun getURL(): URL? + } +} + +''') + then: + beanDefinition instanceof BeanDefinition + def method = beanDefinition.getRequiredMethod("getChild") + method.isTrue(ConfigurationAdvice, "bean") + + when: + def context = ApplicationContext.run('foo.bar.child.url': 'http://test') + def config = ((InstantiatableBeanDefinition) beanDefinition).instantiate(context) + config.child + + then:"we expect a bean resolution" + def e = thrown(NoSuchBeanException) + e.message.contains("No bean of type [test.MyConfig\$ChildConfig] exists") + + cleanup: + context.close() + } + + void "test invalid method"() { + when: + buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + @javax.validation.constraints.NotBlank + fun junk(s: String): String + + @javax.validation.constraints.Min(10L) + fun getServerPort(): Int +} + +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Only getter methods are allowed on @ConfigurationProperties interfaces: junk(java.lang.String). You can change the accessors using @AccessorsStyle annotation'); + } + + void "test getter that returns void method"() { + when: + buildBeanDefinition('test.MyConfig$Intercepted', ''' +package test + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("foo.bar") +interface MyConfig { + fun getServerPort() +} +''') + then: + def e = thrown(RuntimeException) + e.message.contains('Getter methods must return a value @ConfigurationProperties interfaces') + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy new file mode 100644 index 00000000000..065e56e9198 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/PrimitiveConfigurationPropertiesSpec.groovy @@ -0,0 +1,27 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import spock.lang.Specification + +class PrimitiveConfigurationPropertiesSpec extends Specification { + + // this was just to get the corner case for primitives working + void "test configuration properties binding"() { + given: + ApplicationContext applicationContext = new DefaultApplicationContext("test") + applicationContext.environment.addPropertySource(PropertySource.of( + 'test', + ['foo.bar.port':'8080'] + )) + + applicationContext.start() + + MyPrimitiveConfig config = applicationContext.getBean(MyPrimitiveConfig) + + expect: + config.port == 8080 + config.defaultValue == 9999 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy new file mode 100644 index 00000000000..add763e9d71 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfigurationSpec.groovy @@ -0,0 +1,77 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.context.exceptions.BeanInstantiationException +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.ValidatedBeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class ValidatedConfigurationSpec extends Specification { + + void "test validated config with invalid config"() { + given: + ApplicationContext applicationContext = ApplicationContext.run(["spec.name": getClass().simpleName], "test") + + when: + ValidatedConfig config = applicationContext.getBean(ValidatedConfig) + + then: + applicationContext.getBeanDefinition(ValidatedConfig) instanceof ValidatedBeanDefinition + def e = thrown(BeanInstantiationException) + e.message.contains('url - must not be null') + e.message.contains('name - must not be blank') + + + cleanup: + applicationContext.close() + } + + void "test validated config with valid config"() { + given: + ApplicationContext applicationContext = ApplicationContext.builder() + .properties(["spec.name": getClass().simpleName]) + .environments("test") + .build() + applicationContext.environment.addPropertySource(PropertySource.of( + 'foo.bar.url':'http://localhost', + 'foo.bar.name':'test' + )) + + applicationContext.start() + + when: + ValidatedConfig config = applicationContext.getBean(ValidatedConfig) + + then: + config != null + config.url == new URL("http://localhost") + config.name == 'test' + + cleanup: + applicationContext.close() + } + + void "test config props with @Valid on field is a validating bean definition"() { + when: + BeanDefinition beanDefinition = buildBeanDefinition('test.MyConfig', ''' +package test + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.Pojo + +import javax.validation.Valid + +@ConfigurationProperties("test.valid") +class MyConfig { + + @Valid + var pojos: List? = null +} +''') + + then: + beanDefinition instanceof ValidatedBeanDefinition + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy new file mode 100644 index 00000000000..9292366dc4d --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/VisibilityIssuesSpec.groovy @@ -0,0 +1,74 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.BeanDefinition +import io.micronaut.inject.InstantiatableBeanDefinition +import spock.lang.Specification +import static io.micronaut.annotation.processing.test.KotlinCompiler.* + +class VisibilityIssuesSpec extends Specification { + + void "test extending a class with protected method in a different package fails compilation"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties; + +@ConfigurationProperties("child") +class ChildConfigProperties: ParentConfigProperties() { + var age: Int? = null +} +""") + + when: + def context = ApplicationContext.run( + 'parent.child.age': 22, + 'parent.name': 'Sally', + 'parent.engine.manufacturer': 'Chevy') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + beanDefinition.injectedMethods.size() == 3 + beanDefinition.injectedMethods.find {it.name == "setAge" } + beanDefinition.injectedMethods.find {it.name == "setName" } + beanDefinition.injectedMethods.find {it.name == "setNationality" } + instance.getName() == null //methods that require reflection are not injected + instance.getAge() == 22 + instance.getBuilder().build().getManufacturer() == 'Chevy' + + cleanup: + context.close() + } + + void "test extending a class with protected field in a different package fails compilation"() { + given: + BeanDefinition beanDefinition = buildBeanDefinition("io.micronaut.inject.configproperties.ChildConfigProperties", """ +package io.micronaut.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties + +@ConfigurationProperties("child") +open class ChildConfigProperties: ParentConfigProperties() { + override var name: String? = null +} +""") + + when: + def context = ApplicationContext.run('parent.nationality': 'Italian', 'parent.child.name': 'Sally') + def instance = ((InstantiatableBeanDefinition)beanDefinition).instantiate(context) + + then: + beanDefinition.injectedMethods.size() == 2 + beanDefinition.injectedMethods.find {it.name == "setName" } + beanDefinition.injectedMethods.find {it.name == "setNationality" } + instance.getName() == "Sally" + instance.getNationality() == null //methods that require reflection are not injected + + cleanup: + context.close() + } + +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy new file mode 100644 index 00000000000..2e42a070d75 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/configproperties/itfce/ValidatedInterfaceConfigPropsSpec.groovy @@ -0,0 +1,43 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Specification + +class ValidatedInterfaceConfigPropsSpec extends Specification { + + void 'test validated interface config with invalid config'() { + given: + ApplicationContext context = ApplicationContext.run( + 'my.config.name':'', + 'my.config.foo.name':'', + 'my.config.default.name':'', + 'my.config.foo.nested.bar.name':'', + ) + + when: + context.getBean(MyConfig) + + then: + def e = thrown(BeanInstantiationException) + e.message.contains('MyConfig.getName - must not be blank') + + when: + context.getBean(MyEachConfig, Qualifiers.byName("foo")) + + then: + e = thrown(BeanInstantiationException) + e.message.contains('MyEachConfig.getName - must not be blank') + + when: + context.getBean(MyEachConfig) + + then: + e = thrown(BeanInstantiationException) + e.message.contains('MyEachConfig.getName - must not be blank') + + cleanup: + context.close() + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy new file mode 100644 index 00000000000..d341757b9d9 --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/inject/generics/GenericTypeArgumentsSpec.groovy @@ -0,0 +1,297 @@ +package io.micronaut.kotlin.processing.inject.generics + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.inject.BeanDefinition +import spock.lang.Unroll + +import javax.validation.ConstraintViolationException +import java.util.function.Function +import java.util.function.Supplier + +class GenericTypeArgumentsSpec extends AbstractKotlinCompilerSpec { + void "test generic type arguments with inner classes resolve"() { + given: + def definition = buildBeanDefinition('innergenerics.Outer$FooImpl', ''' +package innergenerics + +class Outer { + + interface Foo + + @jakarta.inject.Singleton + class FooImpl : Foo +} +''') + def itfe = definition.beanType.classLoader.loadClass('innergenerics.Outer$Foo') + + expect: + definition.getTypeParameters(itfe).length == 1 + } + + void "test type arguments with inherited fields"() { + given: + BeanContext context = buildContext('inheritedfields.UserDaoClient', ''' +package inheritedfields + +import jakarta.inject.* + +@Singleton +class UserDaoClient : DaoClient() + +@Singleton +class UserDao : Dao() +class User + +open class DaoClient { + + @Inject + lateinit var dao : Dao +} + +open class Dao + +@Singleton +class FooDao : Dao() +class Foo +''') + def definition = getBeanDefinition(context, 'inheritedfields.UserDaoClient') + + expect: + definition.injectedMethods.first().arguments[0].typeParameters.length == 1 + definition.injectedMethods.first().arguments[0].typeParameters[0].type.simpleName == "User" + getBean(context, 'inheritedfields.UserDaoClient').dao.getClass().simpleName == 'UserDao' + } + + void "test type arguments for exception handler"() { + given: + BeanDefinition definition = buildBeanDefinition('exceptionhandler.Test', '''\ +package exceptionhandler + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import javax.validation.ConstraintViolationException + +@Context +class Test : ExceptionHandler?> { + override fun handle(request : String, e: ConstraintViolationException) : java.util.function.Supplier? { + return null + } +} + +class Foo +interface ExceptionHandler { + fun handle(request : String, exception : T) : R +} +''') + expect: + definition != null + def typeArgs = definition.getTypeArguments("exceptionhandler.ExceptionHandler") + typeArgs.size() == 2 + typeArgs[0].type == ConstraintViolationException + typeArgs[1].type == Supplier + } + + void "test type arguments for factory returning interface"() { + given: + BeanDefinition definition = buildBeanDefinition('factorygenerics.Test$MyFunc0', '''\ +package factorygenerics + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import io.micronaut.context.event.* + +@Factory +class Test { + @Bean + fun myFunc() : BeanCreatedEventListener { + return BeanCreatedEventListener { event -> event.getBean() } + } +} + +interface Foo + +''') + expect: + definition != null + definition.getTypeArguments(BeanCreatedEventListener).size() == 1 + definition.getTypeArguments(BeanCreatedEventListener)[0].type.name == 'factorygenerics.Foo' + } + + @Unroll + void "test generic return type resolution for return type: #returnType"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', """\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* +import java.util.* + +@jakarta.inject.Singleton +class Test { + + @Executable + fun test() : $returnType? { + return null + } +} +""") + def method = definition.getRequiredMethod("test") + + expect: + method.getDescription(true).startsWith("$returnType" ) + + where: + returnType << + ['List>', + 'List>', + 'List', + 'Map'] + } + + void "test type arguments for interface"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : java.util.function.Function{ + + override fun apply(str : String) : Int { + return 10 + } +} + +class Foo +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for inherited interface"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Foo { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + + void "test type arguments for inherited interface 2"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Bar { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Bar : Foo +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for inherited interface - using same name as another type parameter"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@jakarta.inject.Singleton +class Test : Bar { + + override fun apply(str : String) : Int { + return 10 + } +} + +interface Bar : Foo +interface Foo : java.util.function.Function +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } + + void "test type arguments for factory with inheritance"() { + given: + BeanDefinition definition = buildBeanDefinition('test.Test$MyFunc0', '''\ +package test + +import io.micronaut.inject.annotation.* +import io.micronaut.context.annotation.* + +@Factory +class Test { + + @Bean + fun myFunc() : Foo { + return object : Foo { + override fun apply(t: String): Int { + return 10 + } + } + } +} + +interface Foo : java.util.function.Function + +''') + expect: + definition != null + definition.getTypeArguments(Function).size() == 2 + definition.getTypeArguments(Function)[0].name == 'T' + definition.getTypeArguments(Function)[1].name == 'R' + definition.getTypeArguments(Function)[0].type == String + definition.getTypeArguments(Function)[1].type == Integer + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy new file mode 100644 index 00000000000..ec0123849fd --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/BeanIntrospectionSpec.groovy @@ -0,0 +1,2119 @@ +package io.micronaut.kotlin.processing.visitor + +import com.fasterxml.jackson.annotation.JsonClassDescription +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.context.annotation.Executable +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.core.beans.BeanIntrospectionReference +import io.micronaut.core.beans.BeanIntrospector +import io.micronaut.core.beans.BeanMethod +import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.convert.ConversionContext +import io.micronaut.core.convert.TypeConverter +import io.micronaut.core.reflect.InstantiationUtils +import io.micronaut.core.reflect.exception.InstantiationException +import io.micronaut.core.type.Argument +import io.micronaut.inject.ExecutableMethod +import io.micronaut.inject.beans.visitor.IntrospectedTypeElementVisitor +import io.micronaut.inject.beans.visitor.MappedSuperClassIntrospectionMapper +import io.micronaut.kotlin.processing.elementapi.SomeEnum +import io.micronaut.kotlin.processing.elementapi.TestClass +import spock.lang.Specification + +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Version +import javax.validation.Constraint +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Size +import java.lang.reflect.Field + +class BeanIntrospectionSpec extends AbstractKotlinCompilerSpec { + + void "test basic introspection"() { + when: + def introspection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test +""") + + then: + noExceptionThrown() + introspection != null + introspection.instantiate().class.name == "test.Test" + } + + void "test generics in arrays don't stack overflow"() { + given: + def introspection = buildBeanIntrospection('arraygenerics.Test', ''' +package arraygenerics + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected +class Test { + + lateinit var array: Array + lateinit var starArray: Array<*> + lateinit var stringArray: Array + + @Executable + fun myMethod(): Array = array +} +''') + expect: + introspection.beanProperties.size() == 3 + introspection.getRequiredProperty("array", CharSequence[].class).type == CharSequence[].class + introspection.getRequiredProperty("starArray", Object[].class).type == Object[].class + introspection.getRequiredProperty("stringArray", String[].class).type == String[].class + introspection.beanMethods.first().returnType.type == CharSequence[].class + } + + void 'test favor method access'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess + +import io.micronaut.core.annotation.* + +@Introspected(accessKind=[Introspected.AccessKind.METHOD, Introspected.AccessKind.FIELD]) +class Test { + var one: String? = null + private set + get() { + invoked = true + return field + } + var invoked = false +} +''') + + when: + def properties = introspection.getBeanProperties() + def instance = introspection.instantiate() + + then: + properties.size() == 2 + + when: + def one = introspection.getRequiredProperty("one", String) + instance.one = 'test' + + + then: + one.get(instance) == 'test' + instance.invoked + } + + void 'test favor field access'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess; + +import io.micronaut.core.annotation.* + + +@Introspected(accessKind = [Introspected.AccessKind.FIELD, Introspected.AccessKind.METHOD]) +class Test { + var one: String? = null + private set + get() { + invoked = true + return field + } + var invoked = false +} +'''); + when: + def properties = introspection.getBeanProperties() + def instance = introspection.instantiate() + + then: + properties.size() == 2 + + when: + def one = introspection.getRequiredProperty("one", String) + instance.one = 'test' + + then: + one.get(instance) == 'test' + instance.invoked // fields are always private in kotlin so the method will always be referenced + } + + void 'test field access only'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('fieldaccess.Test','''\ +package fieldaccess + +import io.micronaut.core.annotation.* + +@Introspected(accessKind=[Introspected.AccessKind.FIELD]) +open class Test(val two: Integer?) { // read-only + var one: String? = null // read/write + internal var three: String? = null // package protected + protected var four: String? = null // not included since protected + private var five: String? = null // not included since private +} +'''); + when: + def properties = introspection.getBeanProperties() + + then: 'all fields are private in Kotlin' + properties.isEmpty() + } + + void 'test bean constructor'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('beanctor.Test','''\ +package beanctor + +import java.net.URL + +@io.micronaut.core.annotation.Introspected +class Test @com.fasterxml.jackson.annotation.JsonCreator constructor(private val another: String) +''') + + + when: + def constructor = introspection.getConstructor() + def newInstance = constructor.instantiate("test") + + then: + newInstance != null + newInstance.another == "test" + !introspection.getAnnotationMetadata().hasDeclaredAnnotation(com.fasterxml.jackson.annotation.JsonCreator) + constructor.getAnnotationMetadata().hasDeclaredAnnotation(com.fasterxml.jackson.annotation.JsonCreator) + !constructor.getAnnotationMetadata().hasDeclaredAnnotation(Introspected) + !constructor.getAnnotationMetadata().hasAnnotation(Introspected) + !constructor.getAnnotationMetadata().hasStereotype(Introspected) + constructor.arguments.length == 1 + constructor.arguments[0].type == String + } + + void "test generate bean method for introspected class"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.MethodTest', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected +class MethodTest : SuperType(), SomeInt { + + fun nonAnnotated() = true + + @Executable + override fun invokeMe(str: String): String { + return str + } + + @Executable + fun invokePrim(i: Int): Int { + return i + } +} + +open class SuperType { + + @Executable + fun superMethod(str: String): String { + return str + } + + @Executable + open fun invokeMe(str: String): String { + return str + } +} + +interface SomeInt { + + @Executable + fun ok() = true + + fun getName() = "ok" +} +''') + when: + def properties = introspection.getBeanProperties() + Collection beanMethods = introspection.getBeanMethods() + + then: + properties.size() == 1 + beanMethods*.name as Set == ['invokeMe', 'invokePrim', 'superMethod', 'ok'] as Set + beanMethods.every({it.annotationMetadata.hasAnnotation(Executable)}) + beanMethods.every { it.declaringBean == introspection} + + when: + + def invokeMe = beanMethods.find { it.name == 'invokeMe' } + def invokePrim = beanMethods.find { it.name == 'invokePrim' } + def itfeMethod = beanMethods.find { it.name == 'ok' } + def bean = introspection.instantiate() + + then: + invokeMe instanceof ExecutableMethod + invokeMe.invoke(bean, "test") == 'test' + invokePrim.invoke(bean, 10) == 10 + itfeMethod.invoke(bean) == true + } + + void "test custom with prefix"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('customwith.CopyMe', '''\ +package customwith + +import java.net.URL +import java.util.Locale + +@io.micronaut.core.annotation.Introspected(withPrefix = "alter") +class CopyMe(val another: String) { + + fun alterAnother(another: String): CopyMe { + return if (another == this.another) { + this + } else { + CopyMe(another.uppercase(Locale.getDefault())) + } + } +} +''') + when: + def another = introspection.getRequiredProperty("another", String) + def newInstance = introspection.instantiate("test") + + then: + newInstance.another == "test" + + when:"An explicit with method is used" + def result = another.withValue(newInstance, "changed") + + then:"It was invoked" + !result.is(newInstance) + result.another == 'CHANGED' + } + + void "test copy constructor via mutate method"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.CopyMe','''\ +package test + +import java.net.URL +import java.util.Locale + +@io.micronaut.core.annotation.Introspected +class CopyMe(val name: String, + val another: String) { + + var url: URL? = null + + fun withAnother(a: String): CopyMe { + return if (this.another == a) { + this + } else { + CopyMe(this.name, a.uppercase(Locale.getDefault())) + } + } +} +''') + when: + def copyMe = introspection.instantiate("Test", "Another") + def expectUrl = new URL("http://test.com") + copyMe.url = expectUrl + + then: + copyMe.name == 'Test' + copyMe.another == "Another" + copyMe.url == expectUrl + + + when: + def property = introspection.getRequiredProperty("name", String) + def another = introspection.getRequiredProperty("another", String) + def newInstance = property.withValue(copyMe, "Changed") + + then: + !newInstance.is(copyMe) + newInstance.name == 'Changed' + newInstance.url == expectUrl + newInstance.another == "Another" + + when:"the instance is changed with the same value" + def result = property.withValue(newInstance, "Changed") + + then:"The existing instance is returned" + newInstance.is(result) + + when:"An explicit with method is used" + result = another.withValue(newInstance, "changed") + + then:"It was invoked" + !result.is(newInstance) + result.another == 'CHANGED' + } + + void "test secondary constructor for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +data class Foo(val x: Int, val y: Int) { + + constructor(x: Int) : this(x, 20) + + constructor() : this(20, 20) +} +''') + when: + def obj = introspection.instantiate(5, 10) + + then: + obj.getX() == 5 + obj.getY() == 10 + } + + void "test secondary constructor with @Creator for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +data class Foo(val x: Int, val y: Int) { + + @Creator + constructor(x: Int) : this(x, 20) + + constructor() : this(20, 20) +} +''') + when: + def obj = introspection.instantiate(5) + + then: + obj.getX() == 5 + obj.getY() == 20 + } + + void "test annotations on generic type arguments for data classes"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator +import javax.validation.constraints.Min + +@io.micronaut.core.annotation.Introspected +data class Foo(val value: List<@Min(10) Long>) +''') + + when: + BeanProperty property = introspection.getRequiredProperty("value", List) + def genericTypeArg = property.asArgument().getTypeParameters()[0] + + then: + property != null + genericTypeArg.type == Long + genericTypeArg.annotationMetadata.hasStereotype(Constraint) + genericTypeArg.annotationMetadata.hasAnnotation(Min) + genericTypeArg.annotationMetadata.intValue(Min).getAsInt() == 10 + } + + void 'test annotations on generic type arguments'() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import javax.validation.constraints.Min +import kotlin.annotation.AnnotationTarget.* + +@io.micronaut.core.annotation.Introspected +class Foo { + var value : List<@Min(10) @SomeAnn Long>? = null +} + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER, TYPE) +annotation class SomeAnn +''') + when: + BeanProperty property = introspection.getRequiredProperty("value", List) + def genericTypeArg = property.asArgument().getTypeParameters()[0] + + then: + property != null + genericTypeArg.annotationMetadata.hasAnnotation(Min) + genericTypeArg.annotationMetadata.intValue(Min).getAsInt() == 10 + } + + void "test bean introspection on a data class"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +data class Foo(@javax.validation.constraints.NotBlank val name: String, val age: Int) +''') + when: + def test = introspection.instantiate("test", 20) + def property = introspection.getRequiredProperty("name", String) + def argument = introspection.getConstructorArguments()[0] + + then: + argument.name == 'name' + argument.getAnnotationMetadata().hasStereotype(Constraint) + argument.getAnnotationMetadata().hasAnnotation(NotBlank) + test.name == 'test' + test.getName() == 'test' + introspection.propertyNames.length == 2 + introspection.propertyNames == ['name', 'age'] as String[] + property.hasAnnotation(NotBlank) + property.isReadOnly() + property.hasSetterOrConstructorArgument() + property.name == 'name' + property.get(test) == 'test' + + when:"a mutation is applied" + def newTest = property.withValue(test, "Changed") + + then:"a new instance is returned" + !newTest.is(test) + newTest.getName() == 'Changed' + newTest.getAge() == 20 + } + + void "test create bean introspection for external inner class"() { + given: + ClassLoader classLoader = buildClassLoader('test.Foo', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.OuterBean + +@Introspected(classes=[OuterBean.InnerBean::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + String className = "io.micronaut.kotlin.processing.elementapi.OuterBean\$InnerBean" + def innerType = classLoader.loadClass(className) + + then:"The reference is valid" + reference != null + reference.getBeanType().name == className + + when: + BeanIntrospection i = reference.load() + + then: + i.propertyNames.length == 1 + i.propertyNames[0] == 'name' + + when: + innerType.newInstance() + + then: + noExceptionThrown() + + when: + def o = i.instantiate() + + then: + noExceptionThrown() + o.class.name == className + innerType.isInstance(o) + } + + void "test create bean introspection for external inner interface"() { + given: + ClassLoader classLoader = buildClassLoader('test.Foo', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.OuterBean + +@Introspected(classes=[OuterBean.InnerInterface::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + String className = "io.micronaut.kotlin.processing.elementapi.OuterBean\$InnerInterface" + + then:"The reference is valid" + reference != null + reference.getBeanType().name == className + + when: + BeanIntrospection i = reference.load() + + then: + i.propertyNames.length == 1 + i.propertyNames[0] == 'name' + + when: + def o = i.instantiate() + + then: + def e = thrown(InstantiationException) + e.message == 'No default constructor exists' + } + + void "test bean introspection with property of generic interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo : GenBase { + override fun getName() = "test" +} + +interface GenBase { + fun getName(): T +} +''') + when: + def test = introspection.instantiate() + def property = introspection.getRequiredProperty("name", String) + + then: + introspection.beanProperties.first().type == String + property.get(test) == 'test' + !property.hasSetterOrConstructorArgument() + + when: + property.withValue(test, 'try change') + + then: + def e = thrown(UnsupportedOperationException) + e.message =='Cannot mutate property [name] that is not mutable via a setter method, field or constructor argument for type: test.Foo' + } + + void "test bean introspection with property of generic superclass"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo: GenBase() { + override fun getName() = "test" +} + +abstract class GenBase { + abstract fun getName(): T + + fun getOther(): T { + return "other" as T + } +} +''') + when: + def test = introspection.instantiate() + + def beanProperties = introspection.beanProperties.toList() + then: + beanProperties.size() == 2 + beanProperties[0].type == String + beanProperties[1].type == String + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + introspection.getRequiredProperty("other", String) + .get(test) == 'other' + } + + void "test bean introspection with argument of generic interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Foo: GenBase { + override var value: Long? = null +} + +interface GenBase { + var value: T +} + +''') + when: + def test = introspection.instantiate() + BeanProperty bp = introspection.getRequiredProperty("value", Long) + bp.set(test, Long.valueOf(5)) + + then: + bp.get(test) == Long.valueOf(5) + + when: + def returnedBean = bp.withValue(test, Long.valueOf(10)) + + then: + returnedBean.is(test) + bp.get(test) == Long.valueOf(10) + } + + void "test bean introspection with property with static creator method on interface"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +fun interface Foo { + + fun getName(): String + + companion object { + @Creator + fun create(name: String): Foo { + return Foo { name } + } + } +} + +''') + when: + def test = introspection.instantiate("test") + + then: + introspection.constructorArguments.length == 1 + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + } + + void "test bean introspection with property with static creator method on interface with generic type arguments"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Foo', ''' +package test; + +import io.micronaut.core.annotation.Creator; + +@io.micronaut.core.annotation.Introspected +fun interface Foo { + + fun getName(): String + + companion object { + @Creator + fun create(name: String): Foo { + return Foo { name } + } + } +} + +''') + when: + def test = introspection.instantiate("test") + + then: + introspection.constructorArguments.length == 1 + introspection.getRequiredProperty("name", String) + .get(test) == 'test' + } + + void "test bean introspection with property from default interface method"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Test: Foo + +interface Foo { + fun getBar(): String = "good" +} + +''') + when: + def test = introspection.instantiate() + + then: + introspection.getRequiredProperty("bar", String) + .get(test) == 'good' + } + + void "test generate bean introspection for interface"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test','''\ +package test + +@io.micronaut.core.annotation.Introspected +interface Test : io.micronaut.core.naming.Named { + fun setName(name: String) +} +''') + then: + introspection != null + introspection.propertyNames.length == 1 + introspection.propertyNames[0] == 'name' + + when: + introspection.instantiate() + + then: + def e = thrown(InstantiationException) + e.message == 'No default constructor exists' + + when: + def property = introspection.getRequiredProperty("name", String) + String setNameValue + def named = [getName:{-> "test"}, setName:{String n -> setNameValue= n }].asType(introspection.beanType) + + property.set(named, "test") + + then: + property.get(named) == 'test' + setNameValue == 'test' + } + + void "test build introspection"() { + given: + def classLoader = buildClassLoader('test.Address', ''' +package test + +import javax.validation.constraints.* + +@io.micronaut.core.annotation.Introspected +class Address { + + @NotBlank(groups = [GroupOne::class]) + @NotBlank(groups = [GroupThree::class], message = "different message") + @Size(min = 5, max = 20, groups = [GroupTwo::class]) + private var street: String? = null +} + +interface GroupOne +interface GroupTwo +interface GroupThree +''') + def clazz = classLoader.loadClass('test.$Address$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + expect: + reference != null + reference.load() + } + + void "test primary constructor is preferred"() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Book(val title: String) { + + private var author: String? = null + + constructor(title: String, author: String) : this(title) { + this.author = author + } +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == "The Stand" + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test multiple constructors with primary constructor marked as @Creator"() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +import io.micronaut.core.annotation.Creator + +@io.micronaut.core.annotation.Introspected +class Book { + + private var author: String? = null + val title: String + + constructor(title: String, author: String) : this(title) { + this.author = author + } + + @Creator + constructor(title: String) { + this.title = title + } +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == "The Stand" + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test default constructor "() { + given: + def classLoader = buildClassLoader('test.Book', ''' +package test + +@io.micronaut.core.annotation.Introspected +class Book { + var title: String? = null +} +''') + Class clazz = classLoader.loadClass('test.$Book$IntrospectionRef') + BeanIntrospectionReference reference = (BeanIntrospectionReference) clazz.newInstance() + + expect: + reference != null + + when: + BeanIntrospection introspection = reference.load() + + then: + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: "update introspectionMap" + BeanIntrospector introspector = BeanIntrospector.SHARED + Field introspectionMapField = introspector.getClass().getDeclaredField("introspectionMap") + introspectionMapField.setAccessible(true) + introspectionMapField.set(introspector, new HashMap>()); + Map map = (Map) introspectionMapField.get(introspector) + map.put(reference.getName(), reference) + + and: + def book = InstantiationUtils.tryInstantiate(introspection.getBeanType(), ["title": "The Stand"], ConversionContext.of(Argument.of(introspection.beanType))) + def prop = introspection.getRequiredProperty("title", String) + + then: + prop.get(book.get()) == null + + cleanup: + introspectionMapField.set(introspector, null) + } + + void "test multiple constructors with @JsonCreator"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* +import com.fasterxml.jackson.annotation.* + +@Introspected +class Test { + private var name: String? = null + var age: Int = 0 + + @JsonCreator + constructor(@JsonProperty("name") name: String) { + this.name = name + } + + constructor(age: Int) { + this.age = age + } + + fun getName(): String? = name + + fun setName(n: String): Test { + this.name = n + return this + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 2 + + when: + def test = introspection.instantiate("Fred") + def prop = introspection.getRequiredProperty("name", String) + + then: + prop.get(test) == 'Fred' + } + + void "test write bean introspection with builder style properties"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* + +@Introspected +class Test { + private var name: String? = null + + fun getName(): String? = name + fun setName(n: String): Test { + this.name = n + return this + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + + when: + def test = introspection.instantiate() + def prop = introspection.getRequiredProperty("name", String) + prop.set(test, "Foo") + + then: + prop.get(test) == 'Foo' + } + + void "test write bean introspection with inner classes"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import javax.validation.constraints.* +import java.util.* + +@Introspected +class Test { + + private var status: Status? = null + + fun setStatus(status: Status) { + this.status = status + } + + fun getStatus(): Status? { + return this.status + } + + enum class Status { + UP, DOWN + } +} + +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.propertyNames.length == 1 + } + + void "test bean introspection with constructor"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class Test( + @Column(name="test_name") var name: String, + @Size(max=100) var age: Int, + primitiveArray: Array) { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + private var primitiveArray: Array? = null + + private var v: Long? = null + + @Version + fun getAnotherVersion(): Long? { + return v; + } + + fun setAnotherVersion(v: Long) { + this.v = v + } +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + + when:"The introspection is loaded" + BeanIntrospection bi = reference.load() + + then:"it is correct" + bi.getConstructorArguments().length == 3 + bi.getConstructorArguments()[0].name == 'name' + bi.getConstructorArguments()[0].type == String + bi.getConstructorArguments()[1].name == 'age' + bi.getConstructorArguments()[1].getAnnotationMetadata().hasAnnotation(Size) + bi.getIndexedProperties(Id).size() == 1 + bi.getIndexedProperty(Id).isPresent() + !bi.getIndexedProperty(Column, null).isPresent() + bi.getIndexedProperty(Column, "test_name").isPresent() + bi.getIndexedProperty(Column, "test_name").get().name == 'name' + bi.getProperty("version").get().hasAnnotation(Version) + bi.getProperty("anotherVersion").get().hasAnnotation(Version) + // should not inherit metadata from class + !bi.getProperty("anotherVersion").get().hasAnnotation(Entity) + + when: + BeanProperty idProp = bi.getIndexedProperties(Id).first() + + then: + idProp.name == 'id' + !idProp.hasAnnotation(Entity) + !idProp.hasStereotype(Entity) + + + when: + def object = bi.instantiate("test", 10, [20] as Integer[]) + + then: + object.name == 'test' + object.age == 10 + } + + void "test write bean introspection data for entity"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class Test { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + var name: String? = null + + @Size(max=100) + var age: Int? = null +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + + when:"The introspection is loaded" + BeanIntrospection bi = reference.load() + + then:"it is correct" + bi.instantiate() + bi.getIndexedProperties(Id).size() == 1 + bi.getIndexedProperties(Id).first().name == 'id' + } + + void "test write bean introspection data for class in another package"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.OtherTestBean + +@Introspected(classes=[OtherTestBean::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getBeanType().name == "io.micronaut.kotlin.processing.elementapi.OtherTestBean" + + when: + def introspection = reference.load() + + then: "the introspection is under the reference package" + noExceptionThrown() + introspection.class.name == "test.\$io_micronaut_kotlin_processing_elementapi_OtherTestBean\$Introspection" + introspection.instantiate() + } + + void "test write bean introspection data for class already introspected"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.TestBean + +@Introspected(classes=[TestBean::class]) +class Test +''') + + when:"the reference is loaded" + classLoader.loadClass('test.$Test$IntrospectionRef0') + + then:"The reference is not written" + thrown(ClassNotFoundException) + } + + void "test write bean introspection data for package with sources"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* +import io.micronaut.kotlin.processing.elementapi.MarkerAnnotation + +@Introspected(packages = ["io.micronaut.kotlin.processing.elementapi"], includedAnnotations = [MarkerAnnotation::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is generated" + reference != null + } + + void "test write bean introspection data for package with compiled classes"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected(packages=["io.micronaut.inject.beans.visitor"], includedAnnotations=[Internal::class]) +class Test +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getBeanType() == MappedSuperClassIntrospectionMapper + } + + void "test write bean introspection data"() { + given: + def classLoader = buildClassLoader('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.convert.TypeConverter +import javax.validation.constraints.Size + +@Introspected +class Test: ParentBean() { + val readOnly: String = "test" + var name: String? = null + + @Size(max=100) + var age: Int = 0 + + var list: List? = null + var stringArray: Array? = null + var primitiveArray: Array? = null + var flag: Boolean = false + val genericsTest: TypeConverter>? = null + val genericsArrayTest: TypeConverter>? = null +} + +open class ParentBean { + var listOfBytes: List? = null +} +''') + + when:"the reference is loaded" + def clazz = classLoader.loadClass('test.$Test$IntrospectionRef') + BeanIntrospectionReference reference = clazz.newInstance() + + then:"The reference is valid" + reference != null + reference.getAnnotationMetadata().hasAnnotation(Introspected) + reference.isPresent() + reference.beanType.name == 'test.Test' + + when:"the introspection is loaded" + BeanIntrospection introspection = reference.load() + + then:"The introspection is valid" + introspection != null + introspection.hasAnnotation(Introspected) + introspection.instantiate().getClass().name == 'test.Test' + introspection.getBeanProperties().size() == 10 + introspection.getProperty("name").isPresent() + introspection.getProperty("name", String).isPresent() + !introspection.getProperty("name", Integer).isPresent() + + when: + BeanProperty nameProp = introspection.getProperty("name", String).get() + BeanProperty boolProp = introspection.getProperty("flag", boolean.class).get() + BeanProperty ageProp = introspection.getProperty("age", int.class).get() + BeanProperty listProp = introspection.getProperty("list").get() + BeanProperty primitiveArrayProp = introspection.getProperty("primitiveArray").get() + BeanProperty stringArrayProp = introspection.getProperty("stringArray").get() + BeanProperty listOfBytes = introspection.getProperty("listOfBytes").get() + BeanProperty genericsTest = introspection.getProperty("genericsTest").get() + BeanProperty genericsArrayTest = introspection.getProperty("genericsArrayTest").get() + def readOnlyProp = introspection.getProperty("readOnly", String).get() + def instance = introspection.instantiate() + + then: + readOnlyProp.isReadOnly() + nameProp != null + !nameProp.isReadOnly() + !nameProp.isWriteOnly() + nameProp.isReadWrite() + boolProp.get(instance) == false + nameProp.get(instance) == null + ageProp.get(instance) == 0 + genericsTest != null + genericsTest.type == TypeConverter + genericsTest.asArgument().typeParameters.size() == 2 + genericsTest.asArgument().typeParameters[0].type == String + genericsTest.asArgument().typeParameters[1].type == Collection + genericsTest.asArgument().typeParameters[1].typeParameters.length == 1 + genericsArrayTest.type == TypeConverter + genericsArrayTest.asArgument().typeParameters.size() == 2 + genericsArrayTest.asArgument().typeParameters[0].type == String + genericsArrayTest.asArgument().typeParameters[1].type == Object[].class + stringArrayProp.get(instance) == null + stringArrayProp.type == String[] + primitiveArrayProp.get(instance) == null + ageProp.hasAnnotation(Size) + listOfBytes.asArgument().getFirstTypeVariable().get().type == byte[].class + listProp.asArgument().getFirstTypeVariable().isPresent() + listProp.asArgument().getFirstTypeVariable().get().type == Number + + when: + boolProp.set(instance, true) + nameProp.set(instance, "foo") + ageProp.set(instance, 10) + primitiveArrayProp.set(instance, [10] as Integer[]) + stringArrayProp.set(instance, ['foo'] as String[]) + + + then: + boolProp.get(instance) == true + nameProp.get(instance) == 'foo' + ageProp.get(instance) == 10 + stringArrayProp.get(instance) == ['foo'] as String[] + primitiveArrayProp.get(instance) == [10] as Integer[] + + when: + ageProp.convertAndSet(instance, "20") + nameProp.set(instance, "100" ) + + then: + ageProp.get(instance) == 20 + nameProp.get(instance, Integer, null) == 100 + + when: + introspection.instantiate("blah") // illegal argument + + then: + def e = thrown(InstantiationException) + e.message == 'Argument count [1] doesn\'t match required argument count: 0' + } + + void "test constructor argument generics"() { + given: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test(var properties: Map) +''') + expect: + introspection.constructorArguments[0].getTypeVariable("K").get().getType() == String + introspection.constructorArguments[0].getTypeVariable("V").get().getType() == String + } + + void "test static creator"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(name: String): Test { + return Test(name) + } + } +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + introspection.getRequiredProperty("name", String).get(instance) == "Sally" + + when: + introspection.instantiate(new Object[0]) + + then: + thrown(InstantiationException) + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + } + + void "test static creator with no args"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(): Test { + return Test("default") + } + } +} +''') + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + thrown(InstantiationException) + + when: + instance = introspection.instantiate(new Object[0]) + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + + when: + instance = introspection.instantiate() + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + } + + void "test static creator multiple"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test private constructor(val name: String) { + + companion object { + @Creator + fun forName(): Test { + return Test("default") + } + + @Creator + fun forName(name: String): Test { + return Test(name) + } + } +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("Sally") + + then: + introspection.getRequiredProperty("name", String).get(instance) == "Sally" + + when: + instance = introspection.instantiate(new Object[0]) + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + + when: + instance = introspection.instantiate() + + then: + introspection.getRequiredProperty("name", String).get(instance) == "default" + } + + void "test introspections are not created for super classes"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +class Test: Foo() + +open class Foo +''') + + expect: + introspection != null + + when: + introspection.getClass().getClassLoader().loadClass("test.\$Foo\$Introspection") + + then: + thrown(ClassNotFoundException) + } + + void "test enum bean properties"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.* + +@Introspected +enum class Test(val number: Int) { + A(0), B(1), C(2); +} +''') + + expect: + introspection != null + introspection.beanProperties.size() == 1 + introspection.getProperty("number").isPresent() + + when: + def instance = introspection.instantiate("A") + + then: + instance.name() == "A" + introspection.getRequiredProperty("number", int).get(instance) == 0 + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + + when: + introspection.getClass().getClassLoader().loadClass("java.lang.\$Enum\$Introspection") + + then: + thrown(ClassNotFoundException) + } + + void "test instantiating an enum"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +enum class Test { + A, B, C; +} +''') + + expect: + introspection != null + + when: + def instance = introspection.instantiate("A") + + then: + instance.name() == "A" + + when: + introspection.instantiate() + + then: + thrown(InstantiationException) + } + + void "test constructor argument nested generics"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import java.util.List +import java.util.Map + +@Introspected +class Test(map: Map>) + +class Action +''') + + expect: + introspection != null + introspection.constructorArguments[0].typeParameters.size() == 2 + introspection.constructorArguments[0].typeParameters[0].typeName == 'java.lang.String' + introspection.constructorArguments[0].typeParameters[1].typeName == 'java.util.List' + introspection.constructorArguments[0].typeParameters[1].typeParameters.size() == 1 + introspection.constructorArguments[0].typeParameters[1].typeParameters[0].typeName == 'test.Action' + } + + void "test primitive multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + var oneDimension: IntArray? = null + var twoDimensions: Array? = null + var threeDimensions: Array>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", int[].class) + int[] level1 = [1, 2, 3] as int[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", int[][].class) + int[] level2 = [4, 5, 6] as int[] + int[][] twoDimensions = [level1, level2] as int[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", int[][][].class) + int[][][] threeDimensions = [[level1], [level2]] as int[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test class multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + var oneDimension: Array? = null + var twoDimensions: Array>? = null + var threeDimensions: Array>>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", String[].class) + String[] level1 = ["1", "2", "3"] as String[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", String[][].class) + String[] level2 = ["4", "5", "6"] as String[] + String[][] twoDimensions = [level1, level2] as String[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", String[][][].class) + String[][][] threeDimensions = [[level1], [level2]] as String[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test enum multi-dimensional arrays"() { + when: + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import io.micronaut.kotlin.processing.elementapi.SomeEnum + +@Introspected +class Test { + var oneDimension: Array? = null + var twoDimensions: Array>? = null + var threeDimensions: Array>>? = null +} +''') + + then: + noExceptionThrown() + introspection != null + + when: + def instance = introspection.instantiate() + def property = introspection.getRequiredProperty("oneDimension", SomeEnum[].class) + SomeEnum[] level1 = [SomeEnum.A, SomeEnum.B, SomeEnum.A] as SomeEnum[] + property.set(instance, level1) + + then: + property.get(instance) == level1 + + when: + property = introspection.getRequiredProperty("twoDimensions", SomeEnum[][].class) + SomeEnum[] level2 = [SomeEnum.B, SomeEnum.A, SomeEnum.B] as SomeEnum[] + SomeEnum[][] twoDimensions = [level1, level2] as SomeEnum[][] + property.set(instance, twoDimensions) + + then: + property.get(instance) == twoDimensions + + when: + property = introspection.getRequiredProperty("threeDimensions", SomeEnum[][][].class) + SomeEnum[][][] threeDimensions = [[level1], [level2]] as SomeEnum[][][] + property.set(instance, threeDimensions) + + then: + property.get(instance) == threeDimensions + } + + void "test superclass methods are read before interface methods"() { + BeanIntrospection introspection = buildBeanIntrospection('test.Test', ''' +package test + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotNull + +interface IEmail { + fun getEmail(): String? +} + +@Introspected +open class SuperClass: IEmail { + @NotNull + override fun getEmail(): String? = null +} + +@Introspected +class SubClass: SuperClass() + +@Introspected +class Test: SuperClass(), IEmail + +''') + expect: + introspection != null + introspection.getProperty("email").isPresent() + introspection.getIndexedProperties(Constraint).size() == 1 + } + + void "test introspection on abstract class"() { + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +abstract class Test { + var name: String? = null + var author: String? = null +} +""") + + expect: + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 2 + } + + void "test targeting abstract class with @Introspected(classes = "() { + ClassLoader classLoader = buildClassLoader("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected(classes = [io.micronaut.kotlin.processing.elementapi.TestClass::class]) +class MyConfig +""") + + when: + BeanIntrospector beanIntrospector = BeanIntrospector.forClassLoader(classLoader) + + then: + BeanIntrospection beanIntrospection = beanIntrospector.getIntrospection(TestClass) + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 2 + } + + void "test introspection on abstract class with extra getter"() { + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import io.micronaut.core.annotation.Introspected + +@Introspected +abstract class Test { + var name: String? = null + var author: String? = null + + fun getAge(): Int = 0 +} +""") + + expect: + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 3 + } + + void "test class loading is not shared between the introspection and the ref"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test; + +import io.micronaut.core.annotation.Introspected; +import java.util.Set; + +@Introspected(excludedAnnotations = [Deprecated::class]) +public class Test { + var authors: Set? = null +} + +@Introspected(excludedAnnotations = [Deprecated::class]) +class Author { + var name: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + } + + void "test annotation on setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @set:JsonProperty + var foo: String = "bar" +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.hasAnnotation(JsonProperty) + } + + void "test annotation on field"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @field:JsonProperty + var foo: String = "bar" +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.hasAnnotation(JsonProperty) + } + + void "test field annotation overrides getter and setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @field:JsonProperty("field") + @get:JsonProperty("getter") + @set:JsonProperty("setter") + var foo: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.getAnnotation(JsonProperty).stringValue().get() == 'field' + } + + void "test getter annotation overrides setter"() { + when: + BeanIntrospection beanIntrospection = buildBeanIntrospection("test.Test", """ +package test + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Test { + @get:JsonProperty("getter") + @set:JsonProperty("setter") + var foo: String? = null +} +""") + + then: + noExceptionThrown() + beanIntrospection != null + beanIntrospection.getBeanProperties().size() == 1 + beanIntrospection.getBeanProperties()[0].annotationMetadata.getAnnotation(JsonProperty).stringValue().get() == 'getter' + } + + void "test create bean introspection for interface"() { + given: + def classLoader = buildClassLoader('itfcetest.MyInterface',''' +package itfcetest + +import com.fasterxml.jackson.annotation.JsonClassDescription +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected(classes = [MyInterface::class]) +class Test + +@JsonClassDescription +public interface MyInterface { + fun getName(): String + + @Executable + fun name(): String = getName() +} + +class MyImpl: MyInterface { + override fun getName(): String = "ok" +} +''') + when:"the reference is loaded" + def clazz = classLoader.loadClass('itfcetest.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + BeanIntrospection introspection = reference.load() + + then: + introspection.getBeanType().isInterface() + introspection.beanProperties.size() == 1 + introspection.beanMethods.size() == 1 + introspection.hasAnnotation(JsonClassDescription) + } + + void "test create bean introspection for interface - only methods"() { + given: + def classLoader = buildClassLoader('itfcetest.MyInterface',''' +package itfcetest + +import io.micronaut.core.annotation.Introspected +import io.micronaut.context.annotation.Executable + +@Introspected(classes = [MyInterface::class]) +class Test + +interface MyInterface { + @Executable + fun name(): String +} + +class MyImpl: MyInterface { + override fun name(): String = "ok" +} +''') + when:"the reference is loaded" + def clazz = classLoader.loadClass('itfcetest.$Test$IntrospectionRef0') + BeanIntrospectionReference reference = clazz.newInstance() + BeanIntrospection introspection = reference.load() + + then: + introspection.getBeanType().isInterface() + introspection.beanProperties.size() == 0 + introspection.beanMethods.size() == 1 + } +} diff --git a/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy new file mode 100644 index 00000000000..98c96816e1c --- /dev/null +++ b/inject-kotlin/src/test/groovy/io/micronaut/kotlin/processing/visitor/KotlinReconstructionSpec.groovy @@ -0,0 +1,161 @@ +package io.micronaut.kotlin.processing.visitor + +import io.micronaut.annotation.processing.test.AbstractKotlinCompilerSpec +import io.micronaut.inject.ast.GenericPlaceholderElement +import spock.lang.PendingFeature +import spock.lang.Unroll + + +class KotlinReconstructionSpec extends AbstractKotlinCompilerSpec { + @PendingFeature(reason = "Not yet implemented") + @Unroll("field type is #fieldType") + def 'field type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +class Test { + lateinit var field : $fieldType +} +""") + def field = element.getFields()[0] + + expect: + reconstructTypeSignature(field.genericType) == fieldType + + where: + fieldType << [ + 'String', + 'List', + 'List', + 'List>', + 'List', +// 'List', // doesn't work? + 'List>', + 'List>>>' + ] + } + + @Unroll("super type is #superType") + def 'super type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test : $superType() { +} +""") + + expect: + reconstructTypeSignature(element.superType.get()) == superType + + where: + superType << [ +// 'AbstractList', raw types not supported + 'AbstractList', + 'AbstractList', + 'AbstractList>', + 'AbstractList>', + 'AbstractList>>', + 'AbstractList>>>', + 'AbstractList>' + ] + } + + @Unroll("super interface is #superType") + def 'super interface'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test : $superType { +} +""") + + expect: + reconstructTypeSignature(element.interfaces[0]) == superType + + where: + superType << [ +// 'List', + 'List', + 'List', + 'List>', + 'List>', +// 'List>', + 'List>>', + 'List>>>', +// 'List', + 'List>', + ] + } + + @Unroll("type var is #decl") + @PendingFeature + def 'type vars declared on type'() { + given: + def element = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +abstract class Test { +} +""") + + expect: + reconstructTypeSignature(element.declaredGenericPlaceholders[1], true) == decl + + where: + decl << [ + 'T', + 'out T : CharSequence', + 'T : A', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', +// 'T extends List', + ] + } + + @Unroll('declaration is #decl') + @PendingFeature(reason = "Not yet implemented") + def 'fold type variable to null'() { + given: + def classElement = buildClassElement("example.Test", """ +package example; + +import java.util.*; + +class Test { + lateinit var field : $decl; +} +""") + def fieldType = classElement.fields[0].type + + expect: + reconstructTypeSignature(fieldType.foldBoundGenericTypes { + if (it != null && it.isGenericPlaceholder() && ((GenericPlaceholderElement) it).variableName == 'T') { + return null + } else { + return it + } + }) == expected + + where: + decl | expected + 'String' | 'String' + 'List' | 'List' + 'Map' | 'Map' + 'List' | 'List' +// 'List' | 'List' + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt new file mode 100644 index 00000000000..fa8fd47e1de --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/Logged.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(LoggedInterceptor::class) +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Logged diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt new file mode 100644 index 00000000000..ea77fff35cd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/LoggedInterceptor.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext + +class LoggedInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + println("Starting method") + val value = context.proceed() + println("Finished method") + return value + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt new file mode 100644 index 00000000000..e768d06f310 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/adapter/Test.kt @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.adapter + +import io.micronaut.aop.Adapter +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import jakarta.inject.Singleton + +@Singleton +@Requires(property = "foo.bar") +internal class Test { + + var isInvoked = false + private set + + @Adapter(ApplicationEventListener::class) + fun onStartup(event: StartupEvent) { + isInvoked = true + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt new file mode 100644 index 00000000000..fc685ffa5ab --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/AroundConstructAnnTransformer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class AroundConstructAnnTransformer: NamedAnnotationTransformer { + + override fun transform( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .member("kind", InterceptorKind.AROUND_CONSTRUCT) + .member("bindMembers", true) + .build()) + } + + override fun getName(): String { + return "aroundconstructmapperbindingmembers.MyInterceptorBinding" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt new file mode 100644 index 00000000000..b5487336666 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/NamedTestAnnMapper.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class NamedTestAnnMapper: NamedAnnotationMapper { + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .value(name) + .build()) + } + + override fun getName(): String { + return "mapperbinding.TestAnn" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt new file mode 100644 index 00000000000..faab2706980 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/compile/TestStereotypeAnnTransformer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.compile + +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorKind +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.NamedAnnotationTransformer +import io.micronaut.inject.visitor.VisitorContext + +class TestStereotypeAnnTransformer: NamedAnnotationTransformer { + + override fun transform( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + return listOf(AnnotationValue.builder(InterceptorBinding::class.java) + .member("kind", InterceptorKind.AROUND) + .member("bindMembers", true) + .build()) + } + + override fun getName(): String { + return "mapperbindingmembers.MyInterceptorBinding" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt new file mode 100644 index 00000000000..2fcbb771ccd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/AnotherClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.factory + +import jakarta.inject.Singleton + +@Singleton +class AnotherClass diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt new file mode 100644 index 00000000000..56d46518f79 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClass.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.core.annotation.Creator +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import io.micronaut.kotlin.processing.aop.simple.Mutating + +/** + * @author Graeme Rocher + * @since 1.0 + */ +open class ConcreteClass { + private val anotherClass: AnotherClass? + + @Creator + constructor() { + anotherClass = null + } + + constructor(anotherClass: AnotherClass?) { + this.anotherClass = anotherClass + } + + open fun test(name: String): String { + return "Name is $name" + } + + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + open fun test(): String { + return "noargs" + } + + open fun testVoid(name: String) { + assert(name == "changed") + } + + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt new file mode 100644 index 00000000000..c1681508616 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/ConcreteClassFactory.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Primary +import io.micronaut.context.annotation.Prototype +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Named + +@Factory +class ConcreteClassFactory { + + @Prototype + @Mutating("name") + @Primary + fun concreteClass(): ConcreteClass { + return ConcreteClass(AnotherClass()) + } + + @Prototype + @Mutating("name") + @Named("another") + fun anotherImpl(): ConcreteClass { + return ConcreteClass(AnotherClass()) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt new file mode 100644 index 00000000000..2365862ae68 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceClass.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +/** + * @author Graeme Rocher + * @since 1.0 + */ +interface InterfaceClass { + fun test(name: String): String + fun test(name: String, age: Int): String + fun test(): String + fun testVoid(name: String) + fun testVoid(name: String, age: Int) + fun testBoolean(name: String): Boolean + fun testBoolean(name: String, age: Int): Boolean + fun testInt(name: String): Int + fun testLong(name: String): Long + fun testShort(name: String): Short + fun testByte(name: String): Byte + fun testDouble(name: String): Double + fun testFloat(name: String): Float + fun testChar(name: String): Char + fun testByteArray(name: String, data: ByteArray): ByteArray + fun testGenericsWithExtends(name: T, age: Int): T + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt new file mode 100644 index 00000000000..7786993b088 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceFactory.kt @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Primary +import io.micronaut.context.annotation.Prototype +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Named + +@Factory +class InterfaceFactory { + + @Prototype + @Mutating("name") + @Primary + @Executable + fun interfaceClass(): InterfaceClass<*> { + return InterfaceImpl() + } + + @Prototype + @Mutating("name") + @Named("another") + @Executable + fun anotherImpl(): InterfaceClass<*> { + return InterfaceImpl() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt new file mode 100644 index 00000000000..75a0cafe049 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/InterfaceImpl.kt @@ -0,0 +1,86 @@ +package io.micronaut.kotlin.processing.aop.factory; + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +class InterfaceImpl: InterfaceClass { + + override fun test(): String { + return "noargs" + } + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt new file mode 100644 index 00000000000..db791740b8d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/factory/SessionFactoryFactory.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.factory + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import org.hibernate.SessionFactory +import org.hibernate.engine.spi.SessionFactoryDelegatingImpl + +@Factory +class SessionFactoryFactory { + + @Mutating("name") + @Prototype + fun sessionFactory(): SessionFactory { + return SessionFactoryDelegatingImpl(null) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt new file mode 100644 index 00000000000..e17384c8064 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/hotswap/HotswappableProxyingClass.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.aop.hotswap + +import io.micronaut.aop.Around +import io.micronaut.kotlin.processing.aop.proxytarget.Mutating +import jakarta.inject.Singleton + +@Around(proxyTarget = true, hotswap = true) +@Singleton +open class HotswappableProxyingClass { + + var invocationCount = 0 + + @Mutating("name") + open fun test(name: String): String { + invocationCount++ + return "Name is $name" + } + + open fun test2(another: String): String { + invocationCount++ + return "Name is $another" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt new file mode 100644 index 00000000000..733e19c634d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractClass.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Singleton + +@Stub +@Singleton +@Mutating("name") +abstract class AbstractClass : AbstractSuperClass() { + + abstract fun test(name: String): String + + fun nonAbstract(name: String): String { + return test(name) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt new file mode 100644 index 00000000000..6c8bcd94477 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCrudRepo.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +abstract class AbstractCrudRepo { + + abstract fun findById(id: ID): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt new file mode 100644 index 00000000000..1ce5debbba9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomAbstractCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +abstract class AbstractCustomAbstractCrudRepo : AbstractCrudRepo() { + + @Marker + abstract override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt new file mode 100644 index 00000000000..714d01072a3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractCustomCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +abstract class AbstractCustomCrudRepo : CrudRepo { + + @Marker + abstract override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt new file mode 100644 index 00000000000..84fcc4a0580 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/AbstractSuperClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +abstract class AbstractSuperClass : SuperInterface { + + abstract fun test(name: String, age: Int): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt new file mode 100644 index 00000000000..621baaa45b9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ChildIntroduction.kt @@ -0,0 +1,4 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@Stub +interface ChildIntroduction : ParentInterface> diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt new file mode 100644 index 00000000000..5458510e739 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ConcreteClass.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import jakarta.inject.Singleton + +@ListenerAdvice +@Singleton +open class ConcreteClass diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt new file mode 100644 index 00000000000..b9706373c8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CrudRepo.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +interface CrudRepo { + + fun findById(id: ID): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt new file mode 100644 index 00000000000..07721f6152f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/CustomCrudRepo.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import java.util.* + +@RepoDef +interface CustomCrudRepo : CrudRepo { + + @Marker + override fun findById(aLong: Long): Optional +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt new file mode 100644 index 00000000000..afa59aaef8b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/DeleteByIdCrudRepo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import javax.validation.constraints.NotNull + +interface DeleteByIdCrudRepo { + + fun deleteById(@NotNull id: ID) +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt new file mode 100644 index 00000000000..98101b04c18 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InjectParentInterface.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import jakarta.inject.Singleton + +@Singleton +class InjectParentInterface(parentInterface: ParentInterface<*>) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt new file mode 100644 index 00000000000..18989ed5518 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/InterfaceIntroductionClass.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import jakarta.inject.Singleton + +@Stub +@Mutating("name") +@Singleton +interface InterfaceIntroductionClass : SuperInterface { + + fun test(name: String): String + fun test(name: String, age: Int): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt new file mode 100644 index 00000000000..84bcb2f07b2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdvice.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Type +import io.micronaut.context.event.ApplicationEventListener + +/** + * @author graemerocher + * @since 1.0 + */ +@Introduction(interfaces = [ApplicationEventListener::class]) +@Type(ListenerAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Executable +annotation class ListenerAdvice diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt new file mode 100644 index 00000000000..d9829015ae0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceInterceptor.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.util.HashSet + +/** + * @author graemerocher + * @since 1.0 + */ +@Singleton +class ListenerAdviceInterceptor : MethodInterceptor { + + private val recievedMessages: MutableSet = HashSet() + + override fun getOrder(): Int { + return StubIntroducer.POSITION - 10 + } + + fun getRecievedMessages(): Set { + return recievedMessages + } + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + return if (context.methodName == "onApplicationEvent") { + val v = context.parameterValues[0] + recievedMessages.add(v) + null + } else { + context.proceed() + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt new file mode 100644 index 00000000000..cc1e186228a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarker.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ListenerAdviceMarker diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt new file mode 100644 index 00000000000..e6bf4119cd2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ListenerAdviceMarkerMapper.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.inject.annotation.TypedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext +import java.util.ArrayList + +class ListenerAdviceMarkerMapper : TypedAnnotationMapper { + + override fun annotationType(): Class { + return ListenerAdviceMarker::class.java + } + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + val mappedAnnotations: MutableList> = ArrayList() + mappedAnnotations.add( + AnnotationValue.builder( + ListenerAdvice::class.java + ).build() + ) + return mappedAnnotations + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt new file mode 100644 index 00000000000..495d3b56415 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Marker.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@MustBeDocumented +@Retention +annotation class Marker diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt new file mode 100644 index 00000000000..f5410d53c4c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.aop.introduction + +@RepoDef +interface MyRepo : SuperRepo { + + fun aBefore(): String + override fun findAll(): List + fun xAfter(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt new file mode 100644 index 00000000000..651a31fd14f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepo2.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import javax.validation.constraints.NotNull + +@RepoDef +interface MyRepo2 : DeleteByIdCrudRepo { + + override fun deleteById(@NotNull id: Int) +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt new file mode 100644 index 00000000000..944ca133698 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/MyRepoIntroducer.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.lang.reflect.Method +import java.util.ArrayList + +@Singleton +class MyRepoIntroducer : MethodInterceptor { + + var executableMethods = mutableListOf() + + override fun getOrder(): Int { + return 0 + } + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + executableMethods.add(context.executableMethod.targetMethod) + return null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt new file mode 100644 index 00000000000..78b8e891204 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplemented.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(NotImplementedAdvice::class) +@MustBeDocumented +@Retention +annotation class NotImplemented diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt new file mode 100644 index 00000000000..2c64c7c400a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/NotImplementedAdvice.kt @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class NotImplementedAdvice : MethodInterceptor { + var invoked = false + + override fun intercept(context: MethodInvocationContext): Any? { + invoked = true + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt new file mode 100644 index 00000000000..471fe863e46 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/ParentInterface.kt @@ -0,0 +1,3 @@ +package io.micronaut.kotlin.processing.aop.introduction + +interface ParentInterface> diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt new file mode 100644 index 00000000000..c253209a26b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/RepoDef.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(MyRepoIntroducer::class) +@MustBeDocumented +@Retention +annotation class RepoDef diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt new file mode 100644 index 00000000000..bd43ac0d015 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/Stub.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(StubIntroducer::class) +@MustBeDocumented +@Retention +annotation class Stub(val value: String = "") diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt new file mode 100644 index 00000000000..232adee3639 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/StubIntroducer.kt @@ -0,0 +1,28 @@ +package io.micronaut.kotlin.processing.aop.introduction + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.type.MutableArgumentValue +import io.micronaut.core.util.StringUtils +import jakarta.inject.Singleton + +@Singleton +class StubIntroducer : MethodInterceptor { + + override fun getOrder(): Int { + return POSITION + } + + companion object { + const val POSITION = 0 + } + + override fun intercept(context: MethodInvocationContext): Any? { + return context.stringValue(// <3> + Stub::class.java + ).filter { StringUtils.isNotEmpty(it) }.orElseGet { + val iterator: Iterator> = context.parameters.values.iterator() + if (iterator.hasNext()) iterator.next().value?.toString() else null + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt new file mode 100644 index 00000000000..6ef1d465472 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperInterface.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction + +interface SuperInterface { + + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt new file mode 100644 index 00000000000..f7739aacb18 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/SuperRepo.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction + +interface SuperRepo { + + fun findAll(): Iterable +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt new file mode 100644 index 00000000000..597ce0ea73a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/Delegating.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +interface Delegating { + fun test(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt new file mode 100644 index 00000000000..a77c1085dff --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingImpl.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +class DelegatingImpl : Delegating { + + override fun test(): String { + return "good" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt new file mode 100644 index 00000000000..02fde438a3b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingInterceptor.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class DelegatingInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext): Any? { + val executableMethod = context.executableMethod + val parameterValues = context.parameterValues + return if (executableMethod.name == "test2") { + val instance: DelegatingIntroduced = object : DelegatingIntroduced { + override fun test2(): String { + return "good" + } + + override fun test(): String { + return "good" + } + } + executableMethod.invoke(instance, *parameterValues) + } else { + executableMethod.invoke(DelegatingImpl(), *parameterValues) + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt new file mode 100644 index 00000000000..82be9b6817e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegatingIntroduced.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +@DelegationAdvice +interface DelegatingIntroduced : Delegating { + fun test2(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt new file mode 100644 index 00000000000..c7021dfa9cc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/delegation/DelegationAdvice.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.introduction.delegation + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction +@Type(DelegatingInterceptor::class) +@MustBeDocumented +@Retention +annotation class DelegationAdvice diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt new file mode 100644 index 00000000000..14cfe1d9d40 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/temp/MyBean.kt @@ -0,0 +1,18 @@ +package io.micronaut.kotlin.processing.aop.introduction.temp + +import io.micronaut.kotlin.processing.aop.introduction.* +import io.micronaut.context.annotation.* + +@ListenerAdvice +@Stub +@jakarta.inject.Singleton +interface MyBean { + + @Executable + fun getBar(): String + + @Executable + fun getFoo() : String { + return "good" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt new file mode 100644 index 00000000000..16009baeda7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/CustomProxy.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +interface CustomProxy { + fun isProxy(): Boolean +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt new file mode 100644 index 00000000000..dd4203926c3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean1.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroduction +@ProxyAround +open class MyBean1 { + + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt new file mode 100644 index 00000000000..df4d5701ca2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean2.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAround +open class MyBean2 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt new file mode 100644 index 00000000000..389b53c90ee --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean3.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundOneAnnotation +open class MyBean3 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt new file mode 100644 index 00000000000..ccbdb2a0685 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean4.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.core.annotation.Introspected + +@ProxyIntroductionAndAroundOneAnnotation +@Introspected +open class MyBean4 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt new file mode 100644 index 00000000000..8c1e16936dc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean5.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospected +open class MyBean5 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt new file mode 100644 index 00000000000..a001afa9ff1 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean6.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospectedAndExecutable +open class MyBean6 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt new file mode 100644 index 00000000000..bf67ba9c8f9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean7.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.annotation.Executable + +@Executable +@ProxyIntroduction +open class MyBean7 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt new file mode 100644 index 00000000000..ab565fbaeb4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean8.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.context.annotation.Executable + +@Executable +@ProxyAround +open class MyBean8 { + private var id: Long? = null + private var name: String? = null + + open fun getId(): Long? = id + + open fun setId(id: Long?) { + this.id = id + } + + open fun getName(): String? = name + + open fun setId(name: String?) { + this.name = name + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt new file mode 100644 index 00000000000..c6baba97827 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/MyBean9.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@ProxyIntroductionAndAroundAndIntrospectedAndExecutable +open class MyBean9 { + private var multidim: Array>? = null + private var primitiveMultidim: Array? = null + + open fun getMultidim(): Array>? { + return multidim + } + + open fun setMultidim(multidim: Array>?) { + this.multidim = multidim + } + + open fun getPrimitiveMultidim(): Array? { + return primitiveMultidim + } + + open fun setPrimitiveMultidim(primitiveMultidim: Array?) { + this.primitiveMultidim = primitiveMultidim + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt new file mode 100644 index 00000000000..de4228fc145 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ObservableInterceptor.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class ObservableInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext?): Any { + return "World" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt new file mode 100644 index 00000000000..acc881927de --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAdviceInterceptor.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.context.BeanContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton +import java.lang.RuntimeException + +@Singleton +class ProxyAdviceInterceptor(private val beanContext: BeanContext) : MethodInterceptor { + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + if (context.methodName.equals("getId", ignoreCase = true)) { + // Test invocation delegation + return if (context.target is MyBean5) { + val delegate = MyBean5() + delegate.setId(1L) + context.executableMethod.invoke(delegate, *context.parameterValues) + } else if (context.target is MyBean6) { + try { + val proxyTargetMethod = beanContext.getProxyTargetMethod( + MyBean6::class.java, context.methodName, *context.argumentTypes + ) + val delegate = MyBean6() + delegate.setId(1L) + proxyTargetMethod.invoke(delegate, *context.parameterValues) + } catch (e: NoSuchMethodException) { + throw RuntimeException(e) + } + } else { + 1L + } + } + return if (context.methodName.equals("isProxy", ignoreCase = true)) { + true + } else context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt new file mode 100644 index 00000000000..42be6f1ab36 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAround.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(ProxyAroundInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyAround diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt new file mode 100644 index 00000000000..d0b9c840639 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyAroundInterceptor.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton + +@Singleton +class ProxyAroundInterceptor : MethodInterceptor { + + override fun intercept(context: MethodInvocationContext): Any? { + // Intercept everything other when CustomProxy + return if (context.methodName == "getId") { + 1L + } else context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt new file mode 100644 index 00000000000..f862bf31e25 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroduction.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyIntroductionInterceptor::class) +@MustBeDocumented +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyIntroduction diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt new file mode 100644 index 00000000000..92d68e676d8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAround.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +@MustBeDocumented +@Retention +@ProxyIntroduction +@ProxyAround +annotation class ProxyIntroductionAndAround diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt new file mode 100644 index 00000000000..f87c4a05eeb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospected.kt @@ -0,0 +1,15 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type +import io.micronaut.core.annotation.Introspected + +@Around +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Introspected +annotation class ProxyIntroductionAndAroundAndIntrospected diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt new file mode 100644 index 00000000000..47b695e94a6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundAndIntrospectedAndExecutable.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Executable +import io.micronaut.context.annotation.Type +import io.micronaut.core.annotation.Introspected + +@Around(proxyTarget = true) +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS) +@Introspected +@Executable +annotation class ProxyIntroductionAndAroundAndIntrospectedAndExecutable diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt new file mode 100644 index 00000000000..85ddd527f3d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionAndAroundOneAnnotation.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.Around +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Type + +@Around +@Introduction(interfaces = [CustomProxy::class]) +@Type(ProxyAdviceInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class ProxyIntroductionAndAroundOneAnnotation diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt new file mode 100644 index 00000000000..8c08cab7b55 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/introduction/with_around/ProxyIntroductionInterceptor.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.aop.introduction.with_around + +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.core.annotation.Nullable +import jakarta.inject.Singleton + +@Singleton +class ProxyIntroductionInterceptor : MethodInterceptor { + + @Nullable + override fun intercept(context: MethodInvocationContext): Any? { + // Only intercept CustomProxy + if (context.methodName.equals("isProxy", ignoreCase = true)) { + // test introduced interface delegation + val customProxy = object : CustomProxy { + override fun isProxy(): Boolean { + return true + } + } + return context.executableMethod.invoke(customProxy, *context.parameterValues) + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt new file mode 100644 index 00000000000..5d3e913144c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceImpl.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.itfce + +abstract class AbstractInterfaceImpl : InterfaceClass { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt new file mode 100644 index 00000000000..c4ae2f9c7a0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/AbstractInterfaceTypeLevel.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.itfce + +abstract class AbstractInterfaceTypeLevel : InterfaceTypeLevel { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt new file mode 100644 index 00000000000..edb981f3280 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceClass.kt @@ -0,0 +1,88 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import io.micronaut.kotlin.processing.aop.simple.Mutating + +interface InterfaceClass { + + @Mutating("name") + fun test(name: String): String + + @Mutating("age") + fun test(age: Int): String + + @Mutating("name") + fun test(name: String, age: Int): String + + @Mutating("name") + fun test(): String + + @Mutating("name") + fun testVoid(name: String) + + @Mutating("name") + fun testVoid(name: String, age: Int) + + @Mutating("name") + fun testBoolean(name: String): Boolean + + @Mutating("name") + fun testBoolean(name: String, age: Int): Boolean + + @Mutating("name") + fun testInt(name: String): Int + + @Mutating("age") + fun testInt(name: String, age: Int): Int + + @Mutating("name") + fun testLong(name: String): Long + + @Mutating("age") + fun testLong(name: String, age: Int): Long + + @Mutating("name") + fun testShort(name: String): Short + + @Mutating("age") + fun testShort(name: String, age: Int): Short + + @Mutating("name") + fun testByte(name: String): Byte + + @Mutating("age") + fun testByte(name: String, age: Int): Byte + + @Mutating("name") + fun testDouble(name: String): Double + + @Mutating("age") + fun testDouble(name: String, age: Int): Double + + @Mutating("name") + fun testFloat(name: String): Float + + @Mutating("age") + fun testFloat(name: String, age: Int): Float + + @Mutating("name") + fun testChar(name: String): Char + + @Mutating("age") + fun testChar(name: String, age: Int): Char + + @Mutating("name") + fun testByteArray(name: String, data: ByteArray): ByteArray + + @Mutating("name") + fun testGenericsWithExtends(name: T, age: Int): T + + @Mutating("name") + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + + @Mutating("name") + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + + @Mutating("name") + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt new file mode 100644 index 00000000000..0455f099a24 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceImpl.kt @@ -0,0 +1,122 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.inject.Singleton + +@Singleton +open class InterfaceImpl : AbstractInterfaceImpl() { + + override fun test(name: String): String { + return "Name is $name" + } + + override fun test(age: Int): String { + return "Age is $age" + } + + override fun test(): String { + return "noargs" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt new file mode 100644 index 00000000000..760d8757e08 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevel.kt @@ -0,0 +1,27 @@ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.Mutating +import io.micronaut.kotlin.processing.aop.simple.CovariantClass + +@Mutating("name") +interface InterfaceTypeLevel { + fun test(name: String): String + fun test(name: String, age: Int): String + fun test(): String + fun testVoid(name: String) + fun testVoid(name: String, age: Int) + fun testBoolean(name: String): Boolean + fun testBoolean(name: String, age: Int): Boolean + fun testInt(name: String): Int + fun testLong(name: String): Long + fun testShort(name: String): Short + fun testByte(name: String): Byte + fun testDouble(name: String): Double + fun testFloat(name: String): Float + fun testChar(name: String): Char + fun testByteArray(name: String, data: ByteArray): ByteArray + fun testGenericsWithExtends(name: T, age: Int): T + fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass + fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass + fun testGenericsFromType(name: A, age: Int): A +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt new file mode 100644 index 00000000000..ca7172d6f93 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/itfce/InterfaceTypeLevelImpl.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.itfce + +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +open class InterfaceTypeLevelImpl : AbstractInterfaceTypeLevel() { + + override fun test(): String { + return "noargs" + } + + override fun testVoid(name: String) { + assert(name == "changed") + } + + override fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + override fun testBoolean(name: String): Boolean { + return name == "changed" + } + + override fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + override fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + override fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + override fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + override fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + override fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + override fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + override fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + override fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + override fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + override fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + override fun testGenericsFromType(name: Any, age: Int): Any { + return "Name is $name" + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt new file mode 100644 index 00000000000..93c4eb59dfb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/Config.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("config") +class Config(inner: Inner) { + + @ConfigurationProperties("inner") + class Inner +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt new file mode 100644 index 00000000000..cd957fb543e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedFactory.kt @@ -0,0 +1,54 @@ +package io.micronaut.kotlin.processing.aop.named + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Parameter +import io.micronaut.kotlin.processing.aop.Logged +import io.micronaut.runtime.context.scope.Refreshable +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Factory +class NamedFactory { + + @EachProperty(value = "aop.test.named", primary = "default") + @Refreshable + fun namedInterface(@Parameter name: String): NamedInterface { + return object : NamedInterface { + override fun doStuff(): String { + return name + } + } + } + + @Named("first") + @Logged + @Singleton + fun first(): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return "first" + } + } + } + + @Named("second") + @Logged + @Singleton + fun second(): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return "second" + } + } + } + + @EachProperty("other.interfaces") + fun third(config: Config, @Parameter name: String): OtherInterface { + return object : OtherInterface { + override fun doStuff(): String { + return name + } + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt new file mode 100644 index 00000000000..2f9e888e91f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/NamedInterface.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.named + +interface NamedInterface { + fun doStuff(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt new file mode 100644 index 00000000000..d0c4d2ccc19 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherBean.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.aop.named + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +@Singleton +class OtherBean { + + @Inject + @Named("first") + lateinit var first: OtherInterface + + @Inject + @Named("second") + lateinit var second: OtherInterface +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt new file mode 100644 index 00000000000..e91971d1ffe --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/named/OtherInterface.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.aop.named + +interface OtherInterface { + fun doStuff(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt new file mode 100644 index 00000000000..a1ee028d99c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ArgMutatingInterceptor.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class ArgMutatingInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + val m = context.synthesize( + Mutating::class.java + ) + val arg = context.parameters[m.value] as MutableArgumentValue? + if (arg != null) { + val value = arg.value + if (value is Number) { + arg.setValue((value.toInt() * 2)) + } else { + arg.setValue("changed") + } + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt new file mode 100644 index 00000000000..685a259130b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/Mutating.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around(proxyTarget = true) +@Type(ArgMutatingInterceptor::class) +@MustBeDocumented +@Retention +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Mutating(val value: String) + + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt new file mode 100644 index 00000000000..b297355e51b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/proxytarget/ProxyingClass.kt @@ -0,0 +1,164 @@ +package io.micronaut.kotlin.processing.aop.proxytarget + +import io.micronaut.kotlin.processing.aop.simple.Bar +import io.micronaut.kotlin.processing.aop.simple.CovariantClass +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +@Singleton +open class ProxyingClass(private val bar: Bar?) { + + var lifeCycleCount = 0 + var invocationCount = 0 + + @PostConstruct + fun init() { + lifeCycleCount++ + } + + @Mutating("name") + open fun test(name: String): String { + invocationCount++ + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + @Mutating("name") + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + @Mutating("name") + open fun test(): String { + return "noargs" + } + + @Mutating("name") + open fun testVoid(name: String) { + assert(name == "changed") + } + + @Mutating("name") + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + @Mutating("name") + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + @Mutating("name") + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + @Mutating("name") + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + @Mutating("name") + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + @Mutating("name") + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + @Mutating("name") + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + @Mutating("name") + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + @Mutating("name") + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + @Mutating("name") + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + @Mutating("name") + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt new file mode 100644 index 00000000000..02b834cbab9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/AnotherClass.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Mutating("name") +open class AnotherClass { + + // protected methods not proxied + protected fun testProtected(name: String): String { + return "Name is $name" + } + + // protected methods not proxied + private fun testPrivate(name: String): String { + return "Name is $name" + } + + open fun test(name: String): String { + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + open fun test(): String { + return "noargs" + } + + open fun testVoid(name: String) { + assert(name == "changed") + } + + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt new file mode 100644 index 00000000000..4ef395b0bec --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/ArgMutatingInterceptor.kt @@ -0,0 +1,26 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class ArgMutatingInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + val m = context.synthesize( + Mutating::class.java + ) + val arg = context.parameters[m.value] as MutableArgumentValue? + if (arg != null) { + val value = arg.value + if (value is Number) { + arg.setValue(value.toInt() * 2) + } else { + arg.setValue("changed") + } + } + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt new file mode 100644 index 00000000000..6098d18455b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Bar.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.aop.simple + +import jakarta.inject.Singleton + +@Singleton +class Bar diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt new file mode 100644 index 00000000000..2ff2d7234c8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/CovariantClass.kt @@ -0,0 +1,4 @@ +package io.micronaut.kotlin.processing.aop.simple + +data class CovariantClass(private val value: T) { +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt new file mode 100644 index 00000000000..7cee90a100b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Invalid.kt @@ -0,0 +1,14 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type + +@Around +@Type(InvalidInterceptor::class) +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.CLASS +) +annotation class Invalid diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt new file mode 100644 index 00000000000..5624236144b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/InvalidInterceptor.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Interceptor +import io.micronaut.aop.InvocationContext +import io.micronaut.core.type.Argument +import io.micronaut.core.type.MutableArgumentValue +import jakarta.inject.Singleton + +@Singleton +class InvalidInterceptor : Interceptor { + + override fun intercept(context: InvocationContext): Any? { + context.parameters["test"] = MutableArgumentValue.create( + Argument.STRING, + "value" + ) + return context.proceed() + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt new file mode 100644 index 00000000000..ae2937a8902 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/Mutating.kt @@ -0,0 +1,20 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around +import io.micronaut.context.annotation.Type +import java.lang.annotation.Inherited + +@Around +@Type(ArgMutatingInterceptor::class) +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.CLASS, + AnnotationTarget.FIELD +) +@Inherited +annotation class Mutating(val value: String) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt new file mode 100644 index 00000000000..569c8e3a30c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/SimpleClass.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.aop.simple + +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +@Singleton +open class SimpleClass(private val bar: Bar?) { + var isPostConstructInvoked = false + private set + + init { + assert(bar != null) + } + + @PostConstruct + fun onCreate() { + isPostConstructInvoked = true + } + + @Mutating("name") + open fun test(name: String): String { + return "Name is $name" + } + + @Mutating("age") + open fun test(age: Int): String { + return "Age is $age" + } + + @Mutating("name") + open fun test(name: String, age: Int): String { + return "Name is $name and age is $age" + } + + @Mutating("name") + open fun test(): String { + return "noargs" + } + + @Mutating("name") + open fun testVoid(name: String) { + assert(name == "changed") + } + + @Mutating("name") + open fun testVoid(name: String, age: Int) { + assert(name == "changed") + assert(age == 10) + } + + @Mutating("name") + open fun testBoolean(name: String): Boolean { + return name == "changed" + } + + @Mutating("name") + open fun testBoolean(name: String, age: Int): Boolean { + assert(age == 10) + return name == "changed" + } + + @Mutating("name") + open fun testInt(name: String): Int { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testInt(name: String, age: Int): Int { + assert(name == "test") + return age + } + + @Mutating("name") + open fun testLong(name: String): Long { + return if (name == "changed") 1 else 0 + } + + @Mutating("age") + open fun testLong(name: String, age: Int): Long { + assert(name == "test") + return age.toLong() + } + + @Mutating("name") + open fun testShort(name: String): Short { + return (if (name == "changed") 1 else 0).toShort() + } + + @Mutating("age") + open fun testShort(name: String, age: Int): Short { + assert(name == "test") + return age.toShort() + } + + @Mutating("name") + open fun testByte(name: String): Byte { + return (if (name == "changed") 1 else 0).toByte() + } + + @Mutating("age") + open fun testByte(name: String, age: Int): Byte { + assert(name == "test") + return age.toByte() + } + + @Mutating("name") + open fun testDouble(name: String): Double { + return if (name == "changed") 1.0 else 0.0 + } + + @Mutating("age") + open fun testDouble(name: String, age: Int): Double { + assert(name == "test") + return age.toDouble() + } + + @Mutating("name") + open fun testFloat(name: String): Float { + return if (name == "changed") 1F else 0F + } + + @Mutating("age") + open fun testFloat(name: String, age: Int): Float { + assert(name == "test") + return age.toFloat() + } + + @Mutating("name") + open fun testChar(name: String): Char { + return (if (name == "changed") 1 else 0).toChar() + } + + @Mutating("age") + open fun testChar(name: String, age: Int): Char { + assert(name == "test") + return age.toChar() + } + + @Mutating("name") + open fun testByteArray(name: String, data: ByteArray): ByteArray { + assert(name == "changed") + return data + } + + @Mutating("name") + open fun testGenericsWithExtends(name: T, age: Int): T { + return "Name is $name" as T + } + + @Mutating("name") + open fun testListWithWildCardIn(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testListWithWildCardOut(name: T, p2: CovariantClass): CovariantClass { + return CovariantClass(name.toString()) + } + + @Mutating("name") + open fun testGenericsFromType(name: A, age: Int): A { + return "Name is $name" as A + } + + @Invalid + open fun invalidInterceptor() { + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt new file mode 100644 index 00000000000..7a05ea30fa2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/aop/simple/TestBinding.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.aop.simple + +import io.micronaut.aop.Around + +@Around +@MustBeDocumented +@Retention +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.CLASS +) +annotation class TestBinding diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt new file mode 100644 index 00000000000..75b55d4b7f9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/aliasfor/TestAnnotation.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.aliasfor + +import io.micronaut.context.annotation.AliasFor +import io.micronaut.context.annotation.Executable +import jakarta.inject.Named +import jakarta.inject.Singleton + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target( + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Singleton +@Executable +annotation class TestAnnotation ( + @get:AliasFor(annotation = Named::class, member = "value") + val value: String = "" +) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt new file mode 100644 index 00000000000..a1bd9834d2c --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MyIterable.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton + +@Singleton +class MyIterable : Iterable { + override fun iterator(): MutableIterator { + return object : MutableIterator { + override fun hasNext(): Boolean { + return false + } + + override fun next(): String? { + return null + } + + override fun remove() { + + } + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt new file mode 100644 index 00000000000..df2a15de1d5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/MySetOfStrings.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton +import java.util.HashSet + +@Singleton +class MySetOfStrings : HashSet() { + init { + add("foo") + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt new file mode 100644 index 00000000000..65be801492a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMyIterable.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Singleton + +@Singleton +class ThingThatNeedsMyIterable(myIterable: MyIterable) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt new file mode 100644 index 00000000000..3f53c8afa90 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/collect/ThingThatNeedsMySetOfStrings.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.collect + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class ThingThatNeedsMySetOfStrings(var strings: MySetOfStrings) { + + @Inject + var otherStrings: MySetOfStrings? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt new file mode 100644 index 00000000000..99ae8d9d9f4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/AnnWithClass.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import kotlin.reflect.KClass + +@MustBeDocumented +@Retention +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class AnnWithClass(val value: KClass<*>) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt new file mode 100644 index 00000000000..d7c42f3e5f6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MapProperties.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("map") +class MapProperties { + var setter: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt new file mode 100644 index 00000000000..c2a93fa32eb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfig.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.ReadableBytes +import java.net.URL +import java.util.* + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port = 0 + var defaultValue = 9999 + var stringList: List? = null + var intList: List? = null + var urlList: List? = null + var urlList2: List? = null + var emptyList: List? = null + var flags: Map? = null + var url: Optional? = null + var anotherUrl = Optional.empty() + var inner: Inner? = null + var defaultPort = 9999 + protected set + var anotherPort: Int? = null + protected set + var innerVals: List? = null + + @ReadableBytes + var maxSize = 0 + + @ReadableBytes + var anotherSize = 0 + var map: Map> = HashMap() + + class Value { + var property = 0 + var property2: Value2? = null + + constructor() {} + constructor(property: Int, property2: Value2?) { + this.property = property + this.property2 = property2 + } + } + + class Value2 { + var property = 0 + + constructor() {} + constructor(property: Int) { + this.property = property + } + } + + @ConfigurationProperties("inner") + class Inner { + var enabled = false + fun isEnabled(): Boolean { + return enabled + } + } +} + +class InnerVal { + var expireUnsignedSeconds: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt new file mode 100644 index 00000000000..205fc317f83 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/MyConfigInner.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfigInner { + var innerVals: List? = null + + class InnerVal { + var expireUnsignedSeconds: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt new file mode 100644 index 00000000000..18aa6dd60a6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jProperties.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +import org.neo4j.driver.v1.Config +import java.net.URI +import java.net.URISyntaxException + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + var uri: URI? = null + + @ConfigurationBuilder(prefixes = ["with"], allowZeroArgs = true) + var options = Config.build() +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt new file mode 100644 index 00000000000..23a106f2dea --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Neo4jPropertiesFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.net.URI +import java.net.URISyntaxException + +@Factory +class Neo4jPropertiesFactory { + + @Singleton + @Replaces(Neo4jProperties::class) + @Requires(property = "spec.name", value = "ConfigurationPropertiesFactorySpec") + fun neo4jProperties(): Neo4jProperties { + val props = Neo4jProperties() + try { + props.uri = URI("https://google.com") + } catch (e: URISyntaxException) { + } + return props + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt new file mode 100644 index 00000000000..aeee41d218a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/Pojo.kt @@ -0,0 +1,18 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.core.annotation.Introspected + +import javax.validation.constraints.Email +import javax.validation.constraints.NotBlank + +@Introspected +class Pojo { + + @Email(message = "Email should be valid") + var email: String? = null + + @NotBlank + var name: String? = null + +} + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt new file mode 100644 index 00000000000..5c09f752526 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/RecConf.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.beans.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.* + +@ConfigurationProperties("rec") +class RecConf { + var namesListOf: List? = null + var mapChildren: Map? = null + var listChildren: List? = null + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val recConf = o as RecConf + return namesListOf == recConf.namesListOf && + mapChildren == recConf.mapChildren && + listChildren == recConf.listChildren + } + + override fun hashCode(): Int { + return Objects.hash(namesListOf, mapChildren, listChildren) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt new file mode 100644 index 00000000000..5c0a45549bd --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/ValidatedConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties; + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires + +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import java.net.URL + +@Requires(property = "spec.name", value = "ValidatedConfigurationSpec") +@ConfigurationProperties("foo.bar") +class ValidatedConfig { + + @NotNull + var url: URL? = null + + @NotBlank + internal var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt new file mode 100644 index 00000000000..b32c211ea03 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ChildConfig.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("baz") +class ChildConfig : MyConfig() { + var stuff: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt new file mode 100644 index 00000000000..b086b7847e6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port = 0 + var host: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt new file mode 100644 index 00000000000..1b7afa9f8b4 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/MyOtherConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.baz") +class MyOtherConfig : ParentPojo() { + var otherProperty: String? = null + var onlySetter: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt new file mode 100644 index 00000000000..df859ad5b13 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachProps.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachProps internal constructor(@Parameter private val index: Int) : Ordered { + var wins: Int? = null + var manager: ManagerProps? = null + + override fun getOrder(): Int { + return index + } + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter private val index: Int) : Ordered { + var age: Int? = null + + override fun getOrder(): Int { + return index + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt new file mode 100644 index 00000000000..4d29f440e1b --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentArrayEachPropsCtor.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.annotation.Nullable +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachPropsCtor internal constructor( + @Parameter private val index: Int, + val manager: ManagerProps? +) : Ordered { + var wins: Int? = null + + override fun getOrder(): Int = index + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter private val index: Int) : Ordered { + var age: Int? = null + + override fun getOrder(): Int = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt new file mode 100644 index 00000000000..8d1abdffd3e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachProps.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +@EachProperty("teams") +class ParentEachProps { + var wins: Int? = null + var manager: ManagerProps? = null + + @ConfigurationProperties("manager") + class ManagerProps { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt new file mode 100644 index 00000000000..283c8af39c2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentEachPropsCtor.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.annotation.Nullable + +@EachProperty("teams") +class ParentEachPropsCtor internal constructor( + @Parameter val name: String, + val manager: ManagerProps? +) { + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps internal constructor(@Parameter val name: String) { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt new file mode 100644 index 00000000000..719c11ce6da --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configproperties/inheritance/ParentPojo.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.configproperties.inheritance + +open class ParentPojo { + var port = 0 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt new file mode 100644 index 00000000000..cf238fe185f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/configuration/Engine.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.beans.configuration + +class Engine(val manufacturer: String) { + + class Builder { + private var manufacturer = "Ford" + + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun build(): Engine { + return Engine(manufacturer) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt new file mode 100644 index 00000000000..2f45d58b4f0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.annotation.Executable +import jakarta.inject.Inject + +@Executable +class BookController { + + @Inject + lateinit var bookService: BookService + + @Executable + fun show(id: Long?): String { + return String.format("%d - The Stand", id) + } + + @Executable + fun showArray(id: Array): String { + return String.format("%d - The Stand", id[0]) + } + + @Executable + fun showPrimitive(id: Long): String { + return String.format("%d - The Stand", id) + } + + @Executable + fun showPrimitiveArray(id: LongArray): String { + return String.format("%d - The Stand", id[0]) + } + + @Executable + fun showVoidReturn(jobNames: MutableList) { + jobNames.add("test") + } + + @Executable + fun showPrimitiveReturn(values: IntArray): Int { + return values[0] + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt new file mode 100644 index 00000000000..8213d2cc910 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/BookService.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.executable + +import jakarta.inject.Singleton + +@Singleton +class BookService diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt new file mode 100644 index 00000000000..989082e78d0 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/executable/RepeatableExecutable.kt @@ -0,0 +1,7 @@ +package io.micronaut.kotlin.processing.beans.executable + +import io.micronaut.context.annotation.Executable + +@Repeatable +@Executable(processOnStartup = true) +annotation class RepeatableExecutable(val value: String) diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt new file mode 100644 index 00000000000..acde1f29523 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/beans/factory/beanannotation/A.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.beans.factory.beanannotation + +import io.micronaut.context.annotation.Prototype + +@Prototype +class A diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt new file mode 100644 index 00000000000..a09ac58b3ec --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/EntityAnnotationMapper.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.elementapi + +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.core.annotation.Introspected +import io.micronaut.core.annotation.NonNull +import io.micronaut.inject.annotation.NamedAnnotationMapper +import io.micronaut.inject.visitor.VisitorContext + +class EntityAnnotationMapper : NamedAnnotationMapper { + @NonNull + override fun getName(): String { + return "javax.persistence.Entity" + } + + override fun map( + annotation: AnnotationValue, + visitorContext: VisitorContext + ): List> { + val builder = AnnotationValue.builder( + Introspected::class.java + ) // don't bother with transients properties + .member("excludedAnnotations", "javax.persistence.Transient") // following are indexed for fast lookups + .member( + "indexed", + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Id").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Version").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.GeneratedValue").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Basic").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Embedded").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.OneToMany").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.OneToOne").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.ManyToOne").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.ElementCollection").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Enumerated").build(), + AnnotationValue.builder( + Introspected.IndexedAnnotation::class.java + ) + .member("annotation", "javax.persistence.Column") + .member("member", "name").build() + ) + return listOf>( + builder.build() + ) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt new file mode 100644 index 00000000000..8ec10244dc8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/Foo.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +class Foo: GenBase { + override var value: Long? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt new file mode 100644 index 00000000000..98338e7c02f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/GenBase.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +interface GenBase { + var value: T +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt new file mode 100644 index 00000000000..14478918a59 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/MarkerAnnotation.kt @@ -0,0 +1,3 @@ +package io.micronaut.kotlin.processing.elementapi + +annotation class MarkerAnnotation diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt new file mode 100644 index 00000000000..6328c0a7ce2 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OtherTestBean.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.elementapi + +@MarkerAnnotation +class OtherTestBean { + var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt new file mode 100644 index 00000000000..d16c756535f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/OuterBean.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.elementapi + +class OuterBean { + + class InnerBean { + var name: String? = null + } + + interface InnerInterface { + fun getName(): String + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt new file mode 100644 index 00000000000..8ea0e7e4068 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/SomeEnum.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.elementapi + +enum class SomeEnum { + A, B +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt new file mode 100644 index 00000000000..d4385bd2718 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestBean.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.elementapi + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestBean { + var flag = false + var name: String? = null + var age = 0 + var stringArray: Array? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt new file mode 100644 index 00000000000..d4a831ec6c9 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestClass.kt @@ -0,0 +1,6 @@ +package io.micronaut.kotlin.processing.elementapi + +abstract class TestClass { + var name: String? = null + var author: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt new file mode 100644 index 00000000000..eec2a968e22 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/elementapi/TestEntity.kt @@ -0,0 +1,31 @@ +package io.micronaut.kotlin.processing.elementapi + +import javax.validation.constraints.* +import javax.persistence.* + +@Entity +class TestEntity( + @Column(name="test_name") var name: String, + @Size(max=100) var age: Int, + primitiveArray: Array) { + + @Id + @GeneratedValue + var id: Long? = null + + @Version + var version: Long? = null + + private var primitiveArray: Array? = null + + private var v: Long? = null + + @Version + fun getAnotherVersion(): Long? { + return v; + } + + fun setAnotherVersion(v: Long) { + this.v = v + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt new file mode 100644 index 00000000000..dbcf28a6a8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ChildConfigPropertiesX.kt @@ -0,0 +1,10 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.kotlin.processing.inject.configproperties.other.ParentConfigProperties + +@ConfigurationProperties("child") +class ChildConfigPropertiesX: ParentConfigProperties() { + + var age: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt new file mode 100644 index 00000000000..3744843f252 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MapProperties.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.inject.configproperties; + +import io.micronaut.context.annotation.ConfigurationProperties; + +@ConfigurationProperties("map") +class MapProperties { + + var property: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt new file mode 100644 index 00000000000..ef4e92798d3 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfig.kt @@ -0,0 +1,64 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.ReadableBytes + +import java.net.URL +import java.util.Optional + +@ConfigurationProperties("foo.bar") +class MyConfig { + var port: Int = 0 + var defaultValue: Int = 9999 + var stringList: List? = null + var intList: List? = null + var urlList: List? = null + var urlList2: List? = null + var emptyList: List? = null + var flags: Map? = null + var url: Optional? = null + var anotherUrl: Optional = Optional.empty() + var inner: Inner? = null + protected var defaultPort: Int = 9999 + protected var anotherPort: Int? = null + var innerVals: List? = null + + @ReadableBytes + var maxSize: Int = 0 + + var map: Map> = mapOf() + + class Value { + var property: Int = 0 + var property2: Value2? = null + + constructor() + + constructor(property: Int, property2: Value2) { + this.property = property + this.property2 = property2 + } + } + + class Value2 { + var property: Int = 0 + + constructor() + + constructor(property: Int) { + this.property = property + } + } + + @ConfigurationProperties("inner") + class Inner { + var enabled = false + + fun isEnabled() = enabled + } + +} + +class InnerVal { + var expireUnsignedSeconds: Int? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt new file mode 100644 index 00000000000..74b587ac1b7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyConfigInner.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyConfigInner { + var innerVals: List? = null + + class InnerVal { + var expireUnsignedSeconds: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt new file mode 100644 index 00000000000..8d0f5a869dc --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig.kt @@ -0,0 +1,12 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import io.micronaut.core.naming.conventions.StringConvention + +@ConfigurationProperties("jpa") +class MyHibernateConfig { + + @MapFormat(keyFormat = StringConvention.RAW, transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt new file mode 100644 index 00000000000..1feeea8cd90 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyHibernateConfig2.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat + +@ConfigurationProperties("jpa") +class MyHibernateConfig2 { + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt new file mode 100644 index 00000000000..9277355a928 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/MyPrimitiveConfig.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +class MyPrimitiveConfig { + var port = 0 + var defaultValue = 9999 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt new file mode 100644 index 00000000000..8c5a1b8da4f --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jProperties.kt @@ -0,0 +1,17 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +import org.neo4j.driver.v1.Config +import java.net.URI + +@ConfigurationProperties("neo4j.test") +class Neo4jProperties { + var uri: URI? = null + + @ConfigurationBuilder( + prefixes=["with"], + allowZeroArgs=true + ) + val options: Config.ConfigBuilder = Config.build() +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt new file mode 100644 index 00000000000..3b89ca07593 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Neo4jPropertiesFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.net.URI +import java.net.URISyntaxException + +@Factory +class Neo4jPropertiesFactory { + + @Singleton + @Replaces(Neo4jProperties::class) + @Requires(property = "spec.name", value = "ConfigurationPropertiesFactorySpec") + fun neo4jProperties(): Neo4jProperties { + val props = Neo4jProperties() + try { + props.uri = URI("https://google.com") + } catch (e: URISyntaxException) { + } + return props + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt new file mode 100644 index 00000000000..baa7b01a6f6 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/Pojo.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Email +import javax.validation.constraints.NotBlank + +@Introspected +class Pojo { + + @Email(message = "Email should be valid") + var email: String? = null + + @NotBlank + var name: String? = null +} + diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt new file mode 100644 index 00000000000..2413c6b76e5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/RecConf.kt @@ -0,0 +1,25 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.* + +@ConfigurationProperties("rec") +class RecConf { + + var namesListOf: List? = null + var mapChildren: Map? = null + var listChildren: List? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true; + if (other == null || this.javaClass != other.javaClass) return false; + val recConf = other as RecConf + return Objects.equals(namesListOf, recConf.namesListOf) && + Objects.equals(mapChildren, recConf.mapChildren) && + Objects.equals(listChildren, recConf.listChildren) + } + + override fun hashCode(): Int { + return Objects.hash(namesListOf, mapChildren, listChildren) + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt new file mode 100644 index 00000000000..b8217e6b454 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/ValidatedConfig.kt @@ -0,0 +1,19 @@ +package io.micronaut.kotlin.processing.inject.configproperties + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import java.net.URL + +@Requires(property = "spec.name", value = "ValidatedConfigurationSpec") +@ConfigurationProperties("foo.bar") +@Introspected +class ValidatedConfig { + + @NotNull + var url: URL? = null + @NotBlank + var name: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt new file mode 100644 index 00000000000..d3494fbd57d --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ChildConfig.kt @@ -0,0 +1,8 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("baz") +class ChildConfig: MyConfig() { + var stuff: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt new file mode 100644 index 00000000000..cb33dcc4f9e --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyConfig.kt @@ -0,0 +1,9 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.bar") +open class MyConfig { + var port: Int = 0 + var host: String? = null +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt new file mode 100644 index 00000000000..77415fd8a8a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/MyOtherConfig.kt @@ -0,0 +1,11 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("foo.baz") +class MyOtherConfig: ParentPojo() { + + var onlySetter: String? = null + var otherProperty: String? = null + +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt new file mode 100644 index 00000000000..999e0cb7b89 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachProps.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachProps(@Parameter private val index: Int): Ordered { + var wins: Int? = null + var manager: ManagerProps? = null + + override fun getOrder() = index + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter private val index: Int): Ordered { + + var age: Int? = null + + override fun getOrder() = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt new file mode 100644 index 00000000000..1e027105ec7 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentArrayEachPropsCtor.kt @@ -0,0 +1,22 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered + +@EachProperty(value = "teams", list = true) +class ParentArrayEachPropsCtor(@Parameter private val index: Int, val manager: ManagerProps?): Ordered { + + var wins: Int? = null + + override fun getOrder() = index + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter private val index: Int): Ordered { + + var age: Int? = null + + override fun getOrder() = index + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt new file mode 100644 index 00000000000..20d446be888 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachProps.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty + +@EachProperty("teams") +class ParentEachProps { + + var wins: Int? = null + var manager: ManagerProps? = null + + @ConfigurationProperties("manager") + class ManagerProps { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt new file mode 100644 index 00000000000..b4aa17763fb --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentEachPropsCtor.kt @@ -0,0 +1,16 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter + +@EachProperty("teams") +class ParentEachPropsCtor(@Parameter val name: String, val manager: ManagerProps?) { + + var wins: Int? = null + + @ConfigurationProperties("manager") + class ManagerProps(@Parameter val name: String) { + var age: Int? = null + } +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt new file mode 100644 index 00000000000..30191f8d4c8 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/inheritance/ParentPojo.kt @@ -0,0 +1,5 @@ +package io.micronaut.kotlin.processing.inject.configproperties.inheritance; + +open class ParentPojo { + var port: Int = 0 +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt new file mode 100644 index 00000000000..6b6542d6370 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyConfig.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.context.annotation.Requires +import javax.validation.constraints.NotBlank + +@ConfigurationProperties("my.config") +@Requires(property = "my.config") +interface MyConfig { + + @NotBlank + fun getName(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt new file mode 100644 index 00000000000..d4733248853 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/itfce/MyEachConfig.kt @@ -0,0 +1,13 @@ +package io.micronaut.kotlin.processing.inject.configproperties.itfce + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Requires +import javax.validation.constraints.NotBlank + +@EachProperty(value = "my.config", primary = "default") +@Requires(property = "my.config") +interface MyEachConfig { + + @NotBlank + fun getName(): String +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt new file mode 100644 index 00000000000..b7054dfcac5 --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configproperties/other/ParentConfigProperties.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.kotlin.processing.inject.configproperties.other; + +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.kotlin.processing.inject.configuration.Engine; + +@ConfigurationProperties("parent") +open class ParentConfigProperties { + + open var name: String? = null + protected set + + protected var nationality: String? = null + + @ConfigurationBuilder(value = "engine", prefixes = ["with"]) + val builder = Engine.builder() + +} diff --git a/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt new file mode 100644 index 00000000000..ce19009400a --- /dev/null +++ b/inject-kotlin/src/test/kotlin/io/micronaut/kotlin/processing/inject/configuration/Engine.kt @@ -0,0 +1,23 @@ +package io.micronaut.kotlin.processing.inject.configuration + +class Engine private constructor(val manufacturer: String) { + + companion object { + fun builder(): Builder { + return Builder() + } + } + + class Builder { + private var manufacturer = "Ford"; + + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun build(): Engine { + return Engine(manufacturer) + } + } +} diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper new file mode 100644 index 00000000000..59dc2f50c60 --- /dev/null +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationMapper @@ -0,0 +1,3 @@ +io.micronaut.kotlin.processing.elementapi.EntityAnnotationMapper +io.micronaut.kotlin.processing.aop.compile.NamedTestAnnMapper +io.micronaut.kotlin.processing.aop.introduction.ListenerAdviceMarkerMapper diff --git a/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer new file mode 100644 index 00000000000..d7080d203e5 --- /dev/null +++ b/inject-kotlin/src/test/resources/META-INF/services/io.micronaut.inject.annotation.AnnotationTransformer @@ -0,0 +1,2 @@ +io.micronaut.kotlin.processing.aop.compile.TestStereotypeAnnTransformer +io.micronaut.kotlin.processing.aop.compile.AroundConstructAnnTransformer diff --git a/inject-kotlin/src/test/resources/logback.xml b/inject-kotlin/src/test/resources/logback.xml new file mode 100644 index 00000000000..8294ddb0484 --- /dev/null +++ b/inject-kotlin/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 9553828ae2e..14bcad8f133 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -4097,7 +4097,7 @@ private static final class CollectionHolder { * @since 4.0.0 */ @Internal - final static class BeanDefinitionProducer { + static final class BeanDefinitionProducer { @Nullable private volatile BeanDefinitionReference reference; diff --git a/settings.gradle b/settings.gradle index b2c70db8e22..f68d10d1920 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,6 +46,7 @@ include "inject-groovy" include "inject-groovy-test" include "inject-java" include "inject-java-test" +include 'inject-kotlin' include 'inject-kotlin-test' include "inject-test-utils" include "jackson-core" @@ -68,6 +69,7 @@ include "test-suite-javax-inject" include "test-suite-jakarta-inject-bean-import" include "test-suite-http-server-tck-netty" include "test-suite-kotlin" +include "test-suite-kotlin-ksp" include "test-suite-graal" include "test-suite-groovy" include "test-utils" diff --git a/test-suite-kotlin-ksp/build.gradle b/test-suite-kotlin-ksp/build.gradle new file mode 100644 index 00000000000..69159ecedc5 --- /dev/null +++ b/test-suite-kotlin-ksp/build.gradle @@ -0,0 +1,96 @@ +plugins { + id "io.micronaut.build.internal.convention-test-library" + id "org.jetbrains.kotlin.jvm" + id("com.google.devtools.ksp") version "1.8.0-1.0.8" +} + +micronautBuild { + core { + usesMicronautTestJunit() + usesMicronautTestSpock() + usesMicronautTestKotest() + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { + url "https://s01.oss.sonatype.org/content/repositories/snapshots/" + mavenContent { + snapshotsOnly() + } + } +} + +dependencies { + api libs.kotlin.stdlib + api libs.kotlin.reflect + api libs.kotlinx.coroutines.core + api libs.kotlinx.coroutines.jdk8 + api libs.kotlinx.coroutines.rx2 + api project(':http-server-netty') + api project(':http-client') + api project(':runtime') + + testImplementation project(":context") + testImplementation libs.kotlin.test + testImplementation libs.kotlinx.coroutines.core + testImplementation libs.kotlinx.coroutines.rx2 + testImplementation libs.kotlinx.coroutines.slf4j + testImplementation libs.kotlinx.coroutines.reactor + + // Adding these for now since micronaut-test isnt resolving correctly ... probably need to upgrade gradle there too + testImplementation libs.junit.jupiter.api + + testImplementation project(":validation") + testImplementation project(":management") + testImplementation project(':inject-java') + testImplementation project(":inject") + testImplementation libs.jcache + testImplementation project(':validation') + testImplementation project(":http-client") + testImplementation(libs.micronaut.session) + testImplementation project(":jackson-databind") + testImplementation libs.managed.groovy.templates + + testImplementation project(":function-client") + testImplementation project(":function-web") + testImplementation libs.kotlin.kotest.junit5 + testImplementation libs.logbook.netty + kspTest project(':inject-kotlin') + kspTest project(':validation') + testImplementation libs.javax.inject + testImplementation(platform(libs.test.boms.micronaut.tracing)) + testImplementation(libs.micronaut.tracing.zipkin) { + exclude module: 'micronaut-bom' + exclude module: 'micronaut-http-client' + exclude module: 'micronaut-inject' + exclude module: 'micronaut-runtime' + } + + testRuntimeOnly libs.junit.jupiter.engine + testRuntimeOnly(platform(libs.test.boms.micronaut.aws)) + testRuntimeOnly libs.aws.java.sdk.lambda + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_15)) { + testImplementation libs.bcpkix + } + + testImplementation libs.managed.reactor +} + +configurations.testRuntimeClasspath { + resolutionStrategy.eachDependency { + if (it.requested.group == 'org.jetbrains.kotlin') { + it.useVersion(libs.versions.kotlin.asProvider().get()) + } + } +} + +tasks.named("compileTestKotlin") { + kotlinOptions.jvmTarget = "17" +} + +tasks.named("test") { + useJUnitPlatform() +} diff --git a/test-suite-kotlin-ksp/gradle.properties b/test-suite-kotlin-ksp/gradle.properties new file mode 100644 index 00000000000..1d70cd4a961 --- /dev/null +++ b/test-suite-kotlin-ksp/gradle.properties @@ -0,0 +1,2 @@ +skipDocumentation=true +ksp.incremental = false diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt new file mode 100644 index 00000000000..0f4b1a26c44 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/DemoController.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing + +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces + +@Controller("/demo") +@Produces(MediaType.TEXT_PLAIN) +class DemoController { + + @Get("/sync/any") + fun syncAny(): Any { + return "sync any" + } + + @Get("/sync/string") + fun syncStr(): String { + return "sync string" + } + + @Get("/async/any") + suspend fun asyncAny(): Any { + return "async any" + } + + @Get("/async/string") + suspend fun asyncStr(): String { + return "async string" + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt new file mode 100644 index 00000000000..fc557105f7a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendFunctionInterceptorSpec.kt @@ -0,0 +1,90 @@ +package io.micronaut.annotation.processing + +import io.kotest.matchers.shouldBe +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.Continuation +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.startCoroutine +import kotlin.test.Test +import kotlin.test.assertTrue + +@MicronautTest +class SuspendFunctionInterceptorSpec { + + @Inject + lateinit var demoClient: DemoClient + + @Test + fun interceptSuspendMethod() { + val interceptor = TestCoroutineInterceptor() + val latch = CountDownLatch(1) + var answer: String? = null + demoClient::getSyncString.startCoroutine(Continuation(interceptor) { result -> + answer = result.getOrNull() + latch.countDown() + }) + latch.await(1, TimeUnit.SECONDS) shouldBe true + assertTrue(interceptor.didIntercept()) + answer shouldBe "sync string" + } + + @Test + fun returnToCallerThreadWithSuspendClient() { + val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + runBlocking { + launch(singleThreadDispatcher) { + val threadId = Thread.currentThread().id + demoClient.getSyncString() shouldBe "sync string" + Thread.currentThread().id shouldBe threadId + } + } + } + + @Client("/demo") + @Consumes(MediaType.TEXT_PLAIN) + interface DemoClient { + @Get("/sync/string") + suspend fun getSyncString(): String + } + + class TestCoroutineInterceptor : ContinuationInterceptor { + private val didIntercept = AtomicBoolean(false) + + fun didIntercept() = didIntercept.get() + + override val key: CoroutineContext.Key<*> + get() = ContinuationInterceptor.Key + + override fun interceptContinuation(continuation: Continuation): Continuation { + return InterceptedContinuation(didIntercept, continuation) + } + + class InterceptedContinuation( + private val didIntercept: AtomicBoolean, + private val continuation: Continuation + ) : Continuation { + override val context: CoroutineContext + get() = continuation.context + + override fun resumeWith(result: Result) { + if (result as Any? !== Unit) { // startCoroutine directly calls resumeWith(Unit) after starting the coroutine + didIntercept.set(true) + } + continuation.resumeWith(result) + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt new file mode 100644 index 00000000000..b10327e3bc2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/annotation/processing/SuspendMethodSpec.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.annotation.processing + +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import javax.inject.Inject +import kotlin.test.assertEquals + +// issue https://github.com/micronaut-projects/micronaut-core/issues/5396 +@MicronautTest +class SuspendMethodSpec { + + @Inject + @field:Client("/demo") + lateinit var client: HttpClient + + @Test + fun testSyncMethodReturnTypeAny() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/sync/any"), + Any::class.java + )).blockFirst() + + assertEquals("sync any", res) + } + + @Test + fun testSyncMethodReturnTypeString() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/sync/string"), + Any::class.java + )).blockFirst() + + assertEquals("sync string", res) + } + + + @Test + fun testAsyncMethodReturnTypeAny() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/async/any"), + Any::class.java + )).blockFirst() + + assertEquals("async any", res) + } + + @Test + fun testAsyncMethodReturnTypeString() { + val res = Flux.from(client + .retrieve( + HttpRequest.GET("/async/string"), + Any::class.java + )).blockFirst() + + assertEquals("async string", res) + } + + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt new file mode 100644 index 00000000000..4f23eeea784 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/AbstractTestEntity.kt @@ -0,0 +1,23 @@ +package io.micronaut.core.beans + +abstract class AbstractTestEntity( + var id: Long, + var name: String, + var getSurname: String, + var isDeleted: Boolean, + val isImportant: Boolean, + var corrected: Boolean, + val upgraded: Boolean, +) { + val isMyBool: Boolean + get() = false + var isMyBool2: Boolean + get() = false + set(v) {} + var myBool3: Boolean + get() = false + set(v) {} + val myBool4: Boolean + get() = false + var myBool5: Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt new file mode 100644 index 00000000000..9407455d76e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/Item.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.core.beans + + +import io.micronaut.core.annotation.Introspected +import java.util.* + +@Introspected +abstract class Item> { + + var id: Long? = null + + var revisions: MutableList = ArrayList() +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt new file mode 100644 index 00000000000..ec5b3dc7e17 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/KotlinBeanIntrospectionSpec.kt @@ -0,0 +1,37 @@ +package io.micronaut.core.beans + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import io.micronaut.core.reflect.exception.InstantiationException + +class KotlinBeanIntrospectionSpec { + + @Test + fun testWithValueOnKotlinDataClassWithDefaultValues() { + val introspection = BeanIntrospection.getIntrospection(SomeEntity::class.java) + + val instance = introspection.instantiate(10L, "foo") + + assertEquals(10, instance.id) + assertEquals("foo", instance.something) + + val changed = introspection.getRequiredProperty("something", String::class.java) + .withValue(instance, "changed") + + assertEquals(10, changed.id) + assertEquals("changed", changed.something) + + } + + @Test + fun testIsProperties() { + val introspection = BeanIntrospection.getIntrospection(TestEntity::class.java) + + assertEquals(listOf("id", "name", "getSurname", "isDeleted", "isImportant", "corrected", "upgraded", "isMyBool", "isMyBool2", "myBool3", "myBool4", "myBool5"), introspection.propertyNames.asList()) + + val introspection2 = BeanIntrospection.getIntrospection(TestEntity2::class.java) + + assertEquals(listOf("id", "name", "getSurname", "isDeleted", "isImportant", "corrected", "upgraded", "isMyBool", "isMyBool2", "myBool3", "myBool4", "myBool5"), introspection2.propertyNames.asList()) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt new file mode 100644 index 00000000000..50d645513c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/RecusiveGenericsSpec.kt @@ -0,0 +1,15 @@ +package io.micronaut.core.beans + +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test + +class RecusiveGenericsSpec { + + // issue https://github.com/micronaut-projects/micronaut-core/issues/1607 + @Test + fun testRecursiveGenericsOnBeanIntrospection() { + val introspection = BeanIntrospection.getIntrospection(Item::class.java) + // just check compilation works + assertNotNull(introspection) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt new file mode 100644 index 00000000000..a6ac35a377d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/SomeEntity.kt @@ -0,0 +1,10 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +@Introspected +data class SomeEntity @Creator constructor( + val id: Long? = null, + val something: String? = null +) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt new file mode 100644 index 00000000000..8ee06c6e0eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity.kt @@ -0,0 +1,26 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestEntity( + var id: Long, + var name: String, + var getSurname: String, + var isDeleted: Boolean, + val isImportant: Boolean, + var corrected: Boolean, + val upgraded: Boolean, +) { + val isMyBool: Boolean + get() = false + var isMyBool2: Boolean + get() = false + set(v) {} + var myBool3: Boolean + get() = false + set(v) {} + val myBool4: Boolean + get() = false + var myBool5: Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt new file mode 100644 index 00000000000..0ca460691c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/core/beans/TestEntity2.kt @@ -0,0 +1,7 @@ +package io.micronaut.core.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +class TestEntity2(id: Long, name: String, getSurname: String, isDeleted: Boolean, isImportant: Boolean, corrected: Boolean, upgraded: Boolean) : AbstractTestEntity(id, name, getSurname, isDeleted, isImportant, corrected, upgraded) { +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt new file mode 100644 index 00000000000..de1c94e98ee --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/Pet.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +/** + * @author graemerocher + * @since 1.0 + */ + +// tag::class[] +class Pet { + var name: String? = null + var age: Int = 0 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt new file mode 100644 index 00000000000..123b200c1e6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetClient.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Client("/pets") // <1> +interface PetClient : PetOperations { // <2> + + @SingleResult + override fun save(name: String, age: Int): Publisher // <3> +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt new file mode 100644 index 00000000000..005d525b269 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import reactor.core.publisher.Mono +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Controller("/pets") +open class PetController : PetOperations { + + @SingleResult + override fun save(name: String, age: Int): Publisher { + val pet = Pet() + pet.name = name + pet.age = age + // save to database or something + return Mono.just(pet) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt new file mode 100644 index 00000000000..3e60c89d5da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetControllerSpec.kt @@ -0,0 +1,52 @@ +package io.micronaut.docs.annotation + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +import javax.validation.ConstraintViolationException + +import java.lang.Exception + +class PetControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test post pet" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + // tag::post[] + val pet = Mono.from(client.save("Dino", 10)).block() + + pet.name shouldBe "Dino" + pet.age.toLong() shouldBe 10 + // end::post[] + } + + "test post pet validation" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + // tag::error[] + try { + Mono.from(client.save("Fred", -1)).block() + } catch (e: Exception) { + e.javaClass shouldBe ConstraintViolationException::class.java + e.message shouldBe "save.age: must be greater than or equal to 1" + } + // end::error[] + } + } + + // tag::errorRule[] + // end::errorRule[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt new file mode 100644 index 00000000000..19816ea8a64 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/PetOperations.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation + +// tag::imports[] +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Validated +interface PetOperations { + // tag::save[] + @Post + @SingleResult + fun save(@NotBlank name: String, @Min(1L) age: Int): Publisher + // end::save[] +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt new file mode 100644 index 00000000000..67b5adf3b10 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/HeaderSpec.kt @@ -0,0 +1,27 @@ +package io.micronaut.docs.annotation.headers + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +class HeaderSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("pet.client.id" to "11") ) + ) + + init { + "test sender headers" { + val client = embeddedServer.applicationContext.getBean(PetClient::class.java) + + val pet = Mono.from(client["Fred"]).block() + + pet shouldNotBe null + pet.age.toLong() shouldBe 11 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt new file mode 100644 index 00000000000..e44b099c0b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetClient.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.headers + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.client.annotation.Client +import org.reactivestreams.Publisher +import io.micronaut.core.async.annotation.SingleResult +import reactor.core.publisher.Mono + +// tag::class[] +@Client("/pets") +@Header(name = "X-Pet-Client", value = "\${pet.client.id}") +interface PetClient : PetOperations { + + @SingleResult + override fun save(name: String, age: Int): Publisher + + @Get("/{name}") + @SingleResult + operator fun get(name: String): Publisher +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt new file mode 100644 index 00000000000..f72abd76e25 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/headers/PetController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.headers + +import io.micronaut.docs.annotation.Pet +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header + +@Controller("/pets") +class PetController { + + @Get("/{name}") + operator fun get(name: String, @Header("X-Pet-Client") clientId: String): HttpResponse { + val pet = Pet() + pet.name = name + pet.age = Integer.valueOf(clientId) + return HttpResponse.ok(pet) + .header("X-Pet-Client", clientId) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt new file mode 100644 index 00000000000..91c4edb809a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/RequestAttributeSpec.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.annotation.requestattributes + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Mono + +class RequestAttributeSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + "test sender attributes" { + val client = embeddedServer.applicationContext.getBean(StoryClient::class.java) + val filter = embeddedServer.applicationContext.getBean(StoryClientFilter::class.java) + + val story = Mono.from(client.getById("jan2019")).block() + val attributes = filter.latestRequestAttributes + + story shouldNotBe null + attributes shouldNotBe null + + attributes.get("story-id") shouldBe "jan2019" + attributes.get("client-name") shouldBe "storyClient" + attributes.get("version") shouldBe "1" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt new file mode 100644 index 00000000000..c4835562120 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/Story.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +// tag::class[] +class Story { + var id: String? = null + var title: String? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt new file mode 100644 index 00000000000..3e6ba9c8c76 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClient.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestAttribute +import io.micronaut.http.annotation.RequestAttributes +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher + +// tag::class[] +@Client("/story") +@RequestAttributes(RequestAttribute(name = "client-name", value = "storyClient"), RequestAttribute(name = "version", value = "1")) +interface StoryClient { + + @Get("/{storyId}") + @SingleResult + fun getById(@RequestAttribute storyId: String): Publisher +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt new file mode 100644 index 00000000000..6c4809d2d79 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryClientFilter.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher + +import java.util.HashMap + +@Filter("/story/**") +class StoryClientFilter : HttpClientFilter { + + private var attributes: Map? = null + + /** + * strictly for unit testing + */ + internal val latestRequestAttributes: Map + get() = HashMap(attributes!!) + + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + attributes = request.attributes.asMap() + return chain.proceed(request) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt new file mode 100644 index 00000000000..87306e9becf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/requestattributes/StoryController.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.requestattributes + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/story") +class StoryController { + + @Get("/{id}") + operator fun get(id: String): HttpResponse { + val story = Story() + story.id = id + return HttpResponse.ok(story) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt new file mode 100644 index 00000000000..442e937c601 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetClient.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.retry + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import reactor.core.publisher.Mono + +// tag::class[] +@Client("/pets") +@Retryable +interface PetClient : PetOperations { + + override fun save(name: String, age: Int): Mono +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt new file mode 100644 index 00000000000..394b38e7b66 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/annotation/retry/PetFallback.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.annotation.retry + +import io.micronaut.docs.annotation.Pet +import io.micronaut.docs.annotation.PetOperations +import io.micronaut.retry.annotation.Fallback +import reactor.core.publisher.Mono + +// tag::class[] +@Fallback +open class PetFallback : PetOperations { + override fun save(name: String, age: Int): Mono { + val pet = Pet() + pet.age = age + pet.name = name + return Mono.just(pet) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt new file mode 100644 index 00000000000..ccbc92514b0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/MyBean.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice + +open class MyBean diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java new file mode 100644 index 00000000000..26623527f47 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/Timed.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice; + +public @interface Timed { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt new file mode 100644 index 00000000000..2f16febc202 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/method/MyFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice.method + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.docs.aop.advice.MyBean +import io.micronaut.docs.aop.advice.Timed + +// tag::class[] +@Factory +open class MyFactory { + + @Prototype + @Timed + open fun myBean(): MyBean { + return MyBean() + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt new file mode 100644 index 00000000000..c58cf6cc4b0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/advice/type/MyFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.advice.type + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.docs.aop.advice.MyBean +import io.micronaut.docs.aop.advice.Timed + +// tag::class[] +@Timed +@Factory +open class MyFactory { + + @Prototype + open fun myBean(): MyBean { + return MyBean() + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt new file mode 100644 index 00000000000..04e38d277e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/AroundSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.aop.around + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext + +class AroundSpec: AnnotationSpec() { + + // tag::test[] + @Test + fun testNotNull() { + val applicationContext = ApplicationContext.run() + val exampleBean = applicationContext.getBean(NotNullExample::class.java) + + val exception = shouldThrow { + exampleBean.doWork(null) + } + exception.message shouldBe "Null parameter [taskName] not allowed" + applicationContext.close() + } + // end::test[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt new file mode 100644 index 00000000000..6dbe67012af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNull.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::imports[] +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +// end::imports[] + +// tag::annotation[] +@MustBeDocumented +@Retention(RUNTIME) // <1> +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) // <2> +@Around // <3> +annotation class NotNull +// end::annotation[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt new file mode 100644 index 00000000000..c490943deda --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullExample.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::example[] +import jakarta.inject.Singleton + +@Singleton +open class NotNullExample { + + @NotNull + open fun doWork(taskName: String?) { + println("Doing job: $taskName") + } +} +// end::example[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt new file mode 100644 index 00000000000..4bbb58a9e05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/around/NotNullInterceptor.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.around + +// tag::imports[] +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import java.util.Objects +import jakarta.inject.Singleton +// end::imports[] + +// tag::interceptor[] +@Singleton +@InterceptorBean(NotNull::class) // <1> +class NotNullInterceptor : MethodInterceptor { // <2> + override fun intercept(context: MethodInvocationContext): Any? { + val nullParam = context.parameters + .entries + .stream() + .filter { entry -> + val argumentValue = entry.value + Objects.isNull(argumentValue.value) + } + .findFirst() // <3> + return if (nullParam.isPresent) { + throw IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") // <4> + } else { + context.proceed() // <5> + } + } +} +// end::interceptor[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt new file mode 100644 index 00000000000..4a6ecc78524 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/IntroductionSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.aop.introduction + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext + +class IntroductionSpec: AnnotationSpec() { + + @Test + fun testStubIntroduction() { + val applicationContext = ApplicationContext.run() + + // tag::test[] + val stubExample = applicationContext.getBean(StubExample::class.java) + + stubExample.number.shouldBe(10) + stubExample.date.shouldBe(null) + // end::test[] + + applicationContext.stop() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt new file mode 100644 index 00000000000..5f16d064ec6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/Stub.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +// tag::imports[] +import io.micronaut.aop.Introduction +import io.micronaut.context.annotation.Bean +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER +// end::imports[] + +// tag::class[] +@Introduction // <1> +@Bean // <2> +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, ANNOTATION_CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +annotation class Stub(val value: String = "") +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt new file mode 100644 index 00000000000..34aaee46d26 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubExample.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +import java.time.LocalDateTime + +// tag::class[] +@Stub +interface StubExample { + + @get:Stub("10") + val number: Int + + val date: LocalDateTime? +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt new file mode 100644 index 00000000000..134762d1dfe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/introduction/StubIntroduction.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.introduction + +// tag::imports[] +import io.micronaut.aop.* +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +@InterceptorBean(Stub::class) // <1> +class StubIntroduction : MethodInterceptor { // <2> + + override fun intercept(context: MethodInvocationContext): Any? { + return context.getValue( // <3> + Stub::class.java, + context.returnType.type + ).orElse(null) // <4> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt new file mode 100644 index 00000000000..13054c28666 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/LifeCycleAdviseSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.aop.lifecycle + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class LifeCycleAdviseSpec { + @Test + fun testLifeCycleAdvise() { + ApplicationContext.run().use { applicationContext -> + val productService = + applicationContext.getBean(ProductService::class.java) + val product = + applicationContext.createBean(Product::class.java, "Apple") // + assertTrue(product.active) + assertTrue(productService.findProduct("APPLE").isPresent) + + applicationContext.destroyBean(product) + assertFalse(product.active) + assertFalse(productService.findProduct("APPLE").isPresent) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt new file mode 100644 index 00000000000..4ba20db623b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/Product.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.context.annotation.Parameter +import jakarta.annotation.PreDestroy +// end::imports[] + +// tag::class[] +@ProductBean // <1> +class Product(@param:Parameter val productName: String ) { // <2> + + var active: Boolean = false + @PreDestroy + fun disable() { // <3> + active = false + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt new file mode 100644 index 00000000000..2e7cb40879d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductBean.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.aop.AroundConstruct +import io.micronaut.aop.InterceptorBinding +import io.micronaut.aop.InterceptorBindingDefinitions +import io.micronaut.aop.InterceptorKind +import io.micronaut.context.annotation.Prototype +// end::imports[] + +// tag::class[] +@Retention(AnnotationRetention.RUNTIME) +@AroundConstruct // <1> +@InterceptorBindingDefinitions( + InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT), // <2> + InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) // <3> +) +@Prototype // <4> +annotation class ProductBean +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt new file mode 100644 index 00000000000..d1b8007a71e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductInterceptors.kt @@ -0,0 +1,51 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import io.micronaut.aop.* +import io.micronaut.context.annotation.Factory +// end::imports[] + +// tag::class[] +@Factory +class ProductInterceptors(private val productService: ProductService) { +// end::class[] + + // tag::constructor-interceptor[] + @InterceptorBean(ProductBean::class) + fun aroundConstruct(): ConstructorInterceptor { // <1> + return ConstructorInterceptor { context: ConstructorInvocationContext -> + val parameterValues = context.parameterValues // <2> + val parameterValue = parameterValues[0] + require(!(parameterValue == null || parameterValues[0].toString().isEmpty())) { "Invalid product name" } + val productName = parameterValues[0].toString().uppercase() + parameterValues[0] = productName + val product = context.proceed() // <3> + productService.addProduct(product) + product + } + } + // end::constructor-interceptor[] + + // tag::method-interceptor[] + @InterceptorBean(ProductBean::class) + fun aroundInvoke(): MethodInterceptor { // <1> + return MethodInterceptor { context: MethodInvocationContext -> + val product = context.target + return@MethodInterceptor when (context.kind) { + InterceptorKind.POST_CONSTRUCT -> { // <2> + product.active = true + context.proceed() + } + InterceptorKind.PRE_DESTROY -> { // <3> + productService.removeProduct(product) + context.proceed() + } + else -> context.proceed() + } + } + } + // end::method-interceptor[] + +// tag::class[] +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt new file mode 100644 index 00000000000..5c3ea51248d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/lifecycle/ProductService.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.aop.lifecycle + +// tag::imports[] +import java.util.* +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class ProductService { + private val products: MutableMap = HashMap() + fun addProduct(product: Product) { + products[product.productName] = product + } + + fun removeProduct(product: Product) { + product.active = false + products.remove(product.productName) + } + + fun findProduct(name: String): Optional { + return Optional.ofNullable(products[name]) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt new file mode 100644 index 00000000000..a9cb47a8067 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/Book.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.retry + +class Book(val title: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt new file mode 100644 index 00000000000..76901cc8ca5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/retry/BookService.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.retry + +import io.micronaut.retry.annotation.CircuitBreaker +import io.micronaut.retry.annotation.Retryable +import reactor.core.publisher.Flux + +open class BookService { + + // tag::simple[] + @Retryable + open fun listBooks(): List { + // ... + // end::simple[] + return listOf(Book("The Stand")) + } + + // tag::circuit[] + @CircuitBreaker(reset = "30s") + open fun findBooks(): List { + // ... + // end::circuit[] + return listOf(Book("The Stand")) + } + + // tag::attempts[] + @Retryable(attempts = "5", + delay = "2s") + open fun findBook(title: String): Book { + // ... + // end::attempts[] + return Book(title) + } + + // tag::config[] + @Retryable(attempts = "\${book.retry.attempts:3}", + delay = "\${book.retry.delay:1s}") + open fun getBook(title: String): Book { + // ... + // end::config[] + return Book(title) + } + + // tag::reactive[] + @Retryable + open fun streamBooks(): Flux { + // ... + // end::reactive[] + return Flux.just( + Book("The Stand") + ) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt new file mode 100644 index 00000000000..92dcb1f677b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/ScheduledExample.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.scheduled + +import io.micronaut.scheduling.annotation.Scheduled + +import jakarta.inject.Singleton + +@Singleton +class ScheduledExample { + + // tag::fixedRate[] + @Scheduled(fixedRate = "5m") + internal fun everyFiveMinutes() { + println("Executing everyFiveMinutes()") + } + // end::fixedRate[] + + // tag::fixedDelay[] + @Scheduled(fixedDelay = "5m") + internal fun fiveMinutesAfterLastExecution() { + println("Executing fiveMinutesAfterLastExecution()") + } + // end::fixedDelay[] + + // tag::cron[] + @Scheduled(cron = "0 15 10 ? * MON") + internal fun everyMondayAtTenFifteenAm() { + println("Executing everyMondayAtTenFifteenAm()") + } + // end::cron[] + + // tag::initialDelay[] + @Scheduled(initialDelay = "1m") + internal fun onceOneMinuteAfterStartup() { + println("Executing onceOneMinuteAfterStartup()") + } + // end::initialDelay[] + + // tag::configured[] + @Scheduled(fixedRate = "\${my.task.rate:5m}", + initialDelay = "\${my.task.delay:1m}") + internal fun configuredTask() { + println("Executing configuredTask()") + } + // end::configured[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt new file mode 100644 index 00000000000..299a1498892 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/aop/scheduled/TaskSchedulerInjectExample.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.aop.scheduled + +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.TaskScheduler + +import jakarta.inject.Inject +import jakarta.inject.Named + +class TaskSchedulerInjectExample { + // tag::inject[] + @Inject + @Named(TaskExecutors.SCHEDULED) + lateinit var taskScheduler: TaskScheduler + // end::inject[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt new file mode 100644 index 00000000000..ef012b15c34 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Book.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +class Book { + + var title: String? = null + + @JsonCreator + constructor(@JsonProperty("title") title: String) { + this.title = title + } + + internal constructor() {} +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt new file mode 100644 index 00000000000..48209af31a5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Status + +@Controller("/amazon") +class BookController { + + @Post(value = "/book/{title}", consumes = [MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED]) + @Status(HttpStatus.CREATED) + internal fun save(title: String): Book { + return Book(title) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt new file mode 100644 index 00000000000..08e9633ab9c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/BookControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.basics + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest.POST +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class BookControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test post with uri template" { + // tag::posturitemplate[] + val call = client.exchange( + POST("/amazon/book/{title}", Book("The Stand")), + Book::class.java + ) + // end::posturitemplate[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Book::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().title shouldBe "The Stand" + } + + "test post with form data" { + // tag::postform[] + val call = client.exchange( + POST("/amazon/book/{title}", Book("The Stand")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED), + Book::class.java + ) + // end::postform[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Book::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().title shouldBe "The Stand" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt new file mode 100644 index 00000000000..20b8d027bbb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloController.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpStatus.CREATED +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Status +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import io.micronaut.core.async.annotation.SingleResult +// end::imports[] + +@Requires(property = "spec.name", value = "HelloControllerSpec") +@Controller("/") +class HelloController(@param:Client("/endpoint") private val httpClient: HttpClient) { + + // tag::nonblocking[] + @Get("/hello/{name}") + @SingleResult + internal fun hello(name: String): Publisher { // <1> + return Flux.from(httpClient.retrieve(GET("/hello/$name"))) + .next() // <2> + } + // end::nonblocking[] + + @Get("/endpoint/hello/{name}") + internal fun helloEndpoint(name: String): String { + return "Hello $name" + } + + // tag::json[] + @Get("/greet/{name}") + internal fun greet(name: String): Message { + return Message("Hello $name") + } + // end::json[] + + @Post("/greet") + @Status(CREATED) + internal fun echo(@Body message: Message): Message { + return message + } + + @Post(value = "/hello", consumes = [TEXT_PLAIN], produces = [TEXT_PLAIN]) + @Status(CREATED) + internal fun echoHello(@Body message: String): String { + return message + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt new file mode 100644 index 00000000000..acc4857f989 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/HelloControllerSpec.kt @@ -0,0 +1,131 @@ +package io.micronaut.docs.basics + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.POST +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import java.util.Collections + +class HelloControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to HelloControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test simple retrieve" { + // tag::simple[] + val uri = UriBuilder.of("/hello/{name}") + .expand(Collections.singletonMap("name", "John")) + .toString() + uri shouldBe "/hello/John" + + val result = client.toBlocking().retrieve(uri) + + result shouldBe "Hello John" + // end::simple[] + } + + "test retrieve with headers" { + // tag::headers[] + val response = client.retrieve( + GET("/hello/John") + .header("X-My-Header", "SomeValue") + ) + // end::headers[] + + Flux.from(response).blockFirst() shouldBe "Hello John" + } + + "test retrieve with JSON" { + // tag::jsonmap[] + var response: Flux> = Flux.from(client.retrieve( + GET("/greet/John"), Map::class.java + )) + // end::jsonmap[] + + response.blockFirst()["text"] shouldBe "Hello John" + + // tag::jsonmaptypes[] + response = Flux.from(client.retrieve( + GET("/greet/John"), + Argument.of(Map::class.java, String::class.java, String::class.java) // <1> + )) + // end::jsonmaptypes[] + + response.blockFirst()["text"] shouldBe "Hello John" + } + + "test retrieve with POJO" { + // tag::jsonpojo[] + val response = Flux.from(client.retrieve( + GET("/greet/John"), Message::class.java + )) + + response.blockFirst().text shouldBe "Hello John" + // end::jsonpojo[] + } + + "test retrieve with POJO response" { + // tag::pojoresponse[] + val call = client.exchange( + GET("/greet/John"), Message::class.java // <1> + ) + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Message::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.OK // <3> + // check the body + message.isPresent shouldBe true + message.get().text shouldBe "Hello John" + // end::pojoresponse[] + } + + "test post request with string" { + // tag::poststring[] + val call = client.exchange( + POST("/hello", "Hello John") // <1> + .contentType(MediaType.TEXT_PLAIN_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), String::class.java // <3> + ) + // end::poststring[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(String::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get() shouldBe "Hello John" + } + + "test post request with POJO" { + // tag::postpojo[] + val call = client.exchange( + POST("/greet", Message("Hello John")), Message::class.java // <2> + ) + // end::postpojo[] + + val response = Flux.from(call).blockFirst() + val message = response.getBody(Message::class.java) // <2> + // check the status + response.status shouldBe HttpStatus.CREATED // <3> + // check the body + message.isPresent shouldBe true + message.get().text shouldBe "Hello John" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt new file mode 100644 index 00000000000..442df0bff0f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/basics/Message.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.basics + +// tag::imports[] +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +// end::imports[] + +// tag::class[] +class Message @JsonCreator +constructor(@param:JsonProperty("text") val text: String) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt new file mode 100644 index 00000000000..09eaceaf383 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/ThirdPartyClientFilterSpec.kt @@ -0,0 +1,108 @@ +package io.micronaut.docs.client + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Filter +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +import java.util.Base64 +import jakarta.inject.Singleton +import reactor.core.publisher.Flux + +class ThirdPartyClientFilterSpec: StringSpec() { + private var result: String? = null + private val token = "XXXX" + private val username = "john" + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf( + "bintray.username" to username, + "bintray.token" to token, + "bintray.organization" to "grails", + "spec.name" to ThirdPartyClientFilterSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "a client filter is applied to the request and adds the authorization header" { + val bintrayService = embeddedServer.applicationContext.getBean(BintrayService::class.java) + + result = bintrayService.fetchRepositories().blockFirst().body() + + val encoded = Base64.getEncoder().encodeToString("$username:$token".toByteArray()) + val expected = "Basic $encoded" + + result shouldBe expected + } + } + + @Controller("/repos") + class HeaderController { + + @Get(value = "/grails") + fun echoAuthorization(@Header authorization: String): String { + return authorization + } + } +} + +//tag::bintrayService[] +@Singleton +internal class BintrayService( + @param:Client(BintrayApi.URL) val client: HttpClient, // <1> + @param:Value("\${bintray.organization}") val org: String) { + + fun fetchRepositories(): Flux> { + return Flux.from(client.exchange(HttpRequest.GET("/repos/$org"), String::class.java)) // <2> + } + + fun fetchPackages(repo: String): Flux> { + return Flux.from(client.exchange(HttpRequest.GET("/repos/$org/$repo/packages"), String::class.java)) // <2> + } +} +//end::bintrayService[] + +@Requires(property = "spec.name", value = "ThirdPartyClientFilterSpec") +//tag::bintrayFilter[] +@Filter("/repos/**") // <1> +internal class BintrayFilter( + @param:Value("\${bintray.username}") val username: String, // <2> + @param:Value("\${bintray.token}") val token: String)// <2> + : HttpClientFilter { + + override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher> { + return chain.proceed( + request.basicAuth(username, token) // <3> + ) + } +} +//end::bintrayFilter[] + +/* +//tag::bintrayApiConstants[] +class BintrayApi { + public static final String URL = 'https://api.bintray.com' +} +//end::bintrayApiConstants[] +*/ + +internal object BintrayApi { + const val URL = "/" +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt new file mode 100644 index 00000000000..eda14309668 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuth.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.annotation.FilterMatcher +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@FilterMatcher // <1> +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, VALUE_PARAMETER) +annotation class BasicAuth +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt new file mode 100644 index 00000000000..577a50d898d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClient.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +@BasicAuth // <1> +@Client("/message") +interface BasicAuthClient { + + @Get + fun getMessage(): String +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt new file mode 100644 index 00000000000..3f3acf1b51d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthClientFilter.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher + +import jakarta.inject.Singleton + +@BasicAuth // <1> +@Singleton // <2> +class BasicAuthClientFilter : HttpClientFilter { + + override fun doFilter(request: MutableHttpRequest<*>, + chain: ClientFilterChain): Publisher> { + return chain.proceed(request.basicAuth("user", "pass")) + } +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt new file mode 100644 index 00000000000..9ef21f2bec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/BasicAuthFilterSpec.kt @@ -0,0 +1,36 @@ +package io.micronaut.docs.client.filter + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.runtime.server.EmbeddedServer + +class BasicAuthFilterSpec: StringSpec() { + + val context = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to BasicAuthFilterSpec::class.simpleName)).applicationContext + ) + + init { + "test the filter is applied"() { + val client = context.getBean(BasicAuthClient::class.java) + + client.getMessage() shouldBe "user:pass" + } + } + + @Requires(property = "spec.name", value = "BasicAuthFilterSpec") + @Controller("/message") + class BasicAuthController { + + @Get + internal fun message(basicAuth: io.micronaut.http.BasicAuth): String { + return basicAuth.username + ":" + basicAuth.password + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt new file mode 100644 index 00000000000..476f283b6a4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/filter/GoogleAuthFilter.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.client.filter + +//tag::class[] +import io.micronaut.context.BeanProvider +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.annotation.Filter +import io.micronaut.http.client.HttpClient +import io.micronaut.http.filter.ClientFilterChain +import io.micronaut.http.filter.HttpClientFilter +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.net.URLEncoder + +@Requires(env = [Environment.GOOGLE_COMPUTE]) +@Filter(patterns = ["/google-auth/api/**"]) +class GoogleAuthFilter ( + private val authClientProvider: BeanProvider) : HttpClientFilter { // <1> + + override fun doFilter(request: MutableHttpRequest<*>, + chain: ClientFilterChain): Publisher?> { + return Mono.fromCallable { encodeURI(request) } + .flux() + .map { authURI: String -> + authClientProvider.get().retrieve(HttpRequest.GET(authURI) + .header("Metadata-Flavor", "Google") // <2> + ) + }.flatMap { t -> chain.proceed(request.bearerAuth(t.toString())) } + } + + private fun encodeURI(request: MutableHttpRequest<*>): String { + val receivingURI = "${request.uri.scheme}://${request.uri.host}" + return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" + + URLEncoder.encode(receivingURI, "UTF-8") + } + +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt new file mode 100644 index 00000000000..71a1773d7cc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/upload/MultipartFileUploadSpec.kt @@ -0,0 +1,122 @@ +package io.micronaut.docs.client.upload + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Post +import io.micronaut.runtime.server.EmbeddedServer +import java.io.File +import java.io.FileWriter +// end::imports[] + +// tag::multipartBodyImports[] +import io.micronaut.http.client.multipart.MultipartBody +// end::multipartBodyImports[] + +// tag::controllerImports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.client.HttpClient +import reactor.core.publisher.Flux + +// end::controllerImports[] + +// tag::class[] +class MultipartFileUploadSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test multipart file request byte[]" { + // tag::file[] + val toWrite = "test file" + val file = File.createTempFile("data", ".txt") + val writer = FileWriter(file) + writer.write(toWrite) + writer.close() + // end::file[] + + // tag::multipartBody[] + val requestBody = MultipartBody.builder() // <1> + .addPart( // <2> + "data", + file.name, + MediaType.TEXT_PLAIN_TYPE, + file + ).build() // <3> + + // end::multipartBody[] + + val flowable = Flux.from(client!!.exchange( + + // tag::request[] + HttpRequest.POST("/multipart/upload", requestBody) // <1> + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) // <2> + // end::request[] + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 9 bytes" + } + + "test multipart file request byte[] with ContentType" { + // tag::multipartBodyBytes[] + val requestBody = MultipartBody.builder() + .addPart("data", "sample.txt", MediaType.TEXT_PLAIN_TYPE, "test content".toByteArray()) + .build() + // end::multipartBodyBytes[] + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/multipart/upload", requestBody) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 12 bytes" + } + + "test multipart file request byte[] without ContentType" { + val toWrite = "test file" + val file = File.createTempFile("data", ".txt") + val writer = FileWriter(file) + writer.write(toWrite) + writer.close() + file.createNewFile() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/multipart/upload", MultipartBody.builder().addPart("data", file.name, file)) + .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + val body = response.body.get() + + body shouldBe "Uploaded 9 bytes" + } + } + + @Controller("/multipart") + internal class MultipartController { + + @Post(value = "/upload", consumes = [MediaType.MULTIPART_FORM_DATA], produces = [MediaType.TEXT_PLAIN]) + fun upload(data: ByteArray): HttpResponse { + return HttpResponse.ok("Uploaded ${data.size} bytes") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt new file mode 100644 index 00000000000..35248f4b577 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/client/versioning/HelloClient.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.client.versioning + +// tag::imports[] +import io.micronaut.core.version.annotation.Version +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import reactor.core.publisher.Mono +// end::imports[] + +// tag::clazz[] +@Client("/hello") +@Version("1") // <1> +interface HelloClient { + + @Get("/greeting/{name}") + fun sayHello(name : String) : String + + @Version("2") + @Get("/greeting/{name}") + fun sayHelloTwo(name : String) : Mono // <2> +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt new file mode 100644 index 00000000000..defb6e6e5df --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/CrankShaft.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +* Copyright 2017-2019 original authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.micronaut.docs.config.builder + +internal class CrankShaft(val rodLength: Double?) { + + class Builder { + private var rodLength: Double? = null + fun withRodLength(rodLength: Double): Builder { + this.rodLength = rodLength + return this + } + + fun build(): CrankShaft { + return CrankShaft(rodLength) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt new file mode 100644 index 00000000000..d46d49cdcb8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::class[] +internal interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt new file mode 100644 index 00000000000..06ae14a1735 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineConfig.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationBuilder +import io.micronaut.context.annotation.ConfigurationProperties +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +internal class EngineConfig { + + @ConfigurationBuilder(prefixes = ["with"]) // <2> + val builder = EngineImpl.builder() + + @ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "crank-shaft") // <3> + val crankShaft = CrankShaft.builder() + + @set:ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "spark-plug") // <4> + var sparkPlug = SparkPlug.builder() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt new file mode 100644 index 00000000000..f649927774c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineFactory.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::imports[] +import io.micronaut.context.annotation.Factory +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +internal class EngineFactory { + + @Singleton + fun buildEngine(engineConfig: EngineConfig): EngineImpl { + return engineConfig.builder.build(engineConfig.crankShaft, engineConfig.sparkPlug) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt new file mode 100644 index 00000000000..b693f32c3c1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/EngineImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +// tag::class[] +internal class EngineImpl(manufacturer: String, cylinders: Int, crankShaft: CrankShaft, sparkPlug: SparkPlug) : Engine { + override var cylinders: Int = 0 + private val manufacturer: String + private val crankShaft: CrankShaft + private val sparkPlug: SparkPlug + + init { + this.crankShaft = crankShaft + this.cylinders = cylinders + this.manufacturer = manufacturer + this.sparkPlug = sparkPlug + } + + override fun start(): String { + return "$manufacturer Engine Starting V$cylinders [rodLength=${crankShaft.rodLength ?: 6.0}, sparkPlug=$sparkPlug]" + } + + class Builder { + private var manufacturer = "Ford" + private var cylinders: Int = 0 + fun withManufacturer(manufacturer: String): Builder { + this.manufacturer = manufacturer + return this + } + + fun withCylinders(cylinders: Int): Builder { + this.cylinders = cylinders + return this + } + + fun build(crankShaft: CrankShaft.Builder, sparkPlug: SparkPlug.Builder): EngineImpl { + return EngineImpl(manufacturer, cylinders, crankShaft.build(), sparkPlug.build()) + } + } + + companion object { + fun builder(): Builder { + return Builder() + } + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt new file mode 100644 index 00000000000..4a6014b8d29 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/SparkPlug.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +internal data class SparkPlug( + val name: String?, + val type: String?, + val companyName: String? +) { + override fun toString(): String { + return "${type ?: ""}(${companyName ?: ""} ${name ?: ""})" + } + + companion object { + fun builder(): Builder { + return Builder() + } + } + + data class Builder( + var name: String? = "4504 PK20TT", + var type: String? = "Platinum TT", + var companyName: String? = "Denso" + ) { + fun withName(name: String?): Builder { + this.name = name + return this + } + + fun withType(type: String?): Builder { + this.type = type + return this + } + + fun withCompany(companyName: String?): Builder { + this.companyName = companyName + return this + } + + fun build(): SparkPlug { + return SparkPlug(name, type, companyName) + } + } + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt new file mode 100644 index 00000000000..1a16454cd8d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/Vehicle.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.builder + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt new file mode 100644 index 00000000000..29a37879372 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/builder/VehicleSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.config.builder + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +internal class VehicleSpec : StringSpec({ + + "test start vehicle" { + // tag::start[] + val applicationContext = ApplicationContext.run( + mapOf( + "my.engine.cylinders" to "4", + "my.engine.manufacturer" to "Subaru", + "my.engine.crank-shaft.rod-length" to 4, + "my.engine.spark-plug.name" to "6619 LFR6AIX", + "my.engine.spark-plug.type" to "Iridium", + "my.engine.spark-plug.company" to "NGK" + ), + "test" + ) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Subaru Engine Starting V4 [rodLength=4.0, sparkPlug=Iridium(NGK 6619 LFR6AIX)]") + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt new file mode 100644 index 00000000000..c4b3c050665 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MapToLocalDateConverter.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.converters + +// tag::imports[] +import io.micronaut.context.annotation.Prototype +import io.micronaut.core.convert.ConversionContext +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.convert.TypeConverter +import java.time.DateTimeException +import java.time.LocalDate +import java.util.Optional +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Prototype +class MapToLocalDateConverter : TypeConverter, LocalDate> { // <1> + override fun convert(propertyMap: Map<*, *>, targetType: Class, context: ConversionContext): Optional { + val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java) + val month = ConversionService.SHARED.convert(propertyMap["month"], Int::class.java) + val year = ConversionService.SHARED.convert(propertyMap["year"], Int::class.java) + if (day.isPresent && month.isPresent && year.isPresent) { + try { + return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // <2> + } catch (e: DateTimeException) { + context.reject(propertyMap, e) // <3> + return Optional.empty() + } + } + + return Optional.empty() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt new file mode 100644 index 00000000000..3b98e900250 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationProperties.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.converters + +import io.micronaut.context.annotation.ConfigurationProperties +import java.time.LocalDate + +// tag::class[] +@ConfigurationProperties(MyConfigurationProperties.PREFIX) +class MyConfigurationProperties { + + var updatedAt: LocalDate? = null + protected set + + companion object { + const val PREFIX = "myapp" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt new file mode 100644 index 00000000000..65616ce7e63 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/converters/MyConfigurationPropertiesSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.converters + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import org.junit.Assert.assertEquals +import java.time.LocalDate + +//tag::configSpec[] +class MyConfigurationPropertiesSpec : AnnotationSpec() { + + //tag::runContext[] + lateinit var ctx: ApplicationContext + + @BeforeEach + fun setup() { + ctx = ApplicationContext.run( + mapOf( + "myapp.updatedAt" to mapOf( // <1> + "day" to 28, + "month" to 10, + "year" to 1982 + ) + ) + ) + } + + @AfterEach + fun teardown() { + ctx?.close() + } + //end::runContext[] + + @Test + fun testConvertDateFromMap() { + val props = ctx.getBean(MyConfigurationProperties::class.java) + + val expectedDate = LocalDate.of(1982, 10, 28) + assertEquals(expectedDate, props.updatedAt) + } +} +//end::configSpec[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt new file mode 100644 index 00000000000..81bb4bec3e8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceConfiguration.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +// tag::eachProperty[] +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import java.net.URI +import java.net.URISyntaxException + +@EachProperty("test.datasource") // <1> +class DataSourceConfiguration +@Throws(URISyntaxException::class) +constructor(@param:Parameter val name: String) { // <2> + var url = URI("localhost") // <3> +} +// end::eachProperty[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt new file mode 100644 index 00000000000..a3e74382bde --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/DataSourceFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +import io.micronaut.context.annotation.EachBean +import io.micronaut.context.annotation.Factory + +import java.net.URI +import java.sql.Connection + +// tag::eachBean[] +@Factory // <1> +class DataSourceFactory { + + @EachBean(DataSourceConfiguration::class) // <2> + internal fun dataSource(configuration: DataSourceConfiguration): DataSource { // <3> + val url = configuration.url + return DataSource(url) + } +// end::eachBean[] + + internal class DataSource(private val uri: URI) { + + fun connect(): Connection { + throw UnsupportedOperationException("Can't really connect. I'm not a real data source") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt new file mode 100644 index 00000000000..fd117219b35 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachBeanTest.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.docs.config.env.DataSourceFactory.DataSource +import io.micronaut.inject.qualifiers.Qualifiers +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import java.net.URISyntaxException + +class EachBeanTest : AnnotationSpec() { + + @Test + @Throws(URISyntaxException::class) + fun testEachBean() { + // tag::config[] + val applicationContext = ApplicationContext.run(PropertySource.of( + "test", + mapOf( + "test.datasource.one.url" to "jdbc:mysql://localhost/one", + "test.datasource.two.url" to "jdbc:mysql://localhost/two") + )) + // end::config[] + + // tag::beans[] + val beansOfType = applicationContext.getBeansOfType(DataSource::class.java) + assertEquals(2, beansOfType.size) // <1> + + val firstConfig = applicationContext.getBean( + DataSource::class.java, + Qualifiers.byName("one") // <2> + ) + // end::beans[] + + assertNotNull(firstConfig) + + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt new file mode 100644 index 00000000000..b98c695dbfe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EachPropertyTest.kt @@ -0,0 +1,66 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import io.micronaut.inject.qualifiers.Qualifiers +import org.junit.Assert.assertEquals +import java.net.URI +import java.net.URISyntaxException +import java.util.stream.Collectors + +class EachPropertyTest : AnnotationSpec() { + + @Test + @Throws(URISyntaxException::class) + fun testEachProperty() { + // tag::config[] + val applicationContext = ApplicationContext.run(PropertySource.of( + "test", + mapOf( + "test.datasource.one.url" to "jdbc:mysql://localhost/one", + "test.datasource.two.url" to "jdbc:mysql://localhost/two" + ) + )) + // end::config[] + + // tag::beans[] + val beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration::class.java) + assertEquals(2, beansOfType.size) // <1> + + val firstConfig = applicationContext.getBean( + DataSourceConfiguration::class.java, + Qualifiers.byName("one") // <2> + ) + + assertEquals( + URI("jdbc:mysql://localhost/one"), + firstConfig.url + ) + // end::beans[] + applicationContext.close() + } + + @Test + fun testEachPropertyList() { + val limits: MutableList> = ArrayList() + limits.add(CollectionUtils.mapOf("period", "10s", "limit", "1000")) + limits.add(CollectionUtils.mapOf("period", "1m", "limit", "5000")) + val applicationContext = ApplicationContext.run( + mapOf("ratelimits" to listOf( + mapOf("period" to "10s", "limit" to "1000"), + mapOf("period" to "1m", "limit" to "5000")))) + + val beansOfType = applicationContext.streamOfType(RateLimitsConfiguration::class.java).collect(Collectors.toList()) + + assertEquals( + 2, + beansOfType.size + ) + assertEquals(1000, beansOfType[0].limit) + assertEquals(5000, beansOfType[1].limit) + + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt new file mode 100644 index 00000000000..f2a1f9310ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/EnvironmentTest.kt @@ -0,0 +1,48 @@ +package io.micronaut.docs.config.env + +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.context.env.PropertySource +import io.micronaut.core.util.CollectionUtils +import org.junit.Test + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class EnvironmentTest: AnnotationSpec(){ + + @Test + fun testRunEnvironment() { + // tag::env[] + val applicationContext = ApplicationContext.run("test", "android") + val environment = applicationContext.environment + + assertTrue(environment.activeNames.contains("test")) + assertTrue(environment.activeNames.contains("android")) + // end::env[] + applicationContext.close() + } + + @Test + fun testRunEnvironmentWithProperties() { + // tag::envProps[] + val applicationContext = ApplicationContext.run( + PropertySource.of( + "test", + mapOf( + "micronaut.server.host" to "foo", + "micronaut.server.port" to 8080 + ) + ), + "test", "android") + val environment = applicationContext.environment + + assertEquals( + "foo", + environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost") + ) + // end::envProps[] + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt new file mode 100644 index 00000000000..a2ab0f32faa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/HighRateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +class HighRateLimit(period: Duration?, limit: Int) : RateLimit(period, limit) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt new file mode 100644 index 00000000000..9058bd62b37 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/LowRateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +class LowRateLimit(period: Duration?, limit: Int) : RateLimit(period, limit) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt new file mode 100644 index 00000000000..2a50dac6cc2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/OrderTest.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.config.env + +import io.micronaut.context.ApplicationContext +import org.junit.Assert +import org.junit.jupiter.api.Test +import java.util.stream.Collectors + +class OrderTest { + + @Test + fun testOrderOnFactories() { + val applicationContext = ApplicationContext.run() + val rateLimits = applicationContext.streamOfType(RateLimit::class.java) + .collect(Collectors.toList()) + Assert.assertEquals( + 2, + rateLimits.size + .toLong()) + Assert.assertEquals(1000L, rateLimits[0].limit.toLong()) + Assert.assertEquals(100L, rateLimits[1].limit.toLong()) + applicationContext.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt new file mode 100644 index 00000000000..637c6b505c3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimit.kt @@ -0,0 +1,5 @@ +package io.micronaut.docs.config.env + +import java.time.Duration + +open class RateLimit(val period: Duration?, val limit: Int) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt new file mode 100644 index 00000000000..77cd0ee9e4a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsConfiguration.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.env + +// tag::clazz[] +import io.micronaut.context.annotation.EachProperty +import io.micronaut.context.annotation.Parameter +import io.micronaut.core.order.Ordered +import java.time.Duration + +@EachProperty(value = "ratelimits", list = true) // <1> +class RateLimitsConfiguration + constructor(@param:Parameter private val index: Int) // <3> + : Ordered { // <2> + + var period: Duration? = null + var limit: Int? = null + + override fun getOrder(): Int { + return index + } +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt new file mode 100644 index 00000000000..3be4ffab393 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/env/RateLimitsFactory.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.config.env + +//tag::clazz[] +import io.micronaut.context.annotation.Factory +import io.micronaut.core.annotation.Order +import java.time.Duration +import jakarta.inject.Singleton + +@Factory +class RateLimitsFactory { + + @Singleton + @Order(20) + fun rateLimit2(): LowRateLimit { + return LowRateLimit(Duration.ofMinutes(50), 100) + } + + @Singleton + @Order(10) + fun rateLimit1(): HighRateLimit { + return HighRateLimit(Duration.ofMinutes(50), 1000) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt new file mode 100644 index 00000000000..e4e6d3f4c8d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Engine.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Engine(val config: EngineConfig)// <1> +{ + val cylinders: Int + get() = config.cylinders + + fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.getRodLength().orElse(6.0)}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt new file mode 100644 index 00000000000..04a8ecd1824 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/EngineConfig.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationInject +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.bind.annotation.Bindable +import java.util.Optional +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +data class EngineConfig @ConfigurationInject // <2> + constructor( + @Bindable(defaultValue = "Ford") @NotBlank val manufacturer: String, // <3> + @Min(1) val cylinders: Int, // <4> + @NotNull val crankShaft: CrankShaft) { + + @ConfigurationProperties("crank-shaft") + data class CrankShaft @ConfigurationInject + constructor(// <5> + private val rodLength: Double? // <6> + ) { + + fun getRodLength(): Optional { + return Optional.ofNullable(rodLength) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt new file mode 100644 index 00000000000..605770ebcfa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.immutable + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt new file mode 100644 index 00000000000..5a747d9019f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/immutable/VehicleSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.config.immutable + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=7.0]") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt new file mode 100644 index 00000000000..311ffc7387d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +import jakarta.inject.Singleton + +@Singleton +class Engine(val config: EngineConfig)// <1> +{ + val cylinders: Int + get() = config.cylinders + + fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.rodLength ?: 6.0}]" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt new file mode 100644 index 00000000000..2cc9e4d4961 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/EngineConfig.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.bind.annotation.Bindable +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +interface EngineConfig { + + @get:Bindable(defaultValue = "Ford") // <2> + @get:NotBlank // <3> + val manufacturer: String + + @get:Min(1L) + val cylinders: Int + + @get:NotNull + val crankShaft: CrankShaft // <4> + + @ConfigurationProperties("crank-shaft") + interface CrankShaft { // <5> + val rodLength: Double? // <6> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt new file mode 100644 index 00000000000..7568e699c49 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.itfce + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt new file mode 100644 index 00000000000..9d438d8d128 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/itfce/VehicleSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.config.itfce + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldContain +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.BeanInstantiationException + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + + val vehicle = applicationContext.getBean(Vehicle::class.java) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=7.0]") + + applicationContext.close() + } + + "test start vehicle - invalid" { + // tag::start[] + val map = mapOf( + "my.engine.cylinders" to "-10", + "my.engine.crank-shaft.rod-length" to "7.0" + ) + val applicationContext = ApplicationContext.run(map) + val exception = shouldThrow { + applicationContext.getBean(Vehicle::class.java) + } + exception.message.shouldContain("EngineConfig.getCylinders - must be greater than or equal to 1") + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt new file mode 100644 index 00000000000..5fa41458c08 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Engine.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +interface Engine { + val sensors: Map<*, *>? + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt new file mode 100644 index 00000000000..ceb841f3c77 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineConfig.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat +import javax.validation.constraints.Min +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") +class EngineConfig { + + @Min(1L) + var cylinders: Int = 0 + + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //<1> + var sensors: Map? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt new file mode 100644 index 00000000000..ac899f477e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/EngineImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineImpl : Engine { + + override val sensors: Map<*, *>? + get() = config!!.sensors + + @Inject + var config: EngineConfig? = null + + override fun start(): String { + return "Engine Starting V${config!!.cylinders} [sensors=${sensors!!.size}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt new file mode 100644 index 00000000000..b7a61c5e109 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.mapFormat + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt new file mode 100644 index 00000000000..8477d267cdb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/mapFormat/VehicleSpec.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.config.mapFormat + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + "test start vehicle" { + // tag::start[] + val subMap = mapOf( + 0 to "thermostat", + 1 to "fuel pressure" + ) + val map = mapOf( + "my.engine.cylinders" to "8", + "my.engine.sensors" to subMap + ) + + val applicationContext = ApplicationContext.run(map, "test") + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Engine Starting V8 [sensors=2]") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt new file mode 100644 index 00000000000..06989cc5a4b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +interface Engine { + val cylinders: Int + + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt new file mode 100644 index 00000000000..198beb4f6ef --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineConfig.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +// tag::imports[] +import io.micronaut.context.annotation.ConfigurationProperties +import java.util.Optional +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +// end::imports[] + +// tag::class[] +@ConfigurationProperties("my.engine") // <1> +class EngineConfig { + + @NotBlank // <2> + var manufacturer = "Ford" // <3> + + @Min(1L) + var cylinders: Int = 0 + + var crankShaft = CrankShaft() + + @ConfigurationProperties("crank-shaft") + class CrankShaft { // <4> + var rodLength: Optional = Optional.empty() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt new file mode 100644 index 00000000000..b52d39f91a4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/EngineImpl.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineImpl(val config: EngineConfig) : Engine {// <1> + + override val cylinders: Int + get() = config.cylinders + + override fun start(): String {// <2> + return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.rodLength.orElse(6.0)}]" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt new file mode 100644 index 00000000000..ec24edae554 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.properties + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine)// <6> +{ + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt new file mode 100644 index 00000000000..b8b477fd20e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/properties/VehicleSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.config.properties + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext + +class VehicleSpec: StringSpec({ + + "test start vehicle" { + // tag::start[] + val map = mapOf( "my.engine.cylinders" to "8") + val applicationContext = ApplicationContext.run(map, "test") + + val vehicle = applicationContext.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Ford Engine Starting V8 [rodLength=6.0]") + + applicationContext.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt new file mode 100644 index 00000000000..8685317738b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/Engine.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.property + +// tag::imports[] +import io.micronaut.context.annotation.Property + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// end::imports[] + +// tag::class[] +@Singleton +class Engine { + + @field:Property(name = "my.engine.cylinders") // <1> + protected var cylinders: Int = 0 // <2> + + @set:Inject + @setparam:Property(name = "my.engine.manufacturer") // <3> + var manufacturer: String? = null + + fun cylinders(): Int { + return cylinders + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt new file mode 100644 index 00000000000..ba2504c1bc6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/property/EngineSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.config.property + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import org.junit.Test + +import java.util.LinkedHashMap + +import org.junit.Assert.assertEquals + +class EngineSpec : StringSpec({ + + "test start vehicle with configuration" { + val ctx = ApplicationContext.run(mapOf("my.engine.cylinders" to "8", "my.engine.manufacturer" to "Honda")) + + val engine = ctx.getBean(Engine::class.java) + + engine.manufacturer shouldBe "Honda" + engine.cylinders() shouldBe 8 + + ctx.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt new file mode 100644 index 00000000000..ca9b9481140 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +interface Engine { + val cylinders: Int + + fun start(): String +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt new file mode 100644 index 00000000000..c715c3b7e60 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/EngineImpl.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +// tag::imports[] +import io.micronaut.context.annotation.Value + +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class EngineImpl : Engine { + + @Value("\${my.engine.cylinders:6}") // <1> + override var cylinders: Int = 0 + protected set + + override fun start(): String { // <2> + return "Starting V$cylinders Engine" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt new file mode 100644 index 00000000000..faa25f79933 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/Vehicle.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.config.value + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) {// <6> + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt new file mode 100644 index 00000000000..9b68e46963c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/config/value/VehicleSpec.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.config.value + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.DefaultApplicationContext +import io.micronaut.context.env.PropertySource +import org.codehaus.groovy.runtime.DefaultGroovyMethods + +class VehicleSpec : StringSpec({ + + "test start vehicle with configuration" { + // tag::start[] + val applicationContext = DefaultApplicationContext("test") + val map = mapOf("my.engine.cylinders" to "8") + applicationContext.getEnvironment().addPropertySource(PropertySource.of("test", map)) + applicationContext.start() + + val vehicle = applicationContext.getBean(Vehicle::class.java) + DefaultGroovyMethods.println(this, vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8 Engine") + + applicationContext.close() + } + + "test start vehicle without configuration" { + // tag::start[] + val applicationContext = DefaultApplicationContext("test") + applicationContext.start() + + val vehicle = applicationContext.getBean(Vehicle::class.java) + DefaultGroovyMethods.println(this, vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V6 Engine") + + applicationContext.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt new file mode 100644 index 00000000000..8c86c012ad4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/Application.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context + +// tag::imports[] +import io.micronaut.runtime.Micronaut +// end::imports[] + +// tag::class[] +object Application { + + @JvmStatic + fun main(args: Array) { + Micronaut.build(null) + .mainClass(Application::class.java) + .environmentPropertySource(false) + //or + .environmentVariableIncludes("THIS_ENV_ONLY") + //or + .environmentVariableExcludes("EXCLUDED_ENV") + .start() + } +} +// end::class[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt new file mode 100644 index 00000000000..34e1528e814 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Blue.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import jakarta.inject.Singleton +//end::imports[] + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Singleton +class Blue: ColorPicker { + override fun color(): String { + return "blue" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt new file mode 100644 index 00000000000..e4d18b0924d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/ColorPicker.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +//tag::clazz[] +interface ColorPicker { + fun color(): String +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt new file mode 100644 index 00000000000..a173d92ba4f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/Green.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires + +//tag::imports[] +import io.micronaut.context.annotation.Primary +import jakarta.inject.Singleton +//end::imports[] + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Primary +@Singleton +class Green: ColorPicker { + override fun color(): String { + return "green" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt new file mode 100644 index 00000000000..eec17a5a6ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/PrimarySpec.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.context.annotation.primary + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class PrimarySpec : StringSpec() { + + val embeddedServer = autoClose(ApplicationContext.run(EmbeddedServer::class.java, mapOf( + "spec.name" to "primaryspec" + ), Environment.TEST)) + + val rxClient = autoClose(embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL())) + + init { + "test @Primary annotated beans gets injected in case of a collection" { + embeddedServer.applicationContext.getBeansOfType(ColorPicker::class.java).size.shouldBe(2) + val rsp = rxClient.toBlocking().exchange(HttpRequest.GET("/test"), String::class.java) + + rsp.status.shouldBe(HttpStatus.OK) + rsp.body().shouldBe("green") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt new file mode 100644 index 00000000000..16400790075 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/annotation/primary/TestController.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.annotation.primary + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Requires(property = "spec.name", value = "primaryspec") +//tag::clazz[] +@Controller("/test") +class TestController(val colorPicker: ColorPicker) { // <1> + + @Get + fun index(): String { + return colorPicker.color() + } +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt new file mode 100644 index 00000000000..b61ce0f2e03 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/DefaultEnvironmentSpec.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.context.env + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.Environment +import org.junit.Assert.assertFalse + +class DefaultEnvironmentSpec : StringSpec({ + + // tag::disableEnvDeduction[] + "test disable environment deduction via builder"() { + val ctx = ApplicationContext.builder().deduceEnvironment(false).start() + assertFalse(ctx.environment.activeNames.contains(Environment.TEST)) + ctx.close() + } + // end::disableEnvDeduction[] +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt new file mode 100644 index 00000000000..388c75a99e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/env/EnvironmentSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.context.env + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.context.env.PropertySource +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class EnvironmentSpec : StringSpec({ + + "test run environment" { + // tag::env[] + val applicationContext = ApplicationContext.run("test", "android") + val environment = applicationContext.environment + + assertTrue(environment.activeNames.contains("test")) + assertTrue(environment.activeNames.contains("android")) + // end::env[] + applicationContext.close() + } + + "test run environment with properties" { + // tag::envProps[] + val applicationContext = ApplicationContext.run( + PropertySource.of( + "test", + mapOf( + "micronaut.server.host" to "foo", + "micronaut.server.port" to 8080 + ) + ), + "test", "android" + ) + val environment = applicationContext.environment + + assertEquals( + "foo", + environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost") + ) + // end::envProps[] + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt new file mode 100644 index 00000000000..df89e5c157f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEvent.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events +// tag::class[] +data class SampleEvent(val message: String = "Something happened") +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt new file mode 100644 index 00000000000..a040a023fd5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/SampleEventEmitterBean.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events + +// tag::class[] +import io.micronaut.context.event.ApplicationEventPublisher +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class SampleEventEmitterBean { + + @Inject + internal var eventPublisher: ApplicationEventPublisher? = null + + fun publishSampleEvent() { + eventPublisher!!.publishEvent(SampleEvent()) + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt new file mode 100644 index 00000000000..9e269d3f1eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListener.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.application + +// tag::imports[] +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.docs.context.events.SampleEvent +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class SampleEventListener : ApplicationEventListener { + var invocationCounter = 0 + + override fun onApplicationEvent(event: SampleEvent) { + invocationCounter++ + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..fe1967802d6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/application/SampleEventListenerSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.context.events.application + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean +// end::imports[] + +// tag::class[] +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.shouldBe(0) + emitter.publishSampleEvent() + listener.invocationCounter.shouldBe(1) + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt new file mode 100644 index 00000000000..4e0ca3c3c5e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListener.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.async + +// tag::imports[] +import io.micronaut.docs.context.events.SampleEvent +import io.micronaut.runtime.event.annotation.EventListener +import io.micronaut.scheduling.annotation.Async +import java.util.concurrent.atomic.AtomicInteger +// end::imports[] +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +open class SampleEventListener { + + var invocationCounter = AtomicInteger(0) + + @EventListener + @Async + open fun onSampleEvent(event: SampleEvent) { + println("Incrementing invocation counter...") + invocationCounter.getAndIncrement() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..81a441f7487 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/async/SampleEventListenerSpec.kt @@ -0,0 +1,35 @@ +package io.micronaut.docs.context.events.async + +// tag::imports[] +import io.kotest.assertions.timing.eventually +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.AnnotationSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean +import org.opentest4j.AssertionFailedError +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.toDuration +// end::imports[] + +// tag::class[] +@ExperimentalTime +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + suspend fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.get().shouldBe(0) + emitter.publishSampleEvent() + + eventually(5.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) { + println("Current value of counter: " + listener.invocationCounter.get()) + listener.invocationCounter.get().shouldBe(1) + } + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt new file mode 100644 index 00000000000..b5d8580cf17 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListener.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.context.events.listener + +// tag::imports[] +import io.micronaut.docs.context.events.SampleEvent +import io.micronaut.context.event.StartupEvent +import io.micronaut.context.event.ShutdownEvent +import io.micronaut.runtime.event.annotation.EventListener +// end::imports[] +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class SampleEventListener { + var invocationCounter = 0 + + @EventListener + internal fun onSampleEvent(event: SampleEvent) { + invocationCounter++ + } + + @EventListener + internal fun onStartupEvent(event: StartupEvent) { + // startup logic here + } + + @EventListener + internal fun onShutdownEvent(event: ShutdownEvent) { + // shutdown logic here + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt new file mode 100644 index 00000000000..14532cc948e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/context/events/listener/SampleEventListenerSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.context.events.listener + +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.context.events.SampleEventEmitterBean + +// tag::class[] +class SampleEventListenerSpec : AnnotationSpec() { + + @Test + fun testEventListenerWasNotified() { + val context = ApplicationContext.run() + val emitter = context.getBean(SampleEventEmitterBean::class.java) + val listener = context.getBean(SampleEventListener::class.java) + listener.invocationCounter.shouldBe(0) + emitter.publishSampleEvent() + listener.invocationCounter.shouldBe(1) + + context.close() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt new file mode 100644 index 00000000000..db0e6ef981f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/Email.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +//tag::clazz[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank + +@Introspected +open class Email { + + @NotBlank // <1> + var subject: String? = null + + @NotBlank(groups = [FinalValidation::class]) // <2> + var recipient: String? = null +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt new file mode 100644 index 00000000000..3bb5439d8ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.Valid +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationgroups") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Post("/createDraft") + open fun createDraft(@Body @Valid email: Email): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } + + @Post("/send") + @Validated(groups = [FinalValidation::class]) // <3> + open fun send(@Body @Valid email: Email): HttpResponse<*> { // <4> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt new file mode 100644 index 00000000000..994781d828c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/EmailControllerSpec.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationgroups")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::pojovalidateddefault[] + "test pojo validation using default validation groups" { + val e = shouldThrow { + val email = Email() + email.subject = "" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "" + response = client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidateddefault[] + + //tag::pojovalidatedfinal[] + "test pojo validation using FinalValidation validation group" { + val e = shouldThrow { + val email = Email() + email.subject = "Hi" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "me@micronaut.example" + response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidatedfinal[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt new file mode 100644 index 00000000000..a8fe62a3e1d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/groups/FinalValidation.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.groups + +//tag::clazz[] + +import javax.validation.groups.Default + +interface FinalValidation : Default {} // <1> + +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt new file mode 100644 index 00000000000..66bd525b9c9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailController.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.params + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.validation.Validated +import javax.validation.constraints.NotBlank +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationparams") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Get("/send") + open fun send(@NotBlank recipient: String, // <2> + @NotBlank subject: String): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt new file mode 100644 index 00000000000..dd163e21500 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/params/EmailControllerSpec.kt @@ -0,0 +1,38 @@ +package io.micronaut.docs.datavalidation.params + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationparams")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::paramsvalidated[] + "test params are validated"() { + val e = shouldThrow { + client.toBlocking().exchange("/email/send?subject=Hi&recipient=") + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + response = client.toBlocking().exchange("/email/send?subject=Hi&recipient=me@micronaut.example") + + response.status shouldBe HttpStatus.OK + } + //end::paramsvalidated[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt new file mode 100644 index 00000000000..970db1d7b7b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/Email.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.pogo + +//tag::clazz[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank + +@Introspected +open class Email { + + @NotBlank // <1> + var subject: String? = null + + @NotBlank // <1> + var recipient: String? = null +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt new file mode 100644 index 00000000000..7933d0c764a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailController.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.datavalidation.pogo + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.validation.Validated +import javax.validation.Valid +//end::imports[] + +@Requires(property = "spec.name", value = "datavalidationpogo") +//tag::clazz[] +@Validated // <1> +@Controller("/email") +open class EmailController { + + @Post("/send") + open fun send(@Body @Valid email: Email): HttpResponse<*> { // <2> + return HttpResponse.ok(mapOf("msg" to "OK")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt new file mode 100644 index 00000000000..c74cd35edd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/datavalidation/pogo/EmailControllerSpec.kt @@ -0,0 +1,45 @@ +package io.micronaut.docs.datavalidation.pogo + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class EmailControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "datavalidationpogo")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::pojovalidated[] + "test pojo validation" { + val e = shouldThrow { + val email = Email() + email.subject = "Hi" + email.recipient = "" + client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + } + var response = e.response + + response.status shouldBe HttpStatus.BAD_REQUEST + + val email = Email() + email.subject = "Hi" + email.recipient = "me@micronaut.example" + response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email)) + + response.status shouldBe HttpStatus.OK + } + //end::pojovalidated[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt new file mode 100644 index 00000000000..3057e797081 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt new file mode 100644 index 00000000000..b142ad5cce5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import io.micronaut.context.annotation.Factory + +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class EngineFactory { + + private var engine: V8Engine? = null + private var rodLength = 5.7 + + @PostConstruct + fun initialize() { + engine = V8Engine(rodLength) // <2> + } + + @Singleton + fun v8Engine(): Engine? { + return engine// <3> + } + + fun setRodLength(rodLength: Double) { + this.rodLength = rodLength + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt new file mode 100644 index 00000000000..b1e9b9f6c53 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/EngineInitializer.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import io.micronaut.context.event.BeanInitializedEventListener +import io.micronaut.context.event.BeanInitializingEvent + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class EngineInitializer : BeanInitializedEventListener { // <4> + override fun onInitialized(event: BeanInitializingEvent): EngineFactory { + val engineFactory = event.bean + engineFactory.setRodLength(6.6) // <5> + return engineFactory + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt new file mode 100644 index 00000000000..189d1e46b05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/V8Engine.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +// tag::class[] +class V8Engine(var rodLength: Double) : Engine { // <1> + + override val cylinders = 8 + + override fun start(): String { + return "Starting V$cylinders [rodLength=$rodLength]" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt new file mode 100644 index 00000000000..4b48d41ea8f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/Vehicle.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.events.factory + +import jakarta.inject.Singleton + +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt new file mode 100644 index 00000000000..20ef41d36ba --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/events/factory/VehicleSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.events.factory + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.Test + +import org.junit.Assert.assertEquals + +class VehicleSpec : StringSpec({ + + "test start vehicle" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8 [rodLength=6.6]") + context.close() + } + +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt new file mode 100644 index 00000000000..8f3f179a63a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/CrankShaft.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class CrankShaft +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt new file mode 100644 index 00000000000..bb0a39a7f6f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +// tag::class[] +interface Engine { + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt new file mode 100644 index 00000000000..ab63495cd32 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/EngineFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import jakarta.inject.Singleton + +/** + * @author Graeme Rocher + * @since 1.0 + */ +// tag::class[] +@Factory +internal class EngineFactory { + + @Singleton + fun v8Engine(crankShaft: CrankShaft): Engine { + return V8Engine(crankShaft) + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt new file mode 100644 index 00000000000..5fd6f820043 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/V8Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +// tag::class[] +internal class V8Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt new file mode 100644 index 00000000000..6e0b0fb4fd9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle(val engine: Engine) { + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt new file mode 100644 index 00000000000..bdcb2d73b22 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.factories + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Replaces +import io.micronaut.test.annotation.MockBean +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +// tag::class[] +@MicronautTest +class VehicleMockSpec { + @MockBean(Engine::class) + val mockEngine: Engine = object : Engine { // <1> + override fun start(): String { + return "Mock Started" + } + } + + @Inject + lateinit var vehicle : Vehicle // <2> + + @Test + fun testStartEngine() { + val result = vehicle.start() + Assertions.assertEquals("Mock Started", result) // <3> + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt new file mode 100644 index 00000000000..4ee90674d38 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.factories + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class VehicleSpec { + + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt new file mode 100644 index 00000000000..dca422eb79c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +// tag::class[] +interface Engine { + fun getCylinders(): Int +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt new file mode 100644 index 00000000000..9f925b06b0b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineConfiguration.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +import io.micronaut.context.annotation.EachProperty +import io.micronaut.core.util.Toggleable +import javax.validation.constraints.NotNull + +// tag::class[] +@EachProperty("engines") +class EngineConfiguration : Toggleable { + + var enabled = true + + @NotNull + val cylinders: Int? = null + + override fun isEnabled(): Boolean { + return enabled + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt new file mode 100644 index 00000000000..e8e6c303f6a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable + +import io.micronaut.context.annotation.EachBean +import io.micronaut.context.annotation.Factory +import io.micronaut.context.exceptions.DisabledBeanException + +// tag::class[] +@Factory +class EngineFactory { + + @EachBean(EngineConfiguration::class) + fun buildEngine(engineConfiguration: EngineConfiguration): Engine? { + return if (engineConfiguration.isEnabled) { + object : Engine { + override fun getCylinders(): Int { + return engineConfiguration.cylinders!! + } + } + } else { + throw DisabledBeanException("Engine configuration disabled") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java new file mode 100644 index 00000000000..b055e913d38 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/nullable/EngineSpec.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.factories.nullable; + +import io.micronaut.context.ApplicationContext; +import org.junit.Test; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class EngineSpec { + + @Test + public void testEngineNull() { + Map configuration = new HashMap<>(); + configuration.put("engines.subaru.cylinders", 4); + configuration.put("engines.ford.cylinders", 8); + configuration.put("engines.ford.enabled", false); + configuration.put("engines.lamborghini.cylinders", 12); + ApplicationContext applicationContext = ApplicationContext.run(configuration); + + Collection engines = applicationContext.getBeansOfType(Engine.class); + + assertEquals("There are 2 engines", 2, engines.size()); + int totalCylinders = engines.stream().mapToInt(Engine::getCylinders).sum(); + assertEquals("Subaru + Lamborghini equals 16 cylinders", 16, totalCylinders); + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt new file mode 100644 index 00000000000..3325c0e0007 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/CylinderFactory.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.factories.primitive + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory +import jakarta.inject.Named +// end::imports[] + +// tag::class[] +@Factory +class CylinderFactory { + @get:Bean + @get:Named("V8") // <1> + val v8 = 8 + + @get:Bean + @get:Named("V6") // <1> + val v6 = 6 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt new file mode 100644 index 00000000000..c8dfc11bd41 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/EngineSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.factories.primitive + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class EngineSpec { + @Test + fun testEngine() { + BeanContext.run().use { beanContext -> + val engine = + beanContext.getBean( + V8Engine::class.java + ) + Assertions.assertEquals( + 8, + engine.cylinders + ) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt new file mode 100644 index 00000000000..a902155ca26 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/factories/primitive/V8Engine.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.factories.primitive + +// tag::imports[] +import jakarta.inject.Named +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class V8Engine( + @param:Named("V8") val cylinders: Int // <1> +) +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt new file mode 100644 index 00000000000..f73d7adcdd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/ClientBindController.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.http.client.bind + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.QueryValue +import javax.annotation.Nullable + +@Controller +class ClientBindController { + + @Get("/client/bind") + fun test(@Header("X-Metadata-Version") version: String): String { + return version + } + + @Get("/client/authorized-resource{?name}") + fun authorized(@QueryValue @Nullable name: String): String { + return "Hello, $name" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt new file mode 100644 index 00000000000..3db5fcd8526 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/AnnotationBinderSpec.kt @@ -0,0 +1,23 @@ +package io.micronaut.docs.http.client.bind.annotation + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.* + +class AnnotationBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(MetadataClient::class.java) + + val metadata: MutableMap = LinkedHashMap() + metadata["version"] = 3.6 + metadata["deploymentId"] = 42L + val resp = client.get(metadata) + Assertions.assertEquals("3.6", resp) + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt new file mode 100644 index 00000000000..e78e124c85d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/Metadata.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.annotation + +//tag::clazz[] +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(VALUE_PARAMETER) +@Bindable +annotation class Metadata +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt new file mode 100644 index 00000000000..eb19dff9ad4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClient.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.annotation + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +//tag::clazz[] +@Client("/") +interface MetadataClient { + + @Get("/client/bind") + operator fun get(@Metadata metadata: Map): String +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt new file mode 100644 index 00000000000..89aeace9955 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/annotation/MetadataClientArgumentBinder.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.http.client.bind.annotation + +//tag::clazz[] +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.naming.NameUtils +import io.micronaut.core.util.StringUtils +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder +import io.micronaut.http.client.bind.ClientRequestUriContext +import jakarta.inject.Singleton + +@Singleton +class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder { + + override fun getAnnotationType(): Class { + return Metadata::class.java + } + + override fun bind(context: ArgumentConversionContext, + uriContext: ClientRequestUriContext, + value: Any, + request: MutableHttpRequest<*>) { + if (value is Map<*, *>) { + for ((key1, value1) in value) { + val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false) + request.header("X-Metadata-$key", value1.toString()) + } + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt new file mode 100644 index 00000000000..0c544111b06 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/MethodBinderSpec.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class MethodBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(NameAuthorizedClient::class.java) + + val resp = client.get() + Assertions.assertEquals("Hello, Bob", resp) + + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt new file mode 100644 index 00000000000..eb0118d1138 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorization.kt @@ -0,0 +1,15 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.context.annotation.AliasFor +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.FUNCTION + +//tag::clazz[] +@MustBeDocumented +@Retention(RUNTIME) +@Target(FUNCTION) // <1> +@Bindable +annotation class NameAuthorization(val name: String = "") + +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt new file mode 100644 index 00000000000..41f3244bb01 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizationBinder.kt @@ -0,0 +1,29 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.client.bind.ClientRequestUriContext; +import jakarta.inject.Singleton; + +//tag::clazz[] +import io.micronaut.http.client.bind.AnnotatedClientRequestBinder + +@Singleton // <1> +class NameAuthorizationBinder: AnnotatedClientRequestBinder { // <2> + @NonNull + override fun getAnnotationType(): Class { + return NameAuthorization::class.java + } + + override fun bind( // <3> + @NonNull context: MethodInvocationContext, + @NonNull uriContext: ClientRequestUriContext, + @NonNull request: MutableHttpRequest<*> + ) { + context.getValue(NameAuthorization::class.java, "name") + .ifPresent { name -> uriContext.addQueryParameter("name", name.toString()) } + + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt new file mode 100644 index 00000000000..28fbe4dc1ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/method/NameAuthorizedClient.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.http.client.bind.method; + +import io.micronaut.http.annotation.Get; +import io.micronaut.http.client.annotation.Client; + +//tag::clazz[] +@Client("/") +public interface NameAuthorizedClient { + + @Get("/client/authorized-resource") + @NameAuthorization(name="Bob") // <1> + fun get(): String +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt new file mode 100644 index 00000000000..c3bd68aa922 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/CustomBinderSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.http.client.bind.type + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class CustomBinderSpec { + + @Test + fun testBindingToTheRequest() { + val server = ApplicationContext.run(EmbeddedServer::class.java) + val client = server.applicationContext.getBean(MetadataClient::class.java) + val resp = client.get(Metadata(3.6, 42L)) + Assertions.assertEquals("3.6", resp) + server.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt new file mode 100644 index 00000000000..d75d2eba32c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/Metadata.kt @@ -0,0 +1,3 @@ +package io.micronaut.docs.http.client.bind.type + +class Metadata(val version: Double, val deploymentId: Long) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt new file mode 100644 index 00000000000..819017883af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClient.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.http.client.bind.type + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +//tag::clazz[] +@Client("/") +interface MetadataClient { + + @Get("/client/bind") + operator fun get(metadata: Metadata?): String? +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt new file mode 100644 index 00000000000..60fcd65bfa1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/bind/type/MetadataClientArgumentBinder.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.http.client.bind.type + +//tag::clazz[] +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.type.Argument +import io.micronaut.http.MutableHttpRequest +import io.micronaut.http.client.bind.ClientRequestUriContext +import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder +import jakarta.inject.Singleton + +@Singleton +class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder { + + override fun argumentType(): Argument { + return Argument.of(Metadata::class.java) + } + + override fun bind( + context: ArgumentConversionContext, + uriContext: ClientRequestUriContext, + value: Metadata, + request: MutableHttpRequest<*> + ) { + request.header("X-Metadata-Version", value.version.toString()) + request.header("X-Metadata-Deployment-Id", value.deploymentId.toString()) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt new file mode 100644 index 00000000000..27ee9d40dd6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/client/proxy/ProxyFilter.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.http.client.proxy + +// tag::imports[] +import io.micronaut.core.async.publisher.Publishers +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.client.ProxyHttpClient +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.http.uri.UriBuilder +import io.micronaut.runtime.server.EmbeddedServer +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Filter("/proxy/**") +class ProxyFilter( + private val client: ProxyHttpClient, // <2> + private val embeddedServer: EmbeddedServer +) : HttpServerFilter { // <1> + + override fun doFilter(request: HttpRequest<*>, + chain: ServerFilterChain): Publisher> { + return Publishers.map(client.proxy( // <3> + request.mutate() // <4> + .uri { b: UriBuilder -> // <5> + b.apply { + scheme("http") + host(embeddedServer.host) + port(embeddedServer.port) + replacePath(StringUtils.prependUri( + "/real", + request.path.substring("/proxy".length)) + ) + } + } + .header("X-My-Request-Header", "XXX") // <6> + ), { response: MutableHttpResponse<*> -> response.header("X-My-Response-Header", "YYY") }) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt new file mode 100644 index 00000000000..33ac7b0287b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/ShoppingCartControllerTests.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.http.server.bind + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.cookie.Cookie +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions + +class ShoppingCartControllerTest: StringSpec(){ + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test binding bad credentials" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/annotated") + .cookie(Cookie.of("shoppingCart", "{}")) + + val responseException = Assertions.assertThrows(HttpClientResponseException::class.java) { + client.toBlocking().retrieve(request) + } + val embedded: Map<*, *> = responseException.response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + responseException shouldNotBe null + message shouldBe "Required ShoppingCart [sessionId] not specified" + } + + "test annotation binding" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/annotated") + .cookie(Cookie.of("shoppingCart", "{\"sessionId\":5}")) + val response: String = client.toBlocking().retrieve(request, String::class.java) + + response shouldNotBe null + response shouldBe "Session:5" + } + + "test typed binding" { + val request: HttpRequest<*> = HttpRequest.GET("/customBinding/typed") + .cookie(Cookie.of("shoppingCart", "{\"sessionId\": 5, \"total\": 20}")) + val body: Map = client.toBlocking().retrieve(request, Argument.mapOf(String::class.java, Any::class.java)) + + body shouldNotBe null + body["sessionId"] shouldBe "5" + body["total"] shouldBe 20 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt new file mode 100644 index 00000000000..530483cfa50 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCart.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.http.server.bind.annotation + +// tag::class[] +import io.micronaut.core.bind.annotation.Bindable +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS +import kotlin.annotation.AnnotationTarget.FIELD +import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER + +@Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS) +@Retention(RUNTIME) +@Bindable //<1> +annotation class ShoppingCart(val value: String = "") +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt new file mode 100644 index 00000000000..c8c80c029eb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartController.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.http.server.bind.annotation + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/customBinding") +class ShoppingCartController { + + // tag::method[] + @Get("/annotated") + fun checkSession(@ShoppingCart sessionId: Long): HttpResponse { //<1> + return HttpResponse.ok("Session:$sessionId") + } + // end::method[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt new file mode 100644 index 00000000000..c6f7c3653b8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/annotation/ShoppingCartRequestArgumentBinder.kt @@ -0,0 +1,44 @@ +package io.micronaut.docs.http.server.bind.annotation + +// tag::class[] +import io.micronaut.core.bind.ArgumentBinder.BindingResult +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.convert.ConversionService +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder +import io.micronaut.jackson.serialize.JacksonObjectSerializer +import java.util.Optional +import jakarta.inject.Singleton + +@Singleton +class ShoppingCartRequestArgumentBinder( + private val conversionService: ConversionService, + private val objectSerializer: JacksonObjectSerializer +) : AnnotatedRequestArgumentBinder { //<1> + + override fun getAnnotationType(): Class { + return ShoppingCart::class.java + } + + override fun bind(context: ArgumentConversionContext, + source: HttpRequest<*>): BindingResult { //<2> + + val parameterName = context.annotationMetadata + .stringValue(ShoppingCart::class.java) + .orElse(context.argument.name) + + val cookie = source.cookies.get("shoppingCart") ?: return BindingResult.EMPTY + + val cookieValue: Optional> = objectSerializer.deserialize( + cookie.value.toByteArray(), + Argument.mapOf(String::class.java, Any::class.java)) + + return BindingResult { + cookieValue.flatMap { map: Map -> + conversionService.convert(map[parameterName], context) + } + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt new file mode 100644 index 00000000000..19c95471948 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCart.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.http.server.bind.type + +// tag::class[] +import io.micronaut.core.annotation.Introspected + +@Introspected +class ShoppingCart { + var sessionId: String? = null + var total: Int? = null +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt new file mode 100644 index 00000000000..68d912680b8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartController.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.http.server.bind.type + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Controller("/customBinding") +class ShoppingCartController { + + // tag::method[] + @Get("/typed") + fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { //<1> + return HttpResponse.ok(mapOf( + "sessionId" to shoppingCart.sessionId, + "total" to shoppingCart.total)) + } + // end::method[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt new file mode 100644 index 00000000000..ab1b5704187 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/bind/type/ShoppingCartRequestArgumentBinder.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.http.server.bind.type + +// tag::class[] +import io.micronaut.core.bind.ArgumentBinder +import io.micronaut.core.bind.ArgumentBinder.BindingResult +import io.micronaut.core.convert.ArgumentConversionContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.bind.binders.TypedRequestArgumentBinder +import io.micronaut.jackson.serialize.JacksonObjectSerializer +import java.util.Optional +import jakarta.inject.Singleton + +@Singleton +class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) : + TypedRequestArgumentBinder { + + override fun bind( + context: ArgumentConversionContext, + source: HttpRequest<*> + ): BindingResult { //<1> + + val cookie = source.cookies["shoppingCart"] + + return if (cookie == null) + BindingResult { + Optional.empty() + } + else { + BindingResult { + objectSerializer.deserialize( // <2> + cookie.value.toByteArray(), + ShoppingCart::class.java + ) + } + } + } + + override fun argumentType(): Argument { + return Argument.of(ShoppingCart::class.java) //<3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt new file mode 100644 index 00000000000..90b0246d1cb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/executeon/PersonController.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.http.server.executeon + +// tag::imports[] +import io.micronaut.docs.http.server.reactive.PersonService +import io.micronaut.docs.ioc.beans.Person +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +// end::imports[] + +// tag::class[] +@Controller("/executeOn/people") +class PersonController (private val personService: PersonService) { + + @Get("/{name}") + @ExecuteOn(TaskExecutors.IO) // <1> + fun byName(name: String): Person { + return personService.findByName(name) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt new file mode 100644 index 00000000000..9f6520ff33f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatClientWebSocket.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Future +// end::imports[] + +// tag::class[] +@ClientWebSocket("/chat/{topic}/{username}") // <1> +abstract class ChatClientWebSocket : AutoCloseable { // <2> + + var session: WebSocketSession? = null + private set + var request: HttpRequest<*>? = null + private set + var topic: String? = null + private set + var username: String? = null + private set + private val replies = ConcurrentLinkedQueue() + + @OnOpen + fun onOpen(topic: String, username: String, + session: WebSocketSession, request: HttpRequest<*>) { // <3> + this.topic = topic + this.username = username + this.session = session + this.request = request + } + + fun getReplies(): Collection { + return replies + } + + @OnMessage + fun onMessage(message: String) { + replies.add(message) // <4> + } + + // end::class[] + abstract fun send(message: String) + + abstract fun sendAsync(message: String): Future + + abstract fun sendRx(message: String): Mono +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt new file mode 100644 index 00000000000..5eb22739c03 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ChatServerWebSocket.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +//tag::clazz[] +import io.micronaut.websocket.WebSocketBroadcaster +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket + +import java.util.function.Predicate + +@ServerWebSocket("/chat/{topic}/{username}") // <1> +class ChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) { + + @OnOpen // <2> + fun onOpen(topic: String, username: String, session: WebSocketSession) { + val msg = "[$username] Joined!" + broadcaster.broadcastSync(msg, isValid(topic, session)) + } + + @OnMessage // <3> + fun onMessage(topic: String, username: String, + message: String, session: WebSocketSession) { + val msg = "[$username] $message" + broadcaster.broadcastSync(msg, isValid(topic, session)) // <4> + } + + @OnClose // <5> + fun onClose(topic: String, username: String, session: WebSocketSession) { + val msg = "[$username] Disconnected!" + broadcaster.broadcastSync(msg, isValid(topic, session)) + } + + private fun isValid(topic: String, session: WebSocketSession): Predicate { + return Predicate { + (it !== session && topic.equals(it.uriVariables.get("topic", String::class.java, null), ignoreCase = true)) + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt new file mode 100644 index 00000000000..0874a3e4559 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/Message.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import java.util.Objects + +class Message { + + var text: String? = null + + constructor(text: String) { + this.text = text + } + + internal constructor() {} + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val message = o as Message? + return text == message!!.text + } + + override fun hashCode(): Int { + return Objects.hash(text) + } + + override fun toString(): String { + return "Message{" + + "text='" + text + '\''.toString() + + '}'.toString() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt new file mode 100644 index 00000000000..b80fd46444c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/PojoChatClientWebSocket.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import reactor.core.publisher.Mono +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Future + +@ClientWebSocket("/pojo/chat/{topic}/{username}") +abstract class PojoChatClientWebSocket : AutoCloseable { + + var topic: String? = null + private set + var username: String? = null + private set + private val replies = ConcurrentLinkedQueue() + + @OnOpen + fun onOpen(topic: String, username: String) { + this.topic = topic + this.username = username + } + + fun getReplies(): Collection { + return replies + } + + @OnMessage + fun onMessage( + message: Message) { + println("Client received message = $message") + replies.add(message) + } + + abstract fun send(message: Message) + + abstract fun sendAsync(message: Message): Future + + abstract fun sendRx(message: Message): Mono +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt new file mode 100644 index 00000000000..15fb17b6ac8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/netty/websocket/ReactivePojoChatServerWebSocket.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.http.server.netty.websocket + +import io.micronaut.websocket.WebSocketBroadcaster +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.OnClose +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.OnOpen +import io.micronaut.websocket.annotation.ServerWebSocket +import org.reactivestreams.Publisher + +import java.util.function.Predicate + +@ServerWebSocket("/pojo/chat/{topic}/{username}") +class ReactivePojoChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) { + + @OnOpen + fun onOpen(topic: String, username: String, session: WebSocketSession): Publisher { + val text = "[$username] Joined!" + val message = Message(text) + return broadcaster.broadcast(message, isValid(topic, session)) + } + + // tag::onmessage[] + @OnMessage + fun onMessage(topic: String, username: String, + message: Message, session: WebSocketSession): Publisher { + val text = "[" + username + "] " + message.text + val newMessage = Message(text) + return broadcaster.broadcast(newMessage, isValid(topic, session)) + } + // end::onmessage[] + + @OnClose + fun onClose(topic: String, username: String, + session: WebSocketSession): Publisher { + val text = "[$username] Disconnected!" + val message = Message(text) + return broadcaster.broadcast(message, isValid(topic, session)) + } + + private fun isValid(topic: String, session: WebSocketSession): Predicate { + return Predicate { + it !== session && topic.equals( + it.uriVariables.get("topic", String::class.java, null), ignoreCase = true) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt new file mode 100644 index 00000000000..3e29f1639d3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonController.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.http.server.reactive + +// tag::imports[] +import io.micronaut.docs.ioc.beans.Person +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.scheduling.TaskExecutors +import java.util.concurrent.ExecutorService +import jakarta.inject.Named +import reactor.core.publisher.Mono +import reactor.core.scheduler.Scheduler +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Controller("/subscribeOn/people") +class PersonController internal constructor( + @Named(TaskExecutors.IO) executorService: ExecutorService, // <1> + private val personService: PersonService) { + + private val scheduler: Scheduler = Schedulers.fromExecutorService(executorService) + + @Get("/{name}") + fun byName(name: String): Mono { + return Mono + .fromCallable { personService.findByName(name) } // <2> + .subscribeOn(scheduler) // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt new file mode 100644 index 00000000000..8aa0ff870ca --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/reactive/PersonService.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.http.server.reactive + +import io.micronaut.docs.ioc.beans.Person +import jakarta.inject.Singleton + +@Singleton +class PersonService { + fun findByName(name: String): Person { + return Person(name) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt new file mode 100644 index 00000000000..afab5186fd3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryNettyServer.kt @@ -0,0 +1,56 @@ +package io.micronaut.docs.http.server.secondary + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Context +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.core.util.StringUtils +import io.micronaut.discovery.ServiceInstanceList +import io.micronaut.discovery.StaticServiceInstanceList +import io.micronaut.http.server.netty.NettyEmbeddedServer +import io.micronaut.http.server.netty.NettyEmbeddedServerFactory +import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration +import io.micronaut.http.ssl.ServerSslConfiguration +import jakarta.inject.Named +// end::imports[] + +@Requires(property = "secondary.enabled", value = StringUtils.TRUE) +// tag::class[] +@Factory +class SecondaryNettyServer { + companion object { + const val SERVER_ID = "another" // <1> + } + + @Named(SERVER_ID) + @Context + @Bean(preDestroy = "close") // <2> + @Requires(beans = [Environment::class]) + fun nettyEmbeddedServer( + serverFactory: NettyEmbeddedServerFactory // <3> + ) : NettyEmbeddedServer { + val configuration = NettyHttpServerConfiguration() // <4> + val sslConfiguration = ServerSslConfiguration() // <5> + + sslConfiguration.setBuildSelfSigned(true) + sslConfiguration.isEnabled = true + sslConfiguration.port = -1 // random port + + // configure server programmatically + val embeddedServer = serverFactory.build(configuration, sslConfiguration) // <6> + embeddedServer.start() // <7> + return embeddedServer // <8> + } + + @Bean + fun serviceInstanceList( // <9> + @Named(SERVER_ID) nettyEmbeddedServer: NettyEmbeddedServer + ): ServiceInstanceList { + return StaticServiceInstanceList( + SERVER_ID, setOf(nettyEmbeddedServer.uri) + ) + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt new file mode 100644 index 00000000000..d6df1a2582a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/secondary/SecondaryServerTest.kt @@ -0,0 +1,44 @@ +package io.micronaut.docs.http.server.secondary + +import io.micronaut.context.annotation.Property +import io.micronaut.core.util.StringUtils +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import jakarta.inject.Named +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest +@Property(name = "secondary.enabled", value = StringUtils.TRUE) +@Property(name = "micronaut.http.client.ssl.insecure-trust-all-certificates", value = StringUtils.TRUE) +class SecondaryServerTest { + // tag::inject[] + @Inject + @field:Client(path = "/", id = SecondaryNettyServer.SERVER_ID) + lateinit var httpClient : HttpClient // <1> + + @Inject + @field:Named(SecondaryNettyServer.SERVER_ID) + lateinit var embeddedServer : EmbeddedServer // <2> + // end::inject[] + + @Test + fun testCallSecondaryServer() { + val result = httpClient.toBlocking().retrieve("/test/secondary/server") + Assertions.assertTrue(result.endsWith(embeddedServer.port.toString())) + } +} + +@Controller("/test/secondary/server") +class TestController { + @Get + fun hello(request: HttpRequest<*>): String { + return "Hello from: " + request.serverAddress.port + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt new file mode 100644 index 00000000000..bdf6c5c89e4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamController.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.http.server.stream + +import io.micronaut.core.io.IOUtils +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.scheduling.annotation.ExecuteOn +import io.micronaut.scheduling.TaskExecutors +import java.io.* +import java.nio.charset.StandardCharsets + +@Controller("/stream") +class StreamController { + + // tag::write[] + @Get(value = "/write", produces = [MediaType.TEXT_PLAIN]) + fun write(): InputStream { + val bytes = "test".toByteArray(StandardCharsets.UTF_8) + return ByteArrayInputStream(bytes) // <1> + } + // end::write[] + + // tag::read[] + @Post(value = "/read", processes = [MediaType.TEXT_PLAIN]) + @ExecuteOn(TaskExecutors.IO) // <1> + fun read(@Body inputStream: InputStream): String { // <2> + return IOUtils.readText(BufferedReader(InputStreamReader(inputStream))) // <3> + } + // end::read[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt new file mode 100644 index 00000000000..b0db1830b48 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/http/server/stream/StreamControllerSpec.kt @@ -0,0 +1,60 @@ +package io.micronaut.docs.http.server.stream + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class StreamControllerSpec { + + lateinit var ctx: ApplicationContext + lateinit var client: HttpClient + + @BeforeEach + fun setup() { + val server = ApplicationContext.run( + EmbeddedServer::class.java, + mapOf( + "myapp.updatedAt" to mapOf( // <1> + "day" to 28, + "month" to 10, + "year" to 1982 + ) + ) + ) + ctx = server.applicationContext + client = ctx.createBean(HttpClient::class.java, server.url) + } + + @AfterEach + fun teardown() { + ctx.close() + } + + + @Test + fun testReceivingAStream() { + val response: String = client.toBlocking().retrieve( + HttpRequest.GET("/stream/write"), + String::class.java + ) + + Assertions.assertEquals("test", response) + } + + @Test + fun testReturningAStream() { + val body = "My body" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/stream/read", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + Assertions.assertEquals(body, response) + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt new file mode 100644 index 00000000000..7e439d5b6c2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BindHttpClientExceptionBodySpec.kt @@ -0,0 +1,70 @@ +package io.micronaut.docs.httpclientexceptionbody + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class BindHttpClientExceptionBodySpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run( + EmbeddedServer::class.java, + mapOf( + "spec.name" to BindHttpClientExceptionBodySpec::class.java.simpleName, + "spec.lang" to "java" + ) + ) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + //tag::test[] + "after an httpclient exception the response body can be bound to a POJO" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"), + Argument.of(Book::class.java), // <1> + Argument.of(CustomError::class.java)) // <2> + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.UNAUTHORIZED + } + } + //end::test[] + + "exception binding error response" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"), + Argument.of(Book::class.java), // <1> + Argument.of(OtherError::class.java)) // <2> + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.UNAUTHORIZED + + val jsonError = e.response.getBody(OtherError::class.java) + + jsonError shouldNotBe null + jsonError.isPresent shouldNotBe true + } + } + + "verify bind error is thrown" { + try { + client.toBlocking().exchange(HttpRequest.GET("/books/1491950358"), + Argument.of(Book::class.java), + Argument.of(CustomError::class.java)) + } catch (e: HttpClientResponseException) { + e.response.status shouldBe HttpStatus.OK + e.message!!.startsWith("Error decoding HTTP response body") shouldBe true + e.message!!.contains("cannot deserialize from Object value") shouldBe true // the jackson error + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt new file mode 100644 index 00000000000..4ea2424c544 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/Book.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class Book internal constructor(var isbn: String?, var title: String?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt new file mode 100644 index 00000000000..40475903960 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/BooksController.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get + +@Requires(property = "spec.name", value = "BindHttpClientExceptionBodySpec") +//tag::clazz[] +@Controller("/books") +class BooksController { + + @Get("/{isbn}") + fun find(isbn: String): HttpResponse<*> { + if (isbn == "1680502395") { + val m = mapOf( + "status" to 401, + "error" to "Unauthorized", + "message" to "No message available", + "path" to "/books/$isbn" + ) + return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(m) + } + + return HttpResponse.ok(Book("1491950358", "Building Microservices")) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt new file mode 100644 index 00000000000..138e365b96f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/CustomError.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class CustomError internal constructor(var status: Int?, var error: String?, var message: String?, var path: String?) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt new file mode 100644 index 00000000000..4e20ed52ed5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/httpclientexceptionbody/OtherError.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.httpclientexceptionbody + +import groovy.transform.CompileStatic + +@CompileStatic +class OtherError internal constructor(var status: Int?, var error: String?, var message: String?, var path: String?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt new file mode 100644 index 00000000000..33ef72378f5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/I18nSpec.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.i18n + +import io.micronaut.context.MessageSource +import io.micronaut.context.i18n.ResourceBundleMessageSource +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.util.* + +@MicronautTest(startApplication = false) +class I18nTest { + @Inject + lateinit var messageSource: MessageSource + + @Test + fun itIsPossibleToCreateAMessageSourceFromResourceBundle() { + //tag::test[] + Assertions.assertEquals("Hola", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale("es"))).get()) + Assertions.assertEquals("Hello", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale.ENGLISH)).get()) + //end::test[] + + Assertions.assertEquals("Hola", messageSource.getMessage("hello", Locale("es")).get()) + Assertions.assertEquals("Hello", messageSource.getMessage("hello", Locale.ENGLISH).get()) + + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale("es"), "Sergio").isPresent) + Assertions.assertEquals("Hola Sergio", messageSource.getMessage("hello.name", Locale("es"), "Sergio").get()) + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale.ENGLISH, "Sergio").isPresent) + Assertions.assertEquals("Hello Sergio", messageSource.getMessage("hello.name", Locale.ENGLISH, "Sergio").get()) + + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale("es"), mapOf(Pair("0", "Sergio"))).isPresent) + Assertions.assertEquals( + "Hola Sergio", + messageSource.getMessage("hello.name", Locale("es"), mapOf(Pair("0", "Sergio"))).get() + ) + Assertions.assertTrue(messageSource.getMessage("hello.name", Locale.ENGLISH, mapOf(Pair("0", "Sergio"))).isPresent) + Assertions.assertEquals( + "Hello Sergio", + messageSource.getMessage("hello.name", Locale.ENGLISH, mapOf(Pair("0", "Sergio"))).get() + ) + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt new file mode 100644 index 00000000000..cbdb3a6ade0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/i18n/MessageSourceFactory.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.i18n + +//tag::clazz[] +import io.micronaut.context.MessageSource +import io.micronaut.context.annotation.Factory +import io.micronaut.context.i18n.ResourceBundleMessageSource +import jakarta.inject.Singleton + +@Factory +internal class MessageSourceFactory { + @Singleton + fun createMessageSource(): MessageSource = ResourceBundleMessageSource("io.micronaut.docs.i18n.messages") +} +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt new file mode 100644 index 00000000000..8587487a356 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/AnnotationInheritanceSpec.kt @@ -0,0 +1,20 @@ +package io.micronaut.docs.inject.anninheritance + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.AnnotationUtil +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AnnotationInheritanceSpec { + @Test + fun testAnnotationInheritance() { + val config = mapOf("datasource.url" to "jdbc://someurl") + ApplicationContext.run(config).use { context -> + val beanDefinition = context.getBeanDefinition(BookRepository::class.java) + val name = beanDefinition.stringValue(AnnotationUtil.NAMED).orElse(null) + assertEquals("bookRepository", name) + assertTrue(beanDefinition.isSingleton) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt new file mode 100644 index 00000000000..b265f6f8d36 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BaseSqlRepository.kt @@ -0,0 +1,6 @@ +package io.micronaut.docs.inject.anninheritance + +// tag::class[] +@SqlRepository +abstract class BaseSqlRepository +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt new file mode 100644 index 00000000000..c8c2203fc55 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/BookRepository.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.anninheritance + +//tag::imports[] +import jakarta.inject.Named +import javax.sql.DataSource +//end::imports[] + +//tag::class[] +@Named("bookRepository") +class BookRepository(private val dataSource: DataSource) : BaseSqlRepository() +//end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt new file mode 100644 index 00000000000..e707b5e5d54 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/anninheritance/SqlRepository.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.inject.anninheritance + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import jakarta.inject.Named +import jakarta.inject.Singleton +import java.lang.annotation.Inherited +//end::imports[] + +//tag::class[] +@Inherited // <1> +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Requires(property = "datasource.url") // <2> +@Named // <3> +@Singleton // <4> +annotation class SqlRepository( + val value: String = "" +) +//end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt new file mode 100644 index 00000000000..6262df0a147 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/CylinderProvider.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +interface CylinderProvider { + val cylinders: Int +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt new file mode 100644 index 00000000000..3df937f7e49 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Engine.kt @@ -0,0 +1,14 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + get() = cylinderProvider.cylinders + + fun start(): String { + return "Starting ${cylinderProvider.javaClass.simpleName}" + } + + val cylinderProvider: T +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt new file mode 100644 index 00000000000..2902c797ecc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +class V6 : CylinderProvider { + override val cylinders: Int = 6 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt new file mode 100644 index 00000000000..6c8fdfd2169 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V6Engine.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <1> + override val cylinderProvider: V6 + get() = V6() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt new file mode 100644 index 00000000000..898b6bc9df0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8.kt @@ -0,0 +1,7 @@ +package io.micronaut.docs.inject.generics + +// tag::class[] +class V8 : CylinderProvider { + override val cylinders: Int = 8 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt new file mode 100644 index 00000000000..024ce948867 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/V8Engine.kt @@ -0,0 +1,11 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { // <1> + override val cylinderProvider: V8 + get() = V8() +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt new file mode 100644 index 00000000000..78eab26203c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/Vehicle.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.generics + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::constructor[] +@Singleton +class Vehicle(val engine: Engine) { +// end::constructor[] + + @Inject + lateinit var v6Engines: List> + + @set:Inject + lateinit var anotherV8: Engine + + + fun start(): String { + return engine.start() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt new file mode 100644 index 00000000000..a39561b1c5a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/generics/VehicleSpec.kt @@ -0,0 +1,15 @@ +package io.micronaut.docs.inject.generics + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@MicronautTest +class VehicleSpec(private val vehicle: Vehicle) { + @Test + fun testStartVehicle() { + assertEquals("Starting V8", vehicle.start()) + assertEquals(listOf(6), vehicle.v6Engines + .map { it.cylinders }) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt new file mode 100644 index 00000000000..11dd1d065ce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Engine.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +// tag::class[] +interface Engine { + // <1> + val cylinders: Int + + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt new file mode 100644 index 00000000000..48c476e18bc --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/V8Engine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton// <2> +class V8Engine : Engine { + + override var cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt new file mode 100644 index 00000000000..bbaa20964ea --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.intro + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle(private val engine: Engine) { // <3> + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt new file mode 100644 index 00000000000..b91de60e55a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/intro/VehicleSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.intro + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt new file mode 100644 index 00000000000..f73e2d99308 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt new file mode 100644 index 00000000000..4f3a23973c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V6Engine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <2> + + override var cylinders: Int = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt new file mode 100644 index 00000000000..95c922c63dd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/V8Engine.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { + + override var cylinders: Int = 8 + + override fun start(): String { + return "Starting V8" + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt new file mode 100644 index 00000000000..ad633b78af8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/Vehicle.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.inject.qualifiers.named + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle @Inject +constructor(@param:Named("v8") private val engine: Engine) { // <4> + + fun start(): String { + return engine.start() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt new file mode 100644 index 00000000000..8af2e3eeec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/qualifiers/named/VehicleSpec.kt @@ -0,0 +1,21 @@ +package io.micronaut.docs.inject.qualifiers.named + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + assertEquals("Starting V8", vehicle.start()) + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt new file mode 100644 index 00000000000..b28aa680d82 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/scope/RefreshEventSpec.kt @@ -0,0 +1,108 @@ +package io.micronaut.docs.inject.scope + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.context.scope.Refreshable +import io.micronaut.runtime.context.scope.refresh.RefreshEvent +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.Assert.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.text.SimpleDateFormat +import java.util.* +import jakarta.annotation.PostConstruct +import jakarta.inject.Inject + +class RefreshEventSpec { + + lateinit var embeddedServer: EmbeddedServer + lateinit var client: HttpClient + + @BeforeEach + fun setup() { + embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to RefreshEventSpec::class.simpleName), Environment.TEST) + client = HttpClient.create(embeddedServer.url) + } + + @AfterEach + fun teardown() { + client.close() + embeddedServer.close() + } + + @Test + fun publishingARefreshEventDestroysBeanWithRefreshableScope() { + val firstResponse = fetchForecast() + + assertTrue(firstResponse.contains("{\"forecast\":\"Scattered Clouds")) + + val secondResponse = fetchForecast() + + assertEquals(firstResponse, secondResponse) + + val response = evictForecast() + + assertEquals( + // tag::evictResponse[] + "{\"msg\":\"OK\"}", response)// end::evictResponse[] + + val thirdResponse = fetchForecast() + + assertNotEquals(thirdResponse, secondResponse) + assertTrue(thirdResponse.contains("\"forecast\":\"Scattered Clouds")) + } + + fun fetchForecast(): String { + return client.toBlocking().retrieve("/weather/forecast") + } + + fun evictForecast(): String { + return client.toBlocking().retrieve(HttpRequest.POST( + "/weather/evict", + emptyMap() + )) + } + + //tag::weatherService[] + @Refreshable // <1> + open class WeatherService { + private var forecast: String? = null + + @PostConstruct + open fun init() { + forecast = "Scattered Clouds " + SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(Date())// <2> + } + + open fun latestForecast(): String? { + return forecast + } + } + //end::weatherService[] + + @Requires(property = "spec.name", value = "RefreshEventSpec") + @Controller("/weather") + open class WeatherController(@Inject private val weatherService: WeatherService, @Inject private val applicationContext: ApplicationContext) { + + @Get(value = "/forecast") + fun index(): MutableHttpResponse>? { + return HttpResponse.ok(mapOf("forecast" to weatherService.latestForecast())) + } + + @Post("/evict") + fun evict(): HttpResponse> { + //tag::publishEvent[] + applicationContext.publishEvent(RefreshEvent()) + //end::publishEvent[] + return HttpResponse.ok(mapOf("msg" to "OK")) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt new file mode 100644 index 00000000000..956fd1b30a7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/Engine.kt @@ -0,0 +1,8 @@ +package io.micronaut.docs.inject.typed + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// tag::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt new file mode 100644 index 00000000000..8ce4b4bc6e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/EngineSpec.kt @@ -0,0 +1,27 @@ +package io.micronaut.docs.inject.typed + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import jakarta.inject.Inject + +// tag::class[] +@MicronautTest +class EngineSpec { + @Inject + lateinit var beanContext: BeanContext + + @Test + fun testEngine() { + assertThrows(NoSuchBeanException::class.java) { + beanContext.getBean(V8Engine::class.java) // <1> + } + + val engine = beanContext.getBean(Engine::class.java) // <2> + assertTrue(engine is V8Engine) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt new file mode 100644 index 00000000000..9387d9bf9fa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.inject.typed + +import io.micronaut.context.annotation.Bean +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Bean(typed = [Engine::class]) // <1> +class V8Engine : Engine { // <2> + override fun start(): String { + return "Starting V8" + } + + override val cylinders: Int = 8 +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt new file mode 100644 index 00000000000..95d15451e95 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/CrankShaft.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class CrankShaft +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt new file mode 100644 index 00000000000..00b46535ff6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Cylinders.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Cylinders(val value: Int = 8) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt new file mode 100644 index 00000000000..58b489e2ae7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Engine.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +interface Engine { + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt new file mode 100644 index 00000000000..eeae0510170 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/EngineFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Prototype +import io.micronaut.inject.InjectionPoint + +/** + * @author Graeme Rocher + * @since 1.0 + */ +// tag::class[] +@Factory +internal class EngineFactory { + + @Prototype + fun v8Engine(injectionPoint: InjectionPoint<*>, crankShaft: CrankShaft): Engine { // <1> + val cylinders = injectionPoint + .annotationMetadata + .intValue(Cylinders::class.java).orElse(8) // <2> + return when (cylinders) { // <3> + 6 -> V6Engine(crankShaft) + 8 -> V8Engine(crankShaft) + else -> throw IllegalArgumentException("Unsupported number of cylinders specified: $cylinders") + } + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt new file mode 100644 index 00000000000..c48b106d4af --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V6Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +internal class V6Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt new file mode 100644 index 00000000000..fd475372a72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/V8Engine.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +// tag::class[] +internal class V8Engine(private val crankShaft: CrankShaft) : Engine { + private val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt new file mode 100644 index 00000000000..74e9cbe471b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/Vehicle.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.injectionpoint + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +internal class Vehicle(@param:Cylinders(6) private val engine: Engine) { + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt new file mode 100644 index 00000000000..0755dda439c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/injectionpoint/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.injectionpoint + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + + +internal class VehicleSpec { + + @Test + fun testStartVehicle() { + // tag::start[] + BeanContext.run().use { + val vehicle = it.getBean(Vehicle::class.java) + println(vehicle.start()) + + + Assertions.assertEquals("Starting V6", vehicle.start()) + } + // end::start[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt new file mode 100644 index 00000000000..b8f663e8039 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Business.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +import javax.annotation.concurrent.Immutable + +@Introspected +@Immutable +class Business private constructor(val name: String) { + companion object { + + @Creator // <1> + fun forName(name: String): Business { + return Business(name) + } + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt new file mode 100644 index 00000000000..33bec029b79 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/IntrospectionSpec.kt @@ -0,0 +1,68 @@ +package io.micronaut.docs.ioc.beans + +import io.micronaut.core.beans.BeanIntrospection +import io.micronaut.core.beans.BeanProperty +import io.micronaut.core.beans.BeanWrapper +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class IntrospectionSpec { + + @Test + fun testRetrieveInspection() { + + // tag::usage[] + val introspection = BeanIntrospection.getIntrospection(Person::class.java) // <1> + val person : Person = introspection.instantiate("John") // <2> + print("Hello ${person.name}") + + val property : BeanProperty = introspection.getRequiredProperty("name", String::class.java) // <3> + property.set(person, "Fred") // <4> + val name = property.get(person) // <5> + print("Hello ${person.name}") + // end::usage[] + + assertEquals("Fred", name) + } + + @Test + fun testBeanWrapper() { + // tag::wrapper[] + val wrapper = BeanWrapper.getWrapper(Person("Fred")) // <1> + + wrapper.setProperty("age", "20") // <2> + val newAge = wrapper.getRequiredProperty("age", Int::class.java) // <3> + + println("Person's age now $newAge") + // end::wrapper[] + assertEquals(20, newAge) + } + + @Test + fun testNullable() { + val introspection = BeanIntrospection.getIntrospection(Manufacturer::class.java) + val manufacturer: Manufacturer = introspection.instantiate(null, "John") + + val property : BeanProperty = introspection.getRequiredProperty("name", String::class.java) + property.set(manufacturer, "Jane") + val name = property.get(manufacturer) + + assertEquals("Jane", name) + } + + @Test + fun testVehicle() { + val introspection = BeanIntrospection.getIntrospection(Vehicle::class.java) + val vehicle = introspection.instantiate("Subaru", "WRX", 2) + assertEquals("Subaru", vehicle.make) + assertEquals("WRX", vehicle.model) + assertEquals(2, vehicle.axles) + } + + @Test + fun testBusiness() { + val introspection = BeanIntrospection.getIntrospection(Business::class.java) + val business = introspection.instantiate("Apple") + assertEquals("Apple", business.name) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt new file mode 100644 index 00000000000..e6b2f5484ab --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Manufacturer.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +data class Manufacturer( + var id: Long?, + var name: String +) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt new file mode 100644 index 00000000000..89248ffb793 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Person.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +// end::imports[] + +// tag::class[] +@Introspected +data class Person(var name : String) { + var age : Int = 18 +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt new file mode 100644 index 00000000000..49ff64f6b48 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/PersonConfiguration.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Introspected + +@Introspected(classes = [Person::class]) +class PersonConfiguration +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt new file mode 100644 index 00000000000..a67c4e2d512 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/beans/Vehicle.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.beans + +// tag::class[] +import io.micronaut.core.annotation.Creator +import io.micronaut.core.annotation.Introspected + +import javax.annotation.concurrent.Immutable + +@Introspected +@Immutable +class Vehicle @Creator constructor(val make: String, val model: String, val axles: Int) { // <1> + + constructor(make: String, model: String) : this(make, model, 2) {} +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt new file mode 100644 index 00000000000..6af706eebe1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.scopes + +class Car(val brand: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt new file mode 100644 index 00000000000..564abce830a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/scopes/Driver.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.scopes + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Requires(classes = [Car::class]) // <1> +@Singleton // <2> +@MustBeDocumented +@Retention(RUNTIME) +annotation class Driver +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt new file mode 100644 index 00000000000..b869cf98c45 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/Person.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation + +// tag::class[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +@Introspected +data class Person( + @field:NotBlank var name: String, + @field:Min(18) var age: Int +) +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt new file mode 100644 index 00000000000..dd1e2e9c230 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonService.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation + +// tag::imports[] +import jakarta.inject.Singleton +import javax.validation.constraints.NotBlank +// end::imports[] + +// tag::class[] +@Singleton +open class PersonService { + open fun sayHello(@NotBlank name: String) { + println("Hello $name") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt new file mode 100644 index 00000000000..c248f7c2949 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/PersonServiceSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.ioc.validation + +// tag::imports[] +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException +// end::imports[] + +// tag::test[] +@MicronautTest +class PersonServiceSpec { + + @Inject + lateinit var personService: PersonService + + @Test + fun testThatNameIsValidated() { + val exception = assertThrows(ConstraintViolationException::class.java) { + personService.sayHello("") // <1> + } + + assertEquals("sayHello.name: must not be blank", exception.message) // <2> + } +} +// end::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt new file mode 100644 index 00000000000..f166300ac6b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPattern.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import javax.validation.Constraint +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Retention(RUNTIME) +@Constraint(validatedBy = []) // <1> +annotation class DurationPattern( + val message: String = "invalid duration ({validatedValue})" // <2> +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt new file mode 100644 index 00000000000..b24c226e1c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidator.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import io.micronaut.core.annotation.AnnotationValue +import io.micronaut.validation.validator.constraints.ConstraintValidator +import io.micronaut.validation.validator.constraints.ConstraintValidatorContext +// end::imports[] + +// tag::class[] +class DurationPatternValidator : ConstraintValidator { + override fun isValid( + value: CharSequence?, + annotationMetadata: AnnotationValue, + context: ConstraintValidatorContext): Boolean { + return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex()) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt new file mode 100644 index 00000000000..f8a6b0e2fc1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/DurationPatternValidatorSpec.kt @@ -0,0 +1,26 @@ +package io.micronaut.docs.ioc.validation.custom + +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException + +@MicronautTest +internal class DurationPatternValidatorSpec { + + // tag::test[] + @Inject + lateinit var holidayService: HolidayService + + @Test + fun testCustomValidator() { + val exception = assertThrows(ConstraintViolationException::class.java) { + holidayService.startHoliday("Fred", "junk") // <1> + } + + assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) // <2> + } + // end::test[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt new file mode 100644 index 00000000000..8e70b21d637 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/HolidayService.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +import java.time.Duration +import jakarta.inject.Singleton +import javax.validation.constraints.NotBlank + +// tag::class[] +@Singleton +open class HolidayService { + + open fun startHoliday(@NotBlank person: String, + @DurationPattern duration: String): String { + val d = Duration.parse(duration) + return "Person $person is off on holiday for ${d.toMinutes()} minutes" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt new file mode 100644 index 00000000000..799fe4d6bce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/MyValidatorFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import io.micronaut.context.annotation.Factory +import io.micronaut.validation.validator.constraints.ConstraintValidator +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +class MyValidatorFactory { + + @Singleton + fun durationPatternValidator() : ConstraintValidator { + return ConstraintValidator { value, annotation, context -> + context.messageTemplate("invalid duration ({validatedValue}), additional custom message") // <1> + value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex()) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt new file mode 100644 index 00000000000..903b4de79e2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/custom/TimeOff.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.custom + +// tag::imports[] +import kotlin.annotation.AnnotationRetention.RUNTIME +// end::imports[] + +// tag::class[] +@Retention(RUNTIME) +annotation class TimeOff( + @DurationPattern val duration: String +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt new file mode 100644 index 00000000000..9e463115a99 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.ioc.validation.pojo + +// tag::imports[] +import io.micronaut.docs.ioc.validation.Person + +import jakarta.inject.Singleton +import javax.validation.Valid + +// end::imports[] + +// tag::class[] +@Singleton +open class PersonService { + open fun sayHello(@Valid person: Person) { + println("Hello ${person.name}") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt new file mode 100644 index 00000000000..7e9932adc1b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/ioc/validation/pojo/PersonServiceSpec.kt @@ -0,0 +1,46 @@ +package io.micronaut.docs.ioc.validation.pojo + +// tag::imports[] +import io.micronaut.docs.ioc.validation.Person +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.micronaut.validation.validator.Validator +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import javax.validation.ConstraintViolationException +// end::imports[] + +// tag::test[] +@MicronautTest +class PersonServiceSpec { + + // tag::validator[] + @Inject + lateinit var validator: Validator + + @Test + fun testThatPersonIsValidWithValidator() { + val person = Person("", 10) + val constraintViolations = validator.validate(person) // <1> + + assertEquals(2, constraintViolations.size) // <2> + } + // end::validator[] + + // tag::validate-service[] + @Inject + lateinit var personService: PersonService + + @Test + fun testThatPersonIsValid() { + val person = Person("", 10) + val exception = assertThrows(ConstraintViolationException::class.java) { + personService.sayHello(person) // <1> + } + + assertEquals(2, exception.constraintViolations.size) // <2> + } + // end::validate-service[] +} +// end::test[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt new file mode 100644 index 00000000000..3a9e0f19e34 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Connection.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import java.util.concurrent.atomic.AtomicBoolean + +class Connection { + + internal var stopped = AtomicBoolean(false) + + fun stop() { // <2> + stopped.compareAndSet(false, true) + } + +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt new file mode 100644 index 00000000000..82056d6053c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/ConnectionFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import jakarta.inject.Singleton + +@Factory +class ConnectionFactory { + + @Bean(preDestroy = "stop") // <1> + @Singleton + fun connection(): Connection { + return Connection() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt new file mode 100644 index 00000000000..b02e513a17c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +interface Engine { // <1> + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt new file mode 100644 index 00000000000..e90338e44e5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBean.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::class[] +import jakarta.annotation.PreDestroy // <1> +import jakarta.inject.Singleton +import java.util.concurrent.atomic.AtomicBoolean + +@Singleton +class PreDestroyBean : AutoCloseable { + + internal var stopped = AtomicBoolean(false) + + @PreDestroy // <2> + @Throws(Exception::class) + override fun close() { + stopped.compareAndSet(false, true) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt new file mode 100644 index 00000000000..01e14f92994 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/PreDestroyBeanSpec.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.lifecycle + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import org.junit.Test + +import org.junit.Assert.assertTrue + +class PreDestroyBeanSpec: StringSpec() { + + init { + "test bean closing on context close" { + // tag::start[] + val ctx = BeanContext.run() + val preDestroyBean = ctx.getBean(PreDestroyBean::class.java) + val connection = ctx.getBean(Connection::class.java) + ctx.stop() + // end::start[] + + preDestroyBean.stopped.get() shouldBe true + connection.stopped.get() shouldBe true + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt new file mode 100644 index 00000000000..02898237586 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/V8Engine.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +// tag::imports[] +import jakarta.annotation.PostConstruct +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class V8Engine : Engine { + + override val cylinders = 8 + + var initialized = false + private set // <2> + + override fun start(): String { + check(initialized) { "Engine not initialized!" } + + return "Starting V8" + } + + @PostConstruct // <3> + fun initialize() { + initialized = true + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt new file mode 100644 index 00000000000..9488049c5fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/Vehicle.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.lifecycle + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle internal constructor(internal val engine: Engine)// <3> +{ + + fun start(): String { + return engine.start() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt new file mode 100644 index 00000000000..44aae406c37 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/lifecycle/VehicleSpec.kt @@ -0,0 +1,26 @@ +package io.micronaut.docs.lifecycle + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext + +class VehicleSpec: StringSpec() { + + init { + "test start vehicle" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context + .getBean(Vehicle::class.java) + + println(vehicle.start()) + // end::start[] + + vehicle.engine.javaClass shouldBe V8Engine::class.java + (vehicle.engine as V8Engine).initialized shouldBe true + + context.close() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt new file mode 100644 index 00000000000..7fdeb32a624 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyClientCustomizer.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.netty + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.BeanCreatedEvent +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.http.client.netty.NettyClientCustomizer +import io.micronaut.http.client.netty.NettyClientCustomizer.ChannelRole +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.netty.channel.Channel +import jakarta.inject.Singleton +import org.zalando.logbook.Logbook +import org.zalando.logbook.netty.LogbookClientHandler +// end::imports[] + +// tag::class[] +@Requires(beans = [Logbook::class]) +@Singleton +class LogbookNettyClientCustomizer(private val logbook: Logbook) : + BeanCreatedEventListener { // <1> + + override fun onCreated(event: BeanCreatedEvent): NettyClientCustomizer.Registry { + val registry = event.bean + registry.register(Customizer(null)) // <2> + return registry + } + + private inner class Customizer constructor(private val channel: Channel?) : + NettyClientCustomizer { // <3> + + override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // <4> + + override fun onRequestPipelineBuilt() { + channel!!.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + "logbook", + LogbookClientHandler(logbook) + ) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt new file mode 100644 index 00000000000..5ac765c1f24 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/netty/LogbookNettyServerCustomizer.kt @@ -0,0 +1,42 @@ +package io.micronaut.docs.netty + +// tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.context.event.BeanCreatedEvent +import io.micronaut.context.event.BeanCreatedEventListener +import io.micronaut.http.netty.channel.ChannelPipelineCustomizer +import io.micronaut.http.server.netty.NettyServerCustomizer +import io.micronaut.http.server.netty.NettyServerCustomizer.ChannelRole +import io.netty.channel.Channel +import jakarta.inject.Singleton +import org.zalando.logbook.Logbook +import org.zalando.logbook.netty.LogbookServerHandler +// end::imports[] + +// tag::class[] +@Requires(beans = [Logbook::class]) +@Singleton +class LogbookNettyServerCustomizer(private val logbook: Logbook) : + BeanCreatedEventListener { // <1> + + override fun onCreated(event: BeanCreatedEvent): NettyServerCustomizer.Registry { + val registry = event.bean + registry.register(Customizer(null)) // <2> + return registry + } + + private inner class Customizer constructor(private val channel: Channel?) : + NettyServerCustomizer { // <3> + + override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // <4> + + override fun onStreamPipelineBuilt() { + channel!!.pipeline().addBefore( // <5> + ChannelPipelineCustomizer.HANDLER_HTTP_STREAM, + "logbook", + LogbookServerHandler(logbook) + ) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt new file mode 100644 index 00000000000..b0126b22649 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Engine.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +// tag::class[] +interface Engine { // <1> + val cylinders : Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt new file mode 100644 index 00000000000..fe65ffddf60 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V6Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V6Engine : Engine { // <2> + override val cylinders = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt new file mode 100644 index 00000000000..5fbdb05a7f3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +// tag::imports[] +import jakarta.inject.Qualifier +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy.RUNTIME +// end::imports[] +// tag::class[] +@Qualifier +@Retention(RUNTIME) +annotation class V8 +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt new file mode 100644 index 00000000000..cf6aa089579 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/V8Engine.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class V8Engine : Engine { // <2> + override val cylinders = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt new file mode 100644 index 00000000000..692ae3c53ce --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/Vehicle.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.annotation + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +class Vehicle // tag::constructor[] +@Inject constructor(@V8 val engine: Engine) { + + // end::constructor[] + fun start(): String { + return engine.start() // <5> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt new file mode 100644 index 00000000000..bcfade7bbb4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotation/VehicleSpec.kt @@ -0,0 +1,22 @@ +package io.micronaut.docs.qualifiers.annotation + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext + + +class VehicleSpec : StringSpec({ + + "test vehicle start uses v8" { + // tag::start[] + val context = BeanContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + + vehicle.start().shouldBe("Starting V8") + + context.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt new file mode 100644 index 00000000000..5008b029f20 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Cylinders.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.annotationmember + +// tag::imports[] +import io.micronaut.context.annotation.NonBinding +import jakarta.inject.Qualifier +import kotlin.annotation.Retention +// end::imports[] + +// tag::class[] +@Qualifier // <1> +@Retention(AnnotationRetention.RUNTIME) +annotation class Cylinders( + val value: Int, + @get:NonBinding // <2> + val description: String = "" +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt new file mode 100644 index 00000000000..a9fa5c6e5b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Engine.kt @@ -0,0 +1,8 @@ +package io.micronaut.docs.qualifiers.annotationmember + +// tag::class[] +interface Engine { + val cylinders: Int + fun start(): String +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt new file mode 100644 index 00000000000..166e327d919 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V6Engine.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Cylinders(value = 6, description = "6-cylinder V6 engine") // <1> +class V6Engine : Engine { // <2> + // <2> + override val cylinders: Int + get() = 6 + + override fun start(): String { + return "Starting V6" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt new file mode 100644 index 00000000000..fffaa525b53 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/V8Engine.kt @@ -0,0 +1,16 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + +// tag::class[] +@Singleton +@Cylinders(value = 8, description = "8-cylinder V8 engine") // <1> +class V8Engine : Engine { // <2> + override val cylinders: Int + get() = 8 + + override fun start(): String { + return "Starting V8" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt new file mode 100644 index 00000000000..ea6ebf3131e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/Vehicle.kt @@ -0,0 +1,13 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import jakarta.inject.Singleton + + +// tag::constructor[] +@Singleton +class Vehicle(@param:Cylinders(8) val engine: Engine) { + fun start(): String { + return engine.start() + } +} +// end::constructor[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt new file mode 100644 index 00000000000..bdfa5c9a1c4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/annotationmember/VehicleSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.docs.qualifiers.annotationmember + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class VehicleSpec { + @Test + fun testStartVehicle() { + // tag::start[] + val context = ApplicationContext.run() + val vehicle = context.getBean(Vehicle::class.java) + println(vehicle.start()) + // end::start[] + Assertions.assertEquals("Starting V8", vehicle.start()) + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt new file mode 100644 index 00000000000..8723789d10c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/Vehicle.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.qualifiers.any + +import io.micronaut.docs.qualifiers.annotationmember.Engine +// tag::imports[] +import io.micronaut.context.BeanProvider +import io.micronaut.context.annotation.Any +import jakarta.inject.Singleton +// end::imports[] + +// tag::clazz[] +@Singleton +class Vehicle(@param:Any val engineProvider: BeanProvider) { // <1> + fun start() { + engineProvider.ifPresent { it.start() } // <2> + } + // tag::startAll[] + fun startAll() { + if (engineProvider.isPresent) { // <1> + engineProvider.forEach { it.start() } // <2> + } + } // end::startAll[] +// tag::clazz[] +} +// end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt new file mode 100644 index 00000000000..2ba960b1bec --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/any/VehicleSpec.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.qualifiers.any + +import io.micronaut.docs.qualifiers.annotationmember.Engine +// tag::imports[] +import io.micronaut.context.annotation.Any +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +@MicronautTest +class VehicleSpec { + // tag::any[] + @Inject + @field:Any + lateinit var engine: Engine + // end::any[] + + @Test + fun testEngine() { + assertNotNull(engine) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt new file mode 100644 index 00000000000..00cc491ce80 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/CustomResponseStrategy.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import io.micronaut.context.annotation.Replaces +import jakarta.inject.Singleton + +@Singleton +@Replaces(ResponseStrategy::class) +class CustomResponseStrategy : ResponseStrategy +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt new file mode 100644 index 00000000000..3d9b8f88a84 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultImplementationSpec.kt @@ -0,0 +1,17 @@ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.micronaut.context.BeanContext + +class DefaultImplementationSpec : StringSpec({ + + "test the default implementation is replaced" { + val ctx = BeanContext.run() + val responseStrategy = ctx.getBean(ResponseStrategy::class.java) + + responseStrategy.shouldBeInstanceOf() + + ctx.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt new file mode 100644 index 00000000000..f601d662295 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/DefaultResponseStrategy.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import jakarta.inject.Singleton + +@Singleton +internal class DefaultResponseStrategy : ResponseStrategy +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt new file mode 100644 index 00000000000..f02e3c0ea72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/qualifiers/replaces/defaultimpl/ResponseStrategy.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.qualifiers.replaces.defaultimpl + +//tag::clazz[] +import io.micronaut.context.annotation.DefaultImplementation + +@DefaultImplementation(DefaultResponseStrategy::class) +interface ResponseStrategy +//end::clazz[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt new file mode 100644 index 00000000000..49afc400023 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/reactor/ReactorContextPropagationSpec.kt @@ -0,0 +1,156 @@ +package io.micronaut.docs.reactor + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Requires +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.* +import io.micronaut.http.client.HttpClient +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import io.micronaut.runtime.server.EmbeddedServer +import jakarta.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactor.ReactorContext +import kotlinx.coroutines.reactor.asCoroutineContext +import kotlinx.coroutines.reactor.mono +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Test +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.util.context.Context +import reactor.util.function.Tuple2 +import reactor.util.function.Tuples +import java.util.* + +class ReactorContextPropagationSpec { + + @Test + fun testKotlinPropagation() { + val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("mdc.reactortestpropagation.enabled" to "true" as Any) + ) + val client = embeddedServer.applicationContext.getBean(HttpClient::class.java) + + val result: MutableList> = Flux.range(1, 1000) + .flatMap { + val tracingId = UUID.randomUUID().toString() + val get = HttpRequest.POST("http://localhost:${embeddedServer.port}/trigger", NameRequestBody("sss-" + tracingId)).header("X-TrackingId", tracingId) + Mono.from(client.retrieve(get, String::class.java)) + .map { Tuples.of(it as String, tracingId) } + } + .collectList() + .block() + + for (t in result) { + assert(t.t1 == t.t2) + } + + embeddedServer.stop() + } + + +} + +@Requires(property = "mdc.reactortestpropagation.enabled") +@Controller +class TestController(private val someService: SomeService) { + + @Post("/trigger") + suspend fun trigger(request: HttpRequest<*>, @Body requestBody: SomeBody): String { + return withContext(Dispatchers.IO) { + someService.findValue() + } + } + + // tag::readctx[] + @Get("/data") + suspend fun getTracingId(request: HttpRequest<*>): String { + val reactorContextView = currentCoroutineContext()[ReactorContext.Key]!!.context + return reactorContextView.get("reactorTrackingId") as String + } + // end::readctx[] + +} + +@Introspected +class SomeBody(val name: String) + +@Requires(property = "mdc.reactortestpropagation.enabled") +@Singleton +class SomeService { + + suspend fun findValue(): String { + delay(50) + return withContext(Dispatchers.Default) { + delay(50) + val context = currentCoroutineContext()[ReactorContext.Key]!!.context + val reactorTrackingId = context.get("reactorTrackingId") as String + val suspendTrackingId = context.get("suspendTrackingId") as String + if (reactorTrackingId != suspendTrackingId) { + throw IllegalArgumentException() + } + suspendTrackingId + } + } + +} + +@Introspected +class NameRequestBody(val name: String) + +@Requires(property = "mdc.reactortestpropagation.enabled") +// tag::simplefilter[] +@Filter(Filter.MATCH_ALL_PATTERN) +class ReactorHttpServerFilter : HttpServerFilter { + + override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + val trackingId = request.headers["X-TrackingId"] as String + return Mono.from(chain.proceed(request)).contextWrite { + it.put("reactorTrackingId", trackingId) + } + } + + override fun getOrder(): Int = 1 +} +// end::simplefilter[] + +@Requires(property = "mdc.reactortestpropagation.enabled") +// tag::suspendfilter[] +@Filter(Filter.MATCH_ALL_PATTERN) +class SuspendHttpServerFilter : CoroutineHttpServerFilter { + + override suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*> { + val trackingId = request.headers["X-TrackingId"] as String + //withContext does not merge the current context so data may be lost + return withContext(Context.of("suspendTrackingId", trackingId).asCoroutineContext()) { + chain.next(request) + } + } + + override fun getOrder(): Int = 0 +} + +interface CoroutineHttpServerFilter : HttpServerFilter { + + suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*> + + override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + return mono { + filter(request, chain) + } + } + +} + +suspend fun ServerFilterChain.next(request: HttpRequest<*>): MutableHttpResponse<*> { + return this.proceed(request).asFlow().single() +} +// end::suspendfilter[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt new file mode 100644 index 00000000000..897f530d0d1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class BookFactory { + + @Singleton + internal fun novel(): Book { + return Book("A Great Novel") + } + + @Singleton + internal fun textBook(): TextBook { + return TextBook("Learning 101") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt new file mode 100644 index 00000000000..3699744e4e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/BookService.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.docs.requires.Book + +interface BookService { + fun findBook(title: String): Book? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt new file mode 100644 index 00000000000..4e1bf1ed192 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/CustomBookFactory.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +@Replaces(factory = BookFactory::class) +class CustomBookFactory { + + @Singleton + internal fun otherNovel(): Book { + return Book("An OK Novel") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt new file mode 100644 index 00000000000..304d3768498 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/JdbcBookService.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Requires +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton +import javax.sql.DataSource +import java.sql.SQLException + +// tag::replaces[] +@Singleton +@Requires(beans = [DataSource::class]) +class JdbcBookService(internal var dataSource: DataSource) : BookService { + + // end::replaces[] + + override fun findBook(title: String): Book? { + try { + dataSource.connection.use { connection -> + val ps = connection.prepareStatement("select * from books where title = ?") + ps.setString(1, title) + val rs = ps.executeQuery() + if (rs.next()) { + return Book(rs.getString("title")) + } + } + } catch (ex: SQLException) { + return null + } + + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt new file mode 100644 index 00000000000..d18572661b2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/MockBookService.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Replaces +import io.micronaut.docs.requires.Book + +import jakarta.inject.Singleton +import java.util.LinkedHashMap + +// tag::class[] +@Replaces(JdbcBookService::class) // <1> +@Singleton +class MockBookService : BookService { + + var bookMap: Map = LinkedHashMap() + + override fun findBook(title: String): Book? { + return bookMap[title] + } +} +// tag::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt new file mode 100644 index 00000000000..fd891b66c99 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/RequiresSpec.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.replaces + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.types.shouldBeInstanceOf +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.requires.Book + +class RequiresSpec : StringSpec({ + + "test bean replaces" { + val applicationContext = ApplicationContext.run() + applicationContext.getBean(BookService::class.java).shouldBeInstanceOf() + applicationContext.getBean(Book::class.java).title.shouldBe("An OK Novel") + applicationContext.getBean(TextBook::class.java).title.shouldBe("Learning 305") + + applicationContext.close() + } +}) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt new file mode 100644 index 00000000000..8415d3a00a7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBook.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.docs.requires.Book + +class TextBook(title: String) : Book(title) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt new file mode 100644 index 00000000000..dd83ed0db28 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/replaces/TextBookFactory.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.replaces + +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Replaces + +import jakarta.inject.Singleton + +// tag::class[] +@Factory +class TextBookFactory { + + @Singleton + @Replaces(value = TextBook::class, factory = BookFactory::class) + internal fun textBook(): TextBook { + return TextBook("Learning 305") + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt new file mode 100644 index 00000000000..8d6e19b5371 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/Book.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +open class Book(val title: String) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt new file mode 100644 index 00000000000..97cd71a54c2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/BookService.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +interface BookService { + fun findBook(title: String): Book? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt new file mode 100644 index 00000000000..18b109fc790 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/JdbcBookService.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires + +import jakarta.inject.Singleton +import javax.sql.DataSource +import java.sql.SQLException + +// tag::requires[] +@Singleton +@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url")) +class JdbcBookService(internal var dataSource: DataSource) : BookService { +// end::requires[] + + override fun findBook(title: String): Book? { + try { + dataSource.connection.use { connection -> + val ps = connection.prepareStatement("select * from books where title = ?") + ps.setString(1, title) + val rs = ps.executeQuery() + if (rs.next()) { + return Book(rs.getString("title")) + } + } + } catch (ignored: SQLException) { + return null + } + + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt new file mode 100644 index 00000000000..2df47280cf3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/requires/RequiresJdbc.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.requires + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires + +import javax.sql.DataSource +import java.lang.annotation.* + +// tag::annotation[] +@Documented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url")) +annotation class RequiresJdbc +// end::annotation[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt new file mode 100644 index 00000000000..07d45f80ad5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/respondingnotfound/BooksController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.respondingnotfound + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import reactor.core.publisher.Mono + +@Requires(property = "spec.name", value = "respondingnotfound") +//tag::clazz[] +@Controller("/books") +class BooksController { + + @Get("/stock/{isbn}") + fun stock(isbn: String): Map<*, *>? { + return null //<1> + } + + @Get("/maybestock/{isbn}") + fun maybestock(isbn: String): Mono> { + return Mono.empty() //<2> + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt new file mode 100644 index 00000000000..59582ec56cb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingController.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +import io.micronaut.core.convert.format.Format +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.CookieValue +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Header +import java.time.ZonedDateTime + +@Controller("/binding") +class BindingController { + + // tag::cookie1[] + @Get("/cookieName") + fun cookieName(@CookieValue("myCookie") myCookie: String): String { + // ... + // end::cookie1[] + return myCookie + // tag::cookie1[] + } + // end::cookie1[] + + // tag::cookie2[] + @Get("/cookieInferred") + fun cookieInferred(@CookieValue myCookie: String): String { + // ... + // end::cookie2[] + return myCookie + // tag::cookie2[] + } + // end::cookie2[] + + // tag::cookieMultiple[] + @Get("/cookieMultiple") + fun cookieMultiple(@CookieValue("myCookieA") myCookieA: String, + @CookieValue("myCookieB") myCookieB: String): List { + // ... + // end::cookieMultiple[] + return listOf(myCookieA, myCookieB) + // tag::cookieMultiple[] + } + // end::cookieMultiple[] + + + // tag::header1[] + @Get("/headerName") + fun headerName(@Header("Content-Type") contentType: String): String { + // ... + // end::header1[] + return contentType + // tag::header1[] + } + // end::header1[] + + // tag::header2[] + @Get("/headerInferred") + fun headerInferred(@Header contentType: String): String { + // ... + // end::header2[] + return contentType + // tag::header2[] + } + // end::header2[] + + // tag::header3[] + @Get("/headerNullable") + fun headerNullable(@Header contentType: String?): String? { + // ... + // end::header3[] + return contentType + // tag::header3[] + } + // end::header3[] + + // tag::format1[] + @Get("/date") + fun date(@Header date: ZonedDateTime): String { + // ... + // end::format1[] + return date.toString() + // tag::format1[] + } + // end::format1[] + + // tag::format2[] + @Get("/dateFormat") + fun dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header date: ZonedDateTime): String { + // ... + // end::format2[] + return date.toString() + // tag::format2[] + } + // end::format2[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt new file mode 100644 index 00000000000..5e835f9a931 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BindingControllerTest.kt @@ -0,0 +1,78 @@ +package io.micronaut.docs.server.binding + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.cookie.Cookie +import io.micronaut.runtime.server.EmbeddedServer + +class BindingControllerTest: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test cookie binding" { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieName").cookie(Cookie.of("myCookie", "cookie value"))) + + body shouldNotBe null + body shouldBe "cookie value" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieInferred").cookie(Cookie.of("myCookie", "cookie value"))) + + body shouldNotBe null + body shouldBe "cookie value" + } + + "test multiple cookie binding" { + val cookies = HashSet() + cookies.add(Cookie.of("myCookieA", "cookie A value")) + cookies.add(Cookie.of("myCookieB", "cookie B value")) + + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/cookieMultiple").cookies(cookies)) + + body shouldNotBe null + body shouldBe "[\"cookie A value\",\"cookie B value\"]" + } + + "test header binding"() { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/headerName").header("Content-Type", "test")) + + body shouldNotBe null + body shouldBe "test" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/headerInferred").header("Content-Type", "test")) + + body shouldNotBe null + body shouldBe "test" + + val ex = shouldThrow { + client.toBlocking().retrieve(HttpRequest.GET("/binding/headerNullable")) + } + ex.response.status shouldBe HttpStatus.NOT_FOUND + } + + "test header date binding"() { + var body = client.toBlocking().retrieve(HttpRequest.GET("/binding/date").header("date", "Tue, 3 Jun 2008 11:05:30 GMT")) + + body shouldNotBe null + body shouldBe "2008-06-03T11:05:30Z" + + body = client.toBlocking().retrieve(HttpRequest.GET("/binding/dateFormat").header("date", "03/06/2008 11:05:30 AM GMT")) + + body shouldNotBe null + body shouldBe "2008-06-03T11:05:30Z[GMT]" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt new file mode 100644 index 00000000000..dc3f37b025e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkController.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import javax.validation.Valid +// end::imports[] + +// tag::class[] +@Controller("/api") +open class BookmarkController { + + @Get("/bookmarks/list{?paginationCommand*}") + open fun list(@Valid paginationCommand: PaginationCommand): HttpStatus { + return HttpStatus.OK + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt new file mode 100644 index 00000000000..b8c86dde8ee --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/BookmarkControllerTest.kt @@ -0,0 +1,31 @@ +package io.micronaut.docs.server.binding + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriTemplate + +class BookmarkControllerTest: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test bookmark controller" { + var template = UriTemplate("/api/bookmarks/list{?offset,max,sort,order}") + var uri = template.expand(mapOf("offset" to 0, "max" to 10)) + + var response = client.toBlocking().exchange(uri) + + response.status shouldBe HttpStatus.OK + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt new file mode 100644 index 00000000000..e633ca5c60d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketBean.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.annotation.PathVariable +import io.micronaut.http.annotation.QueryValue +import javax.annotation.Nullable +import javax.validation.constraints.PositiveOrZero +// end::imports[] + +// tag::class[] +@Introspected +data class MovieTicketBean( + val httpRequest: HttpRequest, + @field:PathVariable val movieId: String, + @field:QueryValue @field:PositiveOrZero @field:Nullable val minPrice: Double, + @field:QueryValue @field:PositiveOrZero @field:Nullable val maxPrice: Double +) +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt new file mode 100644 index 00000000000..a93fe966291 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketController.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.RequestBean +import javax.validation.Valid +// end::imports[] + +// tag::class[] +@Controller("/api") +open class MovieTicketController { + + // You can also omit query parameters like: + // @Get("/movie/ticket/{movieId} + @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}") + open fun list(@Valid @RequestBean bean: MovieTicketBean): HttpStatus { + return HttpStatus.OK + } + +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt new file mode 100644 index 00000000000..5fe8bbe80d8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/MovieTicketControllerTest.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.binding + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.uri.UriTemplate +import io.micronaut.runtime.server.EmbeddedServer + +class MovieTicketControllerTest : StringSpec() { + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test bookmark controller" { + var template = UriTemplate("/api/movie/ticket/terminator{?minPrice,maxPrice}") + var uri = template.expand(mapOf("minPrice" to 5.0, "maxPrice" to 20.0)) + + var response = client.toBlocking().exchange(uri) + + response.status shouldBe HttpStatus.OK + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt new file mode 100644 index 00000000000..3396c8824d8 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/binding/PaginationCommand.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.binding + +// tag::imports[] +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Pattern +import javax.validation.constraints.Positive +import javax.validation.constraints.PositiveOrZero + +// end::imports[] + +/** + * @author Puneet Behl + * @since 1.0 + */ +// tag::class[] +@Introspected +class PaginationCommand { + // end::class[] + + // tag::props[] + @PositiveOrZero + var offset: Int? = null + + @Positive + var max: Int? = null + + @Pattern(regexp = "name|href|title") + var sort: String? = null + + @Pattern(regexp = "asc|desc|ASC|DESC") + var order: String? = null + // end::props[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt new file mode 100644 index 00000000000..b6079d18794 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.body + +// tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import javax.validation.constraints.Size +// end::imports[] +// tag::importsreactive[] +import org.reactivestreams.Publisher +import io.micronaut.core.async.annotation.SingleResult +import reactor.core.publisher.Flux +// end::importsreactive[] + +// tag::class[] +@Controller("/receive") +open class MessageController { +// end::class[] + + // tag::echo[] + @Post(value = "/echo", consumes = [MediaType.TEXT_PLAIN]) // <1> + open fun echo(@Size(max = 1024) @Body text: String): String { // <2> + return text // <3> + } + // end::echo[] + + // tag::echoReactive[] + @Post(value = "/echo-publisher", consumes = [MediaType.TEXT_PLAIN]) // <1> + @SingleResult + open fun echoFlow(@Body text: Publisher): Publisher> { //<2> + return Flux.from(text) + .collect({ StringBuffer() }, { obj, str -> obj.append(str) }) // <3> + .map { buffer -> HttpResponse.ok(buffer.toString()) } + } + // end::echoReactive[] + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt new file mode 100644 index 00000000000..78576a20c8a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/body/MessageControllerSpec.kt @@ -0,0 +1,40 @@ +package io.micronaut.docs.server.body + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MessageControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test echo response"() { + val body = "My Text" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/receive/echo", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + response shouldBe body + } + + "test echo reactive response"() { + val body = "My Text" + val response = client.toBlocking().retrieve( + HttpRequest.POST("/receive/echo-publisher", body) + .contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + + response shouldBe body + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt new file mode 100644 index 00000000000..cfcc14fded5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesController.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.consumes + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Consumes +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +//end::imports[] + +@Requires(property = "spec.name", value = "consumesspec") +//tag::clazz[] +@Controller("/consumes") +class ConsumesController { + + @Post // <1> + fun index(): HttpResponse<*> { + return HttpResponse.ok() + } + + @Consumes(MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON) // <2> + @Post("/multiple") + fun multipleConsumes(): HttpResponse<*> { + return HttpResponse.ok() + } + + @Post(value = "/member", consumes = [MediaType.TEXT_PLAIN]) // <3> + fun consumesMember(): HttpResponse<*> { + return HttpResponse.ok() + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt new file mode 100644 index 00000000000..ed2a14afc3b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/consumes/ConsumesControllerSpec.kt @@ -0,0 +1,62 @@ +package io.micronaut.docs.server.consumes + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.Introspected +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class ConsumesControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "consumesspec")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test consumes"() { + val book = Book() + book.title = "The Stand" + book.pages = 1000 + + shouldThrow { + client.toBlocking().exchange(HttpRequest.POST("/consumes", book) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes", book) + .contentType(MediaType.APPLICATION_JSON)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/multiple", book) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/multiple", book) + .contentType(MediaType.APPLICATION_JSON)) + } + + shouldNotThrowAny { + client.toBlocking().exchange(HttpRequest.POST("/consumes/member", book) + .contentType(MediaType.TEXT_PLAIN)) + } + } + } + + @Introspected + class Book { + var title: String? = null + var pages: Int? = null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt new file mode 100644 index 00000000000..d15e9d9a826 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpoint.kt @@ -0,0 +1,38 @@ +package io.micronaut.docs.server.endpoint + +import io.micronaut.context.annotation.Requires +//tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.management.endpoint.annotation.Delete +import io.micronaut.management.endpoint.annotation.Endpoint +import io.micronaut.management.endpoint.annotation.Read +import io.micronaut.management.endpoint.annotation.Sensitive +import io.micronaut.management.endpoint.annotation.Write +import java.util.concurrent.CopyOnWriteArrayList +//end::imports[] + +@Requires(property = "spec.name", value = "AlertsEndpointSpec") +//tag::clazz[] +@Endpoint(id = "alerts", defaultSensitive = false) // <1> +class AlertsEndpoint { + + private val alerts: MutableList = CopyOnWriteArrayList() + + @Read + fun getAlerts(): List { + return alerts + } + + @Delete + @Sensitive(true) // <2> + fun clearAlerts() { + alerts.clear() + } + + @Write(consumes = [MediaType.TEXT_PLAIN]) + @Sensitive(property = "add.sensitive", defaultValue = true) // <3> + fun addAlert(alert: String) { + alerts.add(alert) + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt new file mode 100644 index 00000000000..48471d00049 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/AlertsEndpointSpec.kt @@ -0,0 +1,57 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class AlertsEndpointSpec: StringSpec() { + + init { + "test adding an alert" { + var server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to AlertsEndpointSpec::class.simpleName)) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + try { + client.toBlocking().exchange(HttpRequest.POST("/alerts", "First alert").contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.status() shouldBe HttpStatus.UNAUTHORIZED + } + server.close() + } + + "test adding an alert not sensitive" { + var server = ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to AlertsEndpointSpec::class.simpleName, + "endpoints.alerts.add.sensitive" to false) + ) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + + val response = client.toBlocking().exchange(HttpRequest.POST("/alerts", "First alert").contentType(MediaType.TEXT_PLAIN_TYPE), String::class.java) + response.status() shouldBe HttpStatus.OK + + val alerts = client.toBlocking().retrieve(HttpRequest.GET("/alerts"), Argument.LIST_OF_STRING) + alerts[0] shouldBe "First alert" + + server.close() + } + + "test clearing alerts" { + var server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to AlertsEndpointSpec::class.simpleName)) + var client = server.applicationContext.createBean(HttpClient::class.java, server.url) + try { + client.toBlocking().exchange(HttpRequest.DELETE("/alerts"), String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.status() shouldBe HttpStatus.UNAUTHORIZED + } + server.close() + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt new file mode 100644 index 00000000000..7322fdaac41 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpoint.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.endpoint + +//tag::endpointImport[] +import io.micronaut.management.endpoint.annotation.Endpoint +//end::endpointImport[] + +//tag::readImport[] +import io.micronaut.management.endpoint.annotation.Read +//end::readImport[] + +//tag::mediaTypeImport[] +import io.micronaut.http.MediaType +import io.micronaut.management.endpoint.annotation.Selector +//end::mediaTypeImport[] + +//tag::writeImport[] +import io.micronaut.management.endpoint.annotation.Write +//end::writeImport[] + +import jakarta.annotation.PostConstruct +import java.util.Date + +//tag::endpointClassBegin[] +@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false) +class CurrentDateEndpoint { + //end::endpointClassBegin[] + + //tag::methodSummary[] + //.. endpoint methods + //end::methodSummary[] + + //tag::currentDate[] + private var currentDate: Date? = null + //end::currentDate[] + + @PostConstruct + fun init() { + currentDate = Date() + } + + //tag::simpleRead[] + @Read + fun currentDate(): Date? { + return currentDate + } + //end::simpleRead[] + + //tag::readArg[] + @Read(produces = [MediaType.TEXT_PLAIN]) //<1> + fun currentDatePrefix(@Selector prefix: String): String { + return "$prefix: $currentDate" + } + //end::readArg[] + + //tag::simpleWrite[] + @Write + fun reset(): String { + currentDate = Date() + + return "Current date reset" + } + //end::simpleWrite[] + //tag::endpointClassEnd[] +} +//end::endpointClassEnd[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt new file mode 100644 index 00000000000..f90685b28fd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/CurrentDateEndpointSpec.kt @@ -0,0 +1,80 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +import java.util.Date + +class CurrentDateEndpointSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test read custom date endpoint" { + val response = client.toBlocking().exchange("/date", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + } + + "test read custom date endpoint with argument" { + val response = client.toBlocking().exchange("/date/current_date_is", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body()!!.startsWith("current_date_is: ") shouldBe true + } + + // issue https://github.com/micronaut-projects/micronaut-core/issues/883 + "test read with produces" { + val response = client.toBlocking().exchange("/date/current_date_is", String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_PLAIN_TYPE + } + + "test write custom date endpoint" { + val originalDate: Date + val resetDate: Date + + var response = client.toBlocking().exchange("/date", String::class.java) + originalDate = Date(java.lang.Long.parseLong(response.body()!!)) + + response = client.toBlocking().exchange(HttpRequest.POST>("/date", mapOf()), String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Current date reset" + + response = client.toBlocking().exchange("/date", String::class.java) + resetDate = Date(java.lang.Long.parseLong(response.body()!!)) + + assert(resetDate.time > originalDate.time) + } + + "test disable endpoint" { + embeddedServer.stop() // top the previously created server otherwise a port conflict will occur + + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("custom.date.enabled" to false)) + val rxClient = server.applicationContext.createBean(HttpClient::class.java, server.url) + + try { + rxClient.toBlocking().exchange("/date", String::class.java) + } catch (ex: HttpClientResponseException) { + ex.response.code() shouldBe HttpStatus.NOT_FOUND.code + } + + server.close() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt new file mode 100644 index 00000000000..0a1c78e09e2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpoint.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.endpoint + +//tag::endpointImport[] +import io.micronaut.context.annotation.Requires +import io.micronaut.management.endpoint.annotation.Endpoint +//end::endpointImport[] + +//tag::mediaTypeImport[] +import io.micronaut.http.MediaType +//end::mediaTypeImport[] + +//tag::writeImport[] +import io.micronaut.management.endpoint.annotation.Write +//end::writeImport[] + +//tag::deleteImport[] +import io.micronaut.management.endpoint.annotation.Delete +//end::deleteImport[] + +import io.micronaut.management.endpoint.annotation.Read + +import jakarta.annotation.PostConstruct + +@Requires(property = "spec.name", value = "MessageEndpointSpec") +//tag::endpointClassBegin[] +@Endpoint(id = "message", defaultSensitive = false) +class MessageEndpoint { + //end::endpointClassBegin[] + + //tag::message[] + internal var message: String? = null + //end::message[] + + @PostConstruct + fun init() { + this.message = "default message" + } + + @Read + fun message(): String? { + return this.message + } + + //tag::writeArg[] + @Write(consumes = [MediaType.APPLICATION_FORM_URLENCODED], produces = [MediaType.TEXT_PLAIN]) + fun updateMessage(newMessage: String): String { //<1> + this.message = newMessage + + return "Message updated" + } + //end::writeArg[] + + //tag::simpleDelete[] + @Delete + fun deleteMessage(): String { + this.message = null + + return "Message deleted" + } + //end::simpleDelete[] + + //tag::endpointClassEnd[] +} +//end::endpointClassEnd[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt new file mode 100644 index 00000000000..86144134c72 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/endpoint/MessageEndpointSpec.kt @@ -0,0 +1,63 @@ +package io.micronaut.docs.server.endpoint + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +import org.junit.Assert.fail +import reactor.core.publisher.Flux + +class MessageEndpointSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to MessageEndpointSpec::class.java.simpleName, "endpoints.message.enabled" to true)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test read message endpoint" { + val response = client.toBlocking().exchange("/message", String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "default message" + } + + "test write message endpoint" { + var response = Flux.from(client.exchange(HttpRequest.POST>("/message", mapOf("newMessage" to "A new message")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED), String::class.java)).blockFirst() + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Message updated" + response.contentType.get() shouldBe MediaType.TEXT_PLAIN_TYPE + + response = client.toBlocking().exchange("/message", String::class.java) + + response.body() shouldBe "A new message" + } + + "test delete message endpoint" { + val response = client.toBlocking().exchange(HttpRequest.DELETE("/message"), String::class.java) + + response.code() shouldBe HttpStatus.OK.code + response.body() shouldBe "Message deleted" + + try { + client.toBlocking().exchange("/message", String::class.java) + } catch (e: HttpClientResponseException) { + e.status.code shouldBe 404 + } catch (e: Exception) { + fail("Wrong exception thrown") + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt new file mode 100644 index 00000000000..a8268e461ed --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/BookController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces + +@Requires(property = "spec.name", value = "ExceptionHandlerSpec") +//tag::clazz[] +@Controller("/books") +class BookController { + + @Produces(MediaType.TEXT_PLAIN) + @Get("/stock/{isbn}") + internal fun stock(isbn: String): Int? { + throw OutOfStockException() + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt new file mode 100644 index 00000000000..d6dfc78f22e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/ExceptionHandlerSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.server.exception + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer + +class ExceptionHandlerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to ExceptionHandlerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test exception is handled"() { + val request = HttpRequest.GET("/books/stock/1234") + val errorType = Argument.mapOf( + String::class.java, + Any::class.java + ) + val ex = shouldThrow { + client!!.toBlocking().retrieve(request, Argument.LONG, errorType) + } + + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + response.status().shouldBe(HttpStatus.BAD_REQUEST) + message shouldBe("No stock available") + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt new file mode 100644 index 00000000000..65c8afa2a75 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockException.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +//tag::clazz[] +class OutOfStockException : RuntimeException() +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt new file mode 100644 index 00000000000..9f4c8a2d551 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/exception/OutOfStockExceptionHandler.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.exception + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Produces +import io.micronaut.http.server.exceptions.ExceptionHandler +import io.micronaut.http.server.exceptions.response.ErrorContext +import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor +import jakarta.inject.Singleton + +//tag::clazz[] +@Produces +@Singleton +@Requirements( +//end::clazz[] + Requires(property = "spec.name", value = "ExceptionHandlerSpec"), +//tag::clazz[] + Requires(classes = [OutOfStockException::class, ExceptionHandler::class]) +) +class OutOfStockExceptionHandler(private val errorResponseProcessor: ErrorResponseProcessor) : + ExceptionHandler> { + + override fun handle(request: HttpRequest<*>, exception: OutOfStockException): HttpResponse<*> { + return errorResponseProcessor.processResponse( + ErrorContext.builder(request) + .cause(exception) + .errorMessage("No stock available") + .build(), HttpResponse.badRequest()) // <1> + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt new file mode 100644 index 00000000000..1f455669336 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilter.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.HttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import org.reactivestreams.Publisher +// end::imports[] + +// tag::class[] +@Filter("/hello/**") // <1> +class TraceFilter(// <2> + private val traceService: TraceService)// <3> + : HttpServerFilter { + // end::class[] + + // tag::doFilter[] + override fun doFilter(request: HttpRequest<*>, + chain: ServerFilterChain): Publisher> { + return traceService.trace(request) // <1> + .switchMap { aBoolean -> chain.proceed(request) } // <2> + .doOnNext { res -> + res.headers.add("X-Trace-Enabled", "true") // <3> + } + } + // end::doFilter[] +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt new file mode 100644 index 00000000000..494d401af96 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceFilterSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.filters + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.docs.server.intro.HelloControllerSpec +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class TraceFilterSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, + mapOf("spec.name" to HelloControllerSpec::class.java.simpleName, "spec.lang" to "java")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "test trace filter" { + val response = client.toBlocking().exchange(HttpRequest.GET("/hello")) + + response.headers.get("X-Trace-Enabled") shouldBe "true" + } + } +} + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt new file mode 100644 index 00000000000..c44230d32e5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/filters/TraceService.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.filters + +// tag::imports[] +import io.micronaut.http.HttpRequest +import org.slf4j.LoggerFactory +import jakarta.inject.Singleton +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers + +// end::imports[] + +// tag::class[] +@Singleton +class TraceService { + + private val LOG = LoggerFactory.getLogger(TraceService::class.java) + + internal fun trace(request: HttpRequest<*>): Flux { + return Mono.fromCallable { + // <1> + LOG.debug("Tracing request: {}", request.uri) + // trace logic here, potentially performing I/O <2> + true + }.subscribeOn(Schedulers.boundedElastic()) // <3> + .flux() + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt new file mode 100644 index 00000000000..c350c1cbf91 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/Application.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.runtime.Micronaut +// end::imports[] + +// tag::class[] +object Application { + + @JvmStatic + fun main(args: Array) { + Micronaut.run(Application.javaClass) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt new file mode 100644 index 00000000000..6ca0d8d9969 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClient.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.core.async.annotation.SingleResult +import org.reactivestreams.Publisher +// end::imports[] + +/** + * @author graemerocher + * @since 1.0 + */ +// tag::class[] +@Client("/hello") // <1> +interface HelloClient { + + @Get(consumes = [MediaType.TEXT_PLAIN]) // <2> + @SingleResult + fun hello(): Publisher // <3> +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt new file mode 100644 index 00000000000..3dd87fd50df --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloClientSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.server.intro + +// tag::imports[] +import io.micronaut.context.annotation.Property +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import reactor.core.publisher.Mono + +// end::imports[] + +/** + * @author graemerocher + * @since 1.0 + */ +@Property(name = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@MicronautTest // <1> +class HelloClientSpec { + + @Inject + lateinit var client: HelloClient // <2> + + @Test + fun testHelloWorldResponse() { + assertEquals("Hello World", Mono.from(client.hello()).block())// <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt new file mode 100644 index 00000000000..a065daf5260 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloController.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +@Requires(property = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@Controller("/hello") // <1> +class HelloController { + + @Get(produces = [MediaType.TEXT_PLAIN]) // <2> + fun index(): String { + return "Hello World" // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt new file mode 100644 index 00000000000..acce91eaef2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/intro/HelloControllerSpec.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.intro + +import io.micronaut.context.annotation.Property +// tag::imports[] +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +// end::imports[] + +@Property(name = "spec.name", value = "HelloControllerSpec") +// tag::class[] +@MicronautTest +class HelloControllerSpec { + + @Inject + lateinit var server: EmbeddedServer // <1> + + @Inject + @field:Client("/") + lateinit var client: HttpClient // <2> + + @Test + fun testHelloWorldResponse() { + val rsp: String = client.toBlocking() // <3> + .retrieve("/hello") + assertEquals("Hello World", rsp) // <4> + } +} +//end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt new file mode 100644 index 00000000000..2acc0d4b576 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/Person.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +class Person { + + lateinit var firstName: String + lateinit var lastName: String + var age: Int = 0 + + constructor(firstName: String, lastName: String) { + this.firstName = firstName + this.lastName = lastName + } + + constructor() {} +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt new file mode 100644 index 00000000000..6ae2156ecd4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonController.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +import com.fasterxml.jackson.core.JsonParseException +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.http.hateoas.JsonError +import io.micronaut.http.hateoas.Link +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.util.Optional +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import io.micronaut.core.async.annotation.SingleResult + +@Requires(property = "spec.name", value = "PersonControllerSpec") +// tag::class[] +@Controller("/people") +class PersonController { + + internal var inMemoryDatastore: MutableMap = ConcurrentHashMap() + // end::class[] + + @Get + fun index(): Collection { + return inMemoryDatastore.values + } + + @Get("/{name}") + @SingleResult + operator fun get(name: String): Publisher { + return if (inMemoryDatastore.containsKey(name)) { + Mono.just(inMemoryDatastore[name]) + } else Mono.empty() + } + + // tag::single[] + @Post("/saveReactive") + @SingleResult + fun save(@Body person: Publisher): Publisher> { // <1> + return Mono.from(person).map { p -> + inMemoryDatastore[p.firstName] = p // <2> + HttpResponse.created(p) // <3> + } + } + // end::single[] + + // tag::args[] + @Post("/saveWithArgs") + fun save(firstName: String, lastName: String, age: Optional): HttpResponse { + val p = Person(firstName, lastName) + age.ifPresent { p.age = it } + inMemoryDatastore[p.firstName] = p + return HttpResponse.created(p) + } + // end::args[] + + // tag::future[] + @Post("/saveFuture") + fun save(@Body person: CompletableFuture): CompletableFuture> { + return person.thenApply { p -> + inMemoryDatastore[p.firstName] = p + HttpResponse.created(p) + } + } + // end::future[] + + // tag::regular[] + @Post + fun save(@Body person: Person): HttpResponse { + inMemoryDatastore[person.firstName] = person + return HttpResponse.created(person) + } + // end::regular[] + + // tag::localError[] + @Error + fun jsonError(request: HttpRequest<*>, e: JsonParseException): HttpResponse { // <1> + val error = JsonError("Invalid JSON: ${e.message}") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.status(HttpStatus.BAD_REQUEST, "Fix Your JSON") + .body(error) // <3> + } + // end::localError[] + + @Get("/error") + fun throwError(): String { + throw RuntimeException("Something went wrong") + } + + // tag::globalError[] + @Error(global = true) // <1> + fun error(request: HttpRequest<*>, e: Throwable): HttpResponse { + val error = JsonError("Bad Things Happened: ${e.message}") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.serverError() + .body(error) // <3> + } + // end::globalError[] + + // tag::statusError[] + @Error(status = HttpStatus.NOT_FOUND) + fun notFound(request: HttpRequest<*>): HttpResponse { // <1> + val error = JsonError("Person Not Found") // <2> + .link(Link.SELF, Link.of(request.uri)) + + return HttpResponse.notFound() + .body(error) // <3> + } + // end::statusError[] + + // tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt new file mode 100644 index 00000000000..d49147448ad --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/json/PersonControllerSpec.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.json + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.jupiter.api.Assertions + +import org.junit.Assert.assertTrue +import reactor.core.publisher.Flux + +class PersonControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to PersonControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test global error handler"() { + val e = Assertions.assertThrows(HttpClientResponseException::class.java) { + Flux.from(client!!.exchange("/people/error", Map::class.java)) + .blockFirst() + } + val response = e.response as HttpResponse> + + response.status shouldBe HttpStatus.INTERNAL_SERVER_ERROR + response.body.get()["message"] shouldBe "Bad Things Happened: Something went wrong" + } + + "test save"() { + var response = client.toBlocking().exchange(HttpRequest.POST("/people", "{\"firstName\":\"Fred\",\"lastName\":\"Flintstone\",\"age\":45}"), Person::class.java) + var person = response.body.get() + + person.firstName shouldBe "Fred" + response.status shouldBe HttpStatus.CREATED + + response = client.toBlocking().exchange(HttpRequest.GET("/people/Fred"), Person::class.java) + person = response.body.get() + + person.firstName shouldBe "Fred" + response.status shouldBe HttpStatus.OK + } + + "test save reactive"() { + val response = client.toBlocking().exchange(HttpRequest.POST("/people/saveReactive", "{\"firstName\":\"Wilma\",\"lastName\":\"Flintstone\",\"age\":36}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Wilma" + response.status shouldBe HttpStatus.CREATED + } + + "test save future"() { + val response = client!!.toBlocking().exchange(HttpRequest.POST("/people/saveFuture", "{\"firstName\":\"Pebbles\",\"lastName\":\"Flintstone\",\"age\":0}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Pebbles" + response.status shouldBe HttpStatus.CREATED + } + + "test save args"() { + val response = client!!.toBlocking().exchange(HttpRequest.POST("/people/saveWithArgs", "{\"firstName\":\"Dino\",\"lastName\":\"Flintstone\",\"age\":3}"), Person::class.java) + val person = response.body.get() + + person.firstName shouldBe "Dino" + response.status shouldBe HttpStatus.CREATED + } + + "test person not found"() { + val e = shouldThrow { + Flux.from(client.exchange("/people/Sally", Map::class.java)) + .blockFirst() + } + val response = e.response as HttpResponse> + + response.body.get()["message"] shouldBe "Person Not Found" + response.status shouldBe HttpStatus.NOT_FOUND + } + + "test save invalid json"() { + val e = shouldThrow { + client.toBlocking().exchange(HttpRequest.POST("/people", "{\""), Argument.of(Person::class.java), Argument.of(Map::class.java)) + } + val response = e.response as HttpResponse> + + assertTrue(response.getBody(Map::class.java).get()["message"].toString().startsWith("Invalid JSON: Unexpected end-of-input")) + response.status shouldBe HttpStatus.BAD_REQUEST + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt new file mode 100644 index 00000000000..ce3d9cbdea5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageController.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.request + +// tag::imports[] +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.context.ServerRequestContext +import reactor.core.publisher.Mono +import reactor.util.context.ContextView + +// end::imports[] + +// tag::class[] +@Controller("/request") +class MessageController { +// end::class[] + + // tag::request[] + @Get("/hello") // <1> + fun hello(request: HttpRequest<*>): HttpResponse { + val name = request.parameters + .getFirst("name") + .orElse("Nobody") // <2> + + return HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo") // <3> + } + // end::request[] + + // tag::static-request[] + @Get("/hello-static") // <1> + fun helloStatic(): HttpResponse { + val request: HttpRequest<*> = ServerRequestContext.currentRequest() // <1> + .orElseThrow { RuntimeException("No request present") } + val name = request.parameters + .getFirst("name") + .orElse("Nobody") + return HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo") + } + // end::static-request[] + + // tag::request-context[] + @Get("/hello-reactor") + fun helloReactor(): Mono?>? { + return Mono.deferContextual { ctx: ContextView -> // <1> + val request = ctx.get>(ServerRequestContext.KEY) // <2> + val name = request.parameters + .getFirst("name") + .orElse("Nobody") + Mono.just(HttpResponse.ok("Hello $name!!") + .header("X-My-Header", "Foo")) + } + } + // end::request-context[] +// tag::endclass[] +} +// end::endclass[] + diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt new file mode 100644 index 00000000000..1055fde505d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/request/MessageControllerSpec.kt @@ -0,0 +1,39 @@ +package io.micronaut.docs.server.request + +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MessageControllerSpec: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) // <1> + ) + + init { + "test message controller"() { + var body = client.toBlocking().retrieve("/request/hello?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + + body = client.toBlocking().retrieve("/request/hello-static?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + + body = client.toBlocking().retrieve("/request/hello-reactor?name=John") + + body shouldNotBe null + body shouldBe "Hello John!!" + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt new file mode 100644 index 00000000000..2ac49b02811 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesController.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.response + +//tag::imports[] +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +//end::imports[] + +@Requires(property = "spec.name", value = "producesspec") +//tag::clazz[] +@Controller("/produces") +class ProducesController { + + @Get // <1> + fun index(): HttpResponse<*> { + return HttpResponse.ok().body("{\"msg\":\"This is JSON\"}") + } + + @Produces(MediaType.TEXT_HTML) + @Get("/html") // <2> + fun html(): String { + return "<h1>HTML</h1>" + } + + @Get(value = "/xml", produces = [MediaType.TEXT_XML]) // <3> + fun xml(): String { + return "<h1>XML</h1>" + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt new file mode 100644 index 00000000000..357ffdd90de --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/ProducesControllerSpec.kt @@ -0,0 +1,37 @@ +package io.micronaut.docs.server.response + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class ProducesControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "producesspec")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test content types"() { + var response = client.toBlocking().exchange(HttpRequest.GET("/produces"), String::class.java) + + response.contentType.get() shouldBe MediaType.APPLICATION_JSON_TYPE + + response = client.toBlocking().exchange(HttpRequest.GET("/produces/html"), String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_HTML_TYPE + + response = client.toBlocking().exchange(HttpRequest.GET("/produces/xml"), String::class.java) + + response.contentType.get() shouldBe MediaType.TEXT_XML_TYPE + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt new file mode 100644 index 00000000000..408ffac1481 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusController.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.response + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Status + +@Requires(property = "spec.name", value = "httpstatus") +@Controller("/status") +class StatusController { + + //tag::atstatus[] + @Status(HttpStatus.CREATED) + @Get(produces = [MediaType.TEXT_PLAIN]) + fun index(): String { + return "success" + } + //end::atstatus[] + + //tag::httpstatus[] + @Get("/http-status") + fun httpStatus(): HttpStatus { + return HttpStatus.CREATED + } + //end::httpstatus[] + + //tag::httpresponse[] + @Get(value = "/http-response", produces = [MediaType.TEXT_PLAIN]) + fun httpResponse(): HttpResponse { + return HttpResponse.status(HttpStatus.CREATED).body("success") + } + //end::httpresponse[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt new file mode 100644 index 00000000000..91457ea2d65 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/response/StatusControllerSpec.kt @@ -0,0 +1,41 @@ +package io.micronaut.docs.server.response + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class StatusControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "httpstatus")) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test status"() { + var response = client.toBlocking().exchange(HttpRequest.GET("/status"), String::class.java) + var body = response.body + + response.status shouldBe HttpStatus.CREATED + body.get() shouldBe "success" + + response = client.toBlocking().exchange(HttpRequest.GET("/status/http-response"), String::class.java) + body = response.body + + response.status shouldBe HttpStatus.CREATED + body.get() shouldBe "success" + + response = client.toBlocking().exchange(HttpRequest.GET("/status/http-status"), String::class.java) + + response.status shouldBe HttpStatus.CREATED + } + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt new file mode 100644 index 00000000000..180d62d6266 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesController.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.PathVariable +// end::imports[] + +// tag::class[] +@Controller("/issues") // <1> +class IssuesController { + + @Get("/{number}") // <2> + fun issue(@PathVariable number: Int): String { // <3> + return "Issue # $number!" // <4> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt new file mode 100644 index 00000000000..712ad1fe04c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/IssuesControllerTest.kt @@ -0,0 +1,52 @@ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +// end::imports[] + +// tag::class[] +class IssuesControllerTest: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean( + HttpClient::class.java, + embeddedServer.url) // <1> + ) + + init { + "test issue" { + val body = client.toBlocking().retrieve("/issues/12") // <3> + + body shouldNotBe null + body shouldBe "Issue # 12!" // <4> + } + + "test issue with invalid integer" { + val e = shouldThrow { + client.toBlocking().exchange("/issues/hello") + } + + e.status.code shouldBe 400 // <5> + } + + "test issue without number" { + val e = shouldThrow { + client.toBlocking().exchange("/issues/") + } + + e.status.code shouldBe 404 // <6> + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt new file mode 100644 index 00000000000..d102bda222b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutes.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routes + +// tag::imports[] +import io.micronaut.context.ExecutionHandleLocator +import io.micronaut.web.router.DefaultRouteBuilder +import io.micronaut.web.router.RouteBuilder +import jakarta.inject.Inject +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Singleton +class MyRoutes(executionHandleLocator: ExecutionHandleLocator, + uriNamingStrategy: RouteBuilder.UriNamingStrategy) : + DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) { // <1> + + @Inject + fun issuesRoutes(issuesController: IssuesController) { // <2> + GET("/issues/show/{number}", issuesController, "issue", Int::class.java) // <3> + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt new file mode 100644 index 00000000000..00868c82e0f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routes/MyRoutesSpec.kt @@ -0,0 +1,28 @@ +package io.micronaut.docs.server.routes + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer + +class MyRoutesSpec: StringSpec() { + + val embeddedServer = autoClose( // <2> + ApplicationContext.run(EmbeddedServer::class.java) // <1> + ) + + val client = autoClose( // <2> + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) // <1> + ) + + init { + "test custom route" { + val body = client.toBlocking().retrieve("/issues/show/12") // <3> + + body shouldNotBe null + body shouldBe "Issue # 12!" // <4> + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt new file mode 100644 index 00000000000..7f815b58ea6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleController.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routing + +import io.micronaut.context.annotation.Requires +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +@Requires(property = "spec.name", value = "BackwardCompatibleControllerSpec") +// tag::class[] +@Controller("/hello") +class BackwardCompatibleController { + + @Get(uris = ["/{name}", "/person/{name}"]) // <1> + fun hello(name: String): String { // <2> + return "Hello, $name" + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt new file mode 100644 index 00000000000..55cc8bd85c1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/routing/BackwardCompatibleControllerSpec.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.routing + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import jakarta.inject.Inject + +@Property(name = "spec.name", value = "BackwardCompatibleControllerSpec") +@MicronautTest +class BackwardCompatibleControllerSpec { + + @Inject + @field:Client("/") + lateinit var client: HttpClient + + @Test + fun testHelloWorldResponse() { + var response = client.toBlocking() + .retrieve(HttpRequest.GET("/hello/World")) + assertEquals("Hello, World", response) + + response = client.toBlocking() + .retrieve(HttpRequest.GET("/hello/person/John")) + + assertEquals("Hello, John", response) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt new file mode 100644 index 00000000000..465593a6ddf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/Headline.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.sse + +// tag::class[] +class Headline { + + var title: String? = null + var description: String? = null + + constructor() + + constructor(title: String, description: String) { + this.title = title + this.description = description + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt new file mode 100644 index 00000000000..d77abf1e5da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineController.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.sse + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.sse.Event +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.annotation.ExecuteOn +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux +import reactor.core.publisher.SynchronousSink +import java.util.concurrent.Callable +import java.util.function.BiFunction + +// end::imports[] + +// tag::class[] +@Controller("/headlines") +class HeadlineController { + + @ExecuteOn(TaskExecutors.IO) + @Get(produces = [MediaType.TEXT_EVENT_STREAM]) + fun index(): Publisher> { // <1> + val versions = arrayOf("1.0", "2.0") // <2> + return Flux.generate( + { 0 }, + BiFunction { i: Int, emitter: SynchronousSink> -> // <3> + if (i < versions.size) { + emitter.next( // <4> + Event.of( + Headline( + "Micronaut " + versions[i] + " Released", "Come and get it" + ) + ) + ) + } else { + emitter.complete() // <5> + } + return@BiFunction i + 1 + }) + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..76118b4d950 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/sse/HeadlineControllerSpec.kt @@ -0,0 +1,43 @@ +package io.micronaut.docs.server.sse + +import io.kotest.assertions.timing.eventually +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.sse.SseClient +import io.micronaut.http.sse.Event +import io.micronaut.runtime.server.EmbeddedServer +import org.opentest4j.AssertionFailedError +import reactor.core.publisher.Flux + +import java.util.ArrayList +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.toDuration + +@ExperimentalTime +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + "test consume eventstream object" { + val client = embeddedServer.applicationContext.createBean(SseClient::class.java, embeddedServer.url) + + val events = ArrayList>() + + Flux.from(client.eventStream(HttpRequest.GET("/headlines"), Headline::class.java)).subscribe { + events.add(it) + } + + eventually(2.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) { + events.size shouldBe 2 + events[0].data.title shouldBe "Micronaut 1.0 Released" + events[0].data.description shouldBe "Come and get it" + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt new file mode 100644 index 00000000000..f3b95b50442 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContext.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import kotlin.coroutines.CoroutineContext + +class MyContext(val value: String) : CoroutineContext.Element { + + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key get() = Key + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt new file mode 100644 index 00000000000..18c1c5b38fe --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/MyContextInterceptorAnn.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class MyContextInterceptorAnn diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt new file mode 100644 index 00000000000..2ac1a09fa16 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/Repository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class Repository() diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt new file mode 100644 index 00000000000..e68c8cf424d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendClient.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +@Client("/suspend") +interface SuspendClient { + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simple(): String + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleIgnoreResult() + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleResponse(): HttpResponse + + @Get("/simple", consumes = [MediaType.TEXT_PLAIN]) + suspend fun simpleResponseIgnoreResult(): HttpResponse + + @Get("/delayed", consumes = [MediaType.TEXT_PLAIN]) + suspend fun delayed(): String + + @Get("/illegal", consumes = [MediaType.ALL]) + suspend fun errorCall(): String + + @Get("/illegal", consumes = [MediaType.ALL]) + suspend fun errorCallResponse(): HttpResponse + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt new file mode 100644 index 00000000000..7900fc5cb3f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendController.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.* +import io.micronaut.http.annotation.* +import io.micronaut.http.bind.binders.HttpCoroutineContextFactory +import io.micronaut.http.context.ServerRequestContext +import io.micronaut.scheduling.TaskExecutors +import io.micronaut.tracing.instrument.kotlin.CoroutineTracingDispatcher +import kotlinx.coroutines.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.atomic.AtomicInteger +import jakarta.inject.Named +import org.slf4j.MDC + +@Controller("/suspend") +class SuspendController( + @Named(TaskExecutors.IO) private val executor: ExecutorService, + private val suspendService: SuspendService, + private val suspendRequestScopedService: SuspendRequestScopedService, + private val coroutineTracingDispatcherFactory: HttpCoroutineContextFactory +) { + + private val coroutineDispatcher: CoroutineDispatcher + + init { + coroutineDispatcher = executor.asCoroutineDispatcher() + } + + // tag::suspend[] + @Get("/simple", produces = [MediaType.TEXT_PLAIN]) + suspend fun simple(): String { // <1> + return "Hello" + } + // end::suspend[] + + // tag::suspendDelayed[] + @Get("/delayed", produces = [MediaType.TEXT_PLAIN]) + suspend fun delayed(): String { // <1> + delay(1) // <2> + return "Delayed" + } + // end::suspendDelayed[] + + // tag::suspendStatus[] + @Status(HttpStatus.CREATED) // <1> + @Get("/status") + suspend fun status() { + } + // end::suspendStatus[] + + // tag::suspendStatusDelayed[] + @Status(HttpStatus.CREATED) + @Get("/statusDelayed") + suspend fun statusDelayed() { + delay(1) + } + // end::suspendStatusDelayed[] + + val count = AtomicInteger(0) + + @Get("/count") + suspend fun count(): Int { // <1> + return count.incrementAndGet() + } + + @Get("/greet") + suspend fun suspendingGreet(name: String, request: HttpRequest): HttpResponse { + val json = "{\"message\":\"hello\"}" + return HttpResponse.ok(json).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + } + + @Get("/illegal") + suspend fun illegal() { + throw IllegalArgumentException() + } + + @Get("/illegalWithContext") + suspend fun illegalWithContext(): String = withContext(coroutineDispatcher) { + throw IllegalArgumentException() + } + + @Status(HttpStatus.BAD_REQUEST) + @Error(exception = IllegalArgumentException::class) + @Produces(MediaType.TEXT_PLAIN) + suspend fun onIllegalArgument(e: IllegalArgumentException): String { + return "illegal.argument" + } + + @Get("/callSuspendServiceWithRetries") + suspend fun callSuspendServiceWithRetries(): String { + return suspendService.delayedCalculation1() + } + + @Get("/callSuspendServiceWithRetriesBlocked") + fun callSuspendServiceWithRetriesBlocked(): String { + // Bypass ContinuationArgumentBinder + return runBlocking { + suspendService.delayedCalculation2() + } + } + + @Get("/callSuspendServiceWithRetriesWithoutDelay") + suspend fun callSuspendServiceWithRetriesWithoutDelay(): String { + return suspendService.calculation3() + } + + @Get("/keepRequestScopeInsideCoroutine") + suspend fun keepRequestScopeInsideCoroutine() = coroutineScope { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + val after = async { "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" }.await() + "$before,$after" + } + + @Get("/keepRequestScopeInsideCoroutineWithRetry") + suspend fun keepRequestScopeInsideCoroutineWithRetry() = coroutineScope { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + val after = async { suspendService.requestScopedCalculation() }.await() + "$before,$after" + } + + @Get("/keepRequestScopeAfterSuspend") + suspend fun keepRequestScopeAfterSuspend(): String { + val before = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + delay(10) // suspend + val after = "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + return "$before,$after" + } + + @Get("/requestContext") + suspend fun requestContext(): String { + return suspendService.requestContext() + } + + @Get("/requestContext2") + suspend fun requestContext2(): String = supervisorScope { + require(ServerRequestContext.currentRequest().isPresent) { + "Initial request is not set" + } + val result = withContext(coroutineContext) { + require(ServerRequestContext.currentRequest().isPresent) { + "Request is not available in `withContext`" + } + "test" + } + require(ServerRequestContext.currentRequest().isPresent) { + "Request is lost after `withContext`" + } + result + } + + @Get("/keepTracingContextAfterDelay") + suspend fun keepTracingContextAfterDelay() = coroutineScope { + val before = currentTraceId() + delay(1L) + val after = currentTraceId() + "$before,$after" + } + + @Get("/keepTracingContextInsideCoroutine") + suspend fun keepTracingContextInsideCoroutine() = coroutineScope { + val before = currentTraceId() + val after = withContext(Dispatchers.Default) { currentTraceId() } + "$before,$after" + } + + @Get("/keepTracingContextUsingCoroutineTracingDispatcherExplicitly") + fun keepTracingContextUsingCoroutineTracingDispatcherExplicitly() = runBlocking { + val before = currentTraceId() + val after = withContext(Dispatchers.Default + coroutineTracingDispatcherFactory.create()) { currentTraceId() } + "$before,$after" + } + + private fun currentTraceId(): String? = MDC.get("traceId") +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt new file mode 100644 index 00000000000..8d42b81d188 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendControllerSpec.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.should +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders.* +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.OPTIONS +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.reactive.awaitSingle + +class SuspendControllerSpec : StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run( + EmbeddedServer::class.java, mapOf( + "micronaut.server.cors.enabled" to true, + "micronaut.server.cors.configurations.dev.allowedOrigins" to listOf("foo.com"), + "micronaut.server.cors.configurations.dev.allowedMethods" to listOf("GET"), + "micronaut.server.cors.configurations.dev.allowedHeaders" to listOf(ACCEPT, CONTENT_TYPE), + "tracing.zipkin.enabled" to true + ) + ) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + private var suspendClient = embeddedServer.applicationContext.createBean(SuspendClient::class.java, embeddedServer.url) + + init { + + "test suspend applies CORS options" { + val origin = "foo.com" + val headers = "$CONTENT_TYPE,$ACCEPT" + val method = HttpMethod.GET + val optionsResponse = client.exchange( + OPTIONS("/suspend/greet") + .header(ORIGIN, origin) + .header(ACCESS_CONTROL_REQUEST_METHOD, method) + .header(ACCESS_CONTROL_REQUEST_HEADERS, headers) + ).awaitSingle() + + optionsResponse.status shouldBe HttpStatus.OK + optionsResponse.header(ACCESS_CONTROL_ALLOW_ORIGIN) shouldBe origin + optionsResponse.header(ACCESS_CONTROL_ALLOW_METHODS) shouldBe method.toString() + optionsResponse.headers.getAll(ACCESS_CONTROL_ALLOW_HEADERS).joinToString(",") shouldBe headers + + val response = client.exchange( + GET("/suspend/greet?name=Fred") + .header(ORIGIN, origin) + ).awaitSingle() + + response.status shouldBe HttpStatus.OK + response.header(ACCESS_CONTROL_ALLOW_ORIGIN) shouldBe origin + } + + "test suspend service with retries" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetries"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation1" + response.status shouldBe HttpStatus.OK + } + + "test suspend service with retries blocked" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetriesBlocked"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation2" + response.status shouldBe HttpStatus.OK + } + + "test suspend service with retries without delay" { + val response = client.exchange(GET("/suspend/callSuspendServiceWithRetriesWithoutDelay"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "delayedCalculation3" + response.status shouldBe HttpStatus.OK + } + + "test suspend" { + val response = client.exchange(GET("/suspend/simple"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend calling client" { + val body = suspendClient.simple() + + body shouldBe "Hello" + } + + "test suspend calling client ignore result" { + suspendClient.simpleIgnoreResult() + // No exception thrown + } + + "test suspend calling client method with response return" { + val response = suspendClient.simpleResponse() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend calling client method with response return ignore result" { + val response = suspendClient.simpleResponse() + val body = response.body.get() + + body shouldBe "Hello" + response.status shouldBe HttpStatus.OK + } + + "test suspend delayed" { + val response = client.exchange(GET("/suspend/delayed"), String::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe "Delayed" + response.status shouldBe HttpStatus.OK + } + + "test suspend status" { + val response = client.exchange(GET("/suspend/status"), String::class.java).awaitSingle() + + response.status shouldBe HttpStatus.CREATED + } + + "test suspend status delayed" { + val response = client.exchange(GET("/suspend/statusDelayed"), String::class.java).awaitSingle() + + response.status shouldBe HttpStatus.CREATED + } + + "test suspend invoked once" { + val response = client.exchange(GET("/suspend/count"), Integer::class.java).awaitSingle() + val body = response.body.get() + + body shouldBe 1 + response.status shouldBe HttpStatus.OK + } + + "test error route" { + val ex = shouldThrowExactly { + client.exchange(GET("/suspend/illegal"), String::class.java).awaitSingle() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test error route with client response" { + val ex = shouldThrowExactly { + suspendClient.errorCallResponse() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test error route with client string response" { + val ex = shouldThrowExactly { + suspendClient.errorCall() + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + + "test suspend functions that throw exceptions inside withContext emit an error response to filters" { + val ex = shouldThrowExactly { + client.exchange(GET("/suspend/illegalWithContext"), String::class.java).awaitSingle() + } + val body = ex.response.getBody(String::class.java).get() + val filter = embeddedServer.applicationContext.getBean(SuspendFilter::class.java) + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + filter.response.status shouldBe HttpStatus.BAD_REQUEST + filter.error should { t -> t is IllegalArgumentException } + } + + "test keeping request scope inside coroutine" { + val response = client.exchange(GET("/suspend/keepRequestScopeInsideCoroutine"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeRequestId, beforeThreadId, afterRequestId, afterThreadId) = body.split(',') + beforeRequestId shouldBe afterRequestId + beforeThreadId shouldNotBe afterThreadId + response.status shouldBe HttpStatus.OK + } + + "test keeping request scope after a suspend" { + val response = client.exchange(GET("/suspend/keepRequestScopeAfterSuspend"), String::class.java).awaitSingle() + val body = response.body.get() + val (beforeRequestId, beforeThreadId, afterRequestId, afterThreadId) = body.split(',') + beforeRequestId shouldBe afterRequestId + beforeThreadId shouldNotBe afterThreadId // it will be the default co-routine dispatcher + response.status shouldBe HttpStatus.OK + } + + "test request context is available" { + val response = client.exchange(GET("/suspend/requestContext"), String::class.java).awaitSingle() + val body = response.body.get() + body shouldBe "/suspend/requestContext" + response.status shouldBe HttpStatus.OK + } + + "test request context is available2" { + val response = client.exchange(GET("/suspend/requestContext2"), String::class.java).awaitSingle() + val body = response.body.get() + body shouldBe "test" + response.status shouldBe HttpStatus.OK + } + + "test keeping tracing context after delay" { + val response = client.exchange(GET("/suspend/keepTracingContextAfterDelay"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeTraceId, afterTraceId) = body.split(',') + beforeTraceId shouldBe afterTraceId + response.status shouldBe HttpStatus.OK + } + + "test keeping tracing context inside coroutine" { + val response = client.exchange(GET("/suspend/keepTracingContextInsideCoroutine"), String::class.java).awaitSingle() + val body = response.body.get() + + val (beforeTraceId, afterTraceId) = body.split(',') + beforeTraceId shouldBe afterTraceId + response.status shouldBe HttpStatus.OK + } + +// TODO: HttpCoroutineTracingDispatcherFactory#create should eliminate nulls +// "test keeping tracing context using CoroutineTracingDispatcher explicitly" { +// val response = client.exchange(GET("/suspend/keepTracingContextUsingCoroutineTracingDispatcherExplicitly"), String::class.java).awaitSingle() +// val body = response.body.get() +// +// val (beforeTraceId, afterTraceId) = body.split(',') +// beforeTraceId shouldBe afterTraceId +// response.status shouldBe HttpStatus.OK +// } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt new file mode 100644 index 00000000000..eefc42b8c2a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendFilter.kt @@ -0,0 +1,24 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpAttributes +import io.micronaut.http.HttpRequest +import io.micronaut.http.MutableHttpResponse +import io.micronaut.http.annotation.Filter +import io.micronaut.http.filter.OncePerRequestHttpServerFilter +import io.micronaut.http.filter.ServerFilterChain +import org.reactivestreams.Publisher +import reactor.core.publisher.Flux + +@Filter("/suspend/illegalWithContext") +class SuspendFilter : OncePerRequestHttpServerFilter() { + + lateinit var response: MutableHttpResponse<*> + var error: Throwable? = null + + override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher> { + return Flux.from(chain.proceed(request)).doOnNext { rsp -> + response = rsp + error = rsp.getAttribute(HttpAttributes.EXCEPTION, Throwable::class.java).orElse(null) + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt new file mode 100644 index 00000000000..b22e96d20fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendInterceptor.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import io.micronaut.aop.kotlin.KotlinInterceptedMethod +import jakarta.inject.Singleton + +@InterceptorBean(MyContextInterceptorAnn::class) +@Singleton +class SuspendInterceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + if (interceptedMethod is KotlinInterceptedMethod && interceptedMethod.coroutineContext != null) { + val existingContext = interceptedMethod.coroutineContext[MyContext] + if (existingContext == null) { + interceptedMethod.updateCoroutineContext(interceptedMethod.coroutineContext + MyContext(context.methodName)) + } + } + + interceptedMethod.handleResult( + interceptedMethod.interceptResultAsCompletionStage() + ) + } else { + context.proceed() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt new file mode 100644 index 00000000000..62166d34779 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + + +@Repository +interface SuspendRepository { + + suspend fun get(): String + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt new file mode 100644 index 00000000000..c28ad8a10aa --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositoryInterceptor.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.io.IOException +import java.util.concurrent.CompletableFuture + +@InterceptorBean(Repository::class) +@Singleton +class SuspendRepositoryInterceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext?): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + var cf = CompletableFuture() + cf.complete("hello") + cf = cf.thenApply { + throw IOException() + } + interceptedMethod.handleResult(cf) + } else { + context?.proceed() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt new file mode 100644 index 00000000000..5850fdc7ab1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRepositorySpec.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.should +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders.* +import io.micronaut.http.HttpMethod +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.HttpRequest.OPTIONS +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.reactive.awaitSingle +import java.io.IOException + +class SuspendRepositorySpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var suspendRepository = context.getBean(SuspendRepository::class.java) + + init { + "test exception unwrapped" { + shouldThrow { + suspendRepository.get() + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt new file mode 100644 index 00000000000..5beff87fc89 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendRequestScopedService.kt @@ -0,0 +1,9 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.runtime.http.scope.RequestScope +import java.util.* + +@RequestScope +open class SuspendRequestScopedService { + open val requestId = UUID.randomUUID().toString() +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt new file mode 100644 index 00000000000..e332f5b8ec5 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendService.kt @@ -0,0 +1,90 @@ +package io.micronaut.docs.server.suspend + +import io.micronaut.http.HttpRequest +import io.micronaut.http.context.ServerRequestContext +import io.micronaut.retry.annotation.Retryable +import kotlinx.coroutines.delay +import jakarta.inject.Singleton +import kotlin.coroutines.coroutineContext + +@Singleton +open class SuspendService( + private val suspendRequestScopedService: SuspendRequestScopedService +) { + var counter1: Int = 0 + var counter2: Int = 0 + var counter3: Int = 0 + var counter4: Int = 0 + + @Retryable + open suspend fun delayedCalculation1(): String { + if (counter1 != 2) { + delay(1) + counter1++ + throw RuntimeException("error $counter1") + } + delay(1) + return "delayedCalculation1" + } + + @Retryable + open suspend fun delayedCalculation2(): String { + if (counter2 != 2) { + delay(1) + counter2++ + throw RuntimeException("error $counter2") + } + delay(1) + return "delayedCalculation2" + } + + @Retryable + open suspend fun calculation3(): String { + if (counter3 != 2) { + counter3++ + throw RuntimeException("error $counter3") + } + return "delayedCalculation3" + } + + @Retryable + open suspend fun requestScopedCalculation(): String { + if (counter4 != 2) { + counter4++ + throw RuntimeException("error $counter4") + } + return "${suspendRequestScopedService.requestId},${Thread.currentThread().id}" + } + + suspend fun requestContext(): String { + delay(1) + // called from a suspend controller function + val currentRequest = ServerRequestContext.currentRequest>().orElseGet { + error("Expected a current http server request") + } + return currentRequest.path + } + + suspend fun findMyContextValue(): String? { + return coroutineContext[MyContext]?.value + } + + @MyContextInterceptorAnn + open suspend fun call1(): String? { + return findMyContextValue() + } + + @MyContextInterceptorAnn + open suspend fun call2(): String? { + return call1() + } + + open suspend fun call3(): String? { + return call1() + } + + @MyContextInterceptorAnn + open suspend fun call4(): String? { + return call3() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt new file mode 100644 index 00000000000..3ac9ac0d393 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/SuspendServiceInterceptorSpec.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext + +class SuspendServiceInterceptorSpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var suspendService = context.getBean(SuspendService::class.java) + + init { + "should append to context " { + coroutineContext[MyContext] shouldBe null + + suspendService.call1() shouldBe "call1" + suspendService.call2() shouldBe "call2" + suspendService.call3() shouldBe "call1" + suspendService.call4() shouldBe "call4" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt new file mode 100644 index 00000000000..46583c3a2c0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CoroutineCrudRepository.kt @@ -0,0 +1,101 @@ +package io.micronaut.docs.server.suspend.multiple + +import kotlinx.coroutines.flow.Flow + +interface CoroutineCrudRepository { + + /** + * Saves the given valid entity, returning a possibly new entity representing the saved state. Note that certain implementations may not be able to detect whether a save or update should be performed and may always perform an insert. The [.update] method can be used in this case to explicitly request an update. + * + * @param entity The entity to save. Must not be null. + * @return The saved entity will never be null. + * @param The generic type + */ + suspend fun save(entity: S): S + + /** + * This method issues an explicit update for the given entity. The method differs from [.save] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entity The entity to save. Must not be null. + * @return The updated entity will never be null. + * @param The generic type + */ + suspend fun update(entity: S): S + + /** + * This method issues an explicit update for the given entities. The method differs from [.saveAll] in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown. + * + * @param entities The entities to update. Must not be null. + * @return The updated entities will never be null. + * @param The generic type + */ + fun updateAll(entities: Iterable): Flow + + /** + * Saves all given entities, possibly returning new instances representing the saved state. + * + * @param entities The entities to saved. Must not be null. + * @param The generic type + * @return The saved entities objects. will never be null. + */ + fun saveAll(entities: Iterable): Flow + + /** + * Retrieves an entity by its id. + * + * @param id The ID of the entity to retrieve. Must not be null. + * @return the entity with the given id or none. + */ + suspend fun findById(id: ID): E? + + /** + * Returns whether an entity with the given id exists. + * + * @param id must not be null. + * @return true if an entity with the given id exists, false otherwise. + */ + suspend fun existsById(id: ID): Boolean + + /** + * Returns all instances of the type. + * + * @return all entities + */ + fun findAll(): Flow + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + suspend fun count(): Long + + /** + * Deletes the entity with the given id. + * + * @param id the id. + */ + suspend fun deleteById(id: ID): Int + + /** + * Deletes a given entity. + * + * @param entity The entity to delete + * @return the number of entities deleted + */ + suspend fun delete(entity: E): Int + + /** + * Deletes the given entities. + * + * @param entities The entities to delete + * @return the number of entities deleted + */ + suspend fun deleteAll(entities: Iterable): Int + + /** + * Deletes all entities managed by the repository. + * @return the number of entities deleted + */ + suspend fun deleteAll(): Int +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt new file mode 100644 index 00000000000..375d95059d9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/CustomRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +@MyRepository +interface CustomRepository : CoroutineCrudRepository { + + // As of Kotlin version 1.7.20 and KAPT, this will generate JVM signature: "SomeEntity findById(long id, continuation)" + override suspend fun findById(id: Long): SomeEntity? + + suspend fun xyz(): String + + suspend fun abc(): String + + suspend fun count1(): String + + suspend fun count2(): String + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt new file mode 100644 index 00000000000..1e3b97db72e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/InterceptorSpec.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.ints.shouldBeExactly +import io.kotest.matchers.shouldBe +import io.micronaut.context.ApplicationContext +import kotlinx.coroutines.runBlocking + +class InterceptorSpec : StringSpec() { + + val context = autoClose( + ApplicationContext.run() + ) + + private var myService = context.getBean(MyService::class.java) + + private var repository = context.getBean(CustomRepository::class.java) + + init { + "test correct interceptors calls" { + runBlocking { + MyService.events.clear() + myService.someCall() + MyService.events.size shouldBeExactly 8 + MyService.events[0] shouldBe "intercept1-start" + MyService.events[1] shouldBe "intercept2-start" + MyService.events[2] shouldBe "repository-abc" + MyService.events[3] shouldBe "repository-xyz" + MyService.events[4] shouldBe "intercept2-end" + MyService.events[5] shouldBe "intercept1-end" + MyService.events[6] shouldBe "repository-count1" + MyService.events[7] shouldBe "repository-count2" + } + } + + "test calling generic method" { + runBlocking { + MyService.events.clear() + // Validate that no bytecode error is produced + repository.findById(111) + MyService.events.size shouldBeExactly 1 + MyService.events[0] shouldBe "repository-findById" + } + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt new file mode 100644 index 00000000000..55ed35fee4d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepository.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Introduction +import jakarta.inject.Singleton + +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +@Introduction +@Singleton +annotation class MyRepository() diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt new file mode 100644 index 00000000000..0590cfeddad --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyRepositoryInterceptorImpl.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.io.IOException +import java.util.concurrent.CompletableFuture + +@InterceptorBean(MyRepository::class) +@Singleton +class MyRepositoryInterceptorImpl : MethodInterceptor { + override fun intercept(context: MethodInvocationContext?): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("repository-" + context!!.methodName) + val cf: CompletableFuture = CompletableFuture.supplyAsync{ + Thread.sleep(1000) + context!!.methodName + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt new file mode 100644 index 00000000000..7da2e746538 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/MyService.kt @@ -0,0 +1,35 @@ +package io.micronaut.docs.server.suspend.multiple + +import jakarta.inject.Singleton +import java.util.* +import kotlin.collections.ArrayList + +@Singleton +open class MyService( + private val repository: CustomRepository +) { + + companion object { + val events: MutableList = Collections.synchronizedList(ArrayList()) + } + + open suspend fun someCall() { + // Simulate accessing two different data-source repositories using two transactions + tx1() + // Call another coroutine + repository.count1() + repository.count2() + } + + @Transaction1 + open suspend fun tx1() { + tx2() + } + + @Transaction2 + open suspend fun tx2() { + repository.abc() + repository.xyz() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt new file mode 100644 index 00000000000..68b7aa896c9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/SomeEntity.kt @@ -0,0 +1,4 @@ +package io.micronaut.docs.server.suspend.multiple + +class SomeEntity { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt new file mode 100644 index 00000000000..60722c0556e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction1 diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt new file mode 100644 index 00000000000..d48d3afea70 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction1Interceptor.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture +import java.util.function.BiConsumer + +@InterceptorBean(Transaction1::class) +@Singleton +class Transaction1Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept1-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept1-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt new file mode 100644 index 00000000000..d32723cf1cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.Around +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FILE +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER +import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER + +@MustBeDocumented +@Retention(RUNTIME) +@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) +@Around +annotation class Transaction2 diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt new file mode 100644 index 00000000000..7fe950117e7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/suspend/multiple/Transaction2Interceptor.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.suspend.multiple + +import io.micronaut.aop.InterceptedMethod +import io.micronaut.aop.InterceptorBean +import io.micronaut.aop.MethodInterceptor +import io.micronaut.aop.MethodInvocationContext +import jakarta.inject.Singleton +import java.util.concurrent.CompletableFuture + +@InterceptorBean(Transaction2::class) +@Singleton +class Transaction2Interceptor : MethodInterceptor { + override fun intercept(context: MethodInvocationContext): Any? { + val interceptedMethod = InterceptedMethod.of(context) + return try { + return if (interceptedMethod.resultType() == InterceptedMethod.ResultType.COMPLETION_STAGE) { + MyService.events.add("intercept2-start") + val completionStage = interceptedMethod.interceptResultAsCompletionStage() + val cf = CompletableFuture() + completionStage.whenComplete { value, throwable -> + MyService.events.add("intercept2-end") + if (throwable == null) { + cf.complete(value) + } else { + cf.completeExceptionally(throwable) + } + } + interceptedMethod.handleResult(cf) + } else { + throw IllegalStateException() + } + } catch (e: Exception) { + interceptedMethod.handleException(e) + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt new file mode 100644 index 00000000000..0638eb00cd7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/BytesUploadController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths + +@Controller("/upload") +class BytesUploadController { + + @Post(value = "/bytes", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadBytes(file: ByteArray, fileName: String): HttpResponse { // <2> + return try { + val tempFile = File.createTempFile(fileName, "temp") + val path = Paths.get(tempFile.absolutePath) + Files.write(path, file) // <3> + HttpResponse.ok("Uploaded") + } catch (e: IOException) { + HttpResponse.badRequest("Upload Failed") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt new file mode 100644 index 00000000000..9b6d637f690 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/CompletedUploadController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.CompletedFileUpload +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths + +@Controller("/upload") +class CompletedUploadController { + + @Post(value = "/completed", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadCompleted(file: CompletedFileUpload): HttpResponse { // <2> + return try { + val tempFile = File.createTempFile(file.filename, "temp") //<3> + val path = Paths.get(tempFile.absolutePath) + Files.write(path, file.bytes) //<3> + HttpResponse.ok("Uploaded") + } catch (e: IOException) { + HttpResponse.badRequest("Upload Failed") + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt new file mode 100644 index 00000000000..8ef22b66d05 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadController.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.core.async.annotation.SingleResult +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus.CONFLICT +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.StreamingFileUpload +import org.reactivestreams.Publisher +import reactor.core.publisher.Mono +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.OutputStream + +@Controller("/upload") +class UploadController { +// end::class[] + + // tag::file[] + @Post(value = "/", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun upload(file: StreamingFileUpload): Mono> { // <2> + + val tempFile = File.createTempFile(file.filename, "temp") + val uploadPublisher = file.transferTo(tempFile) // <3> + + return Mono.from(uploadPublisher) // <4> + .map { success -> + if (success) { + HttpResponse.ok("Uploaded") + } else { + HttpResponse.status(CONFLICT) + .body("Upload Failed") + } + } + } + // end::file[] + + // tag::outputStream[] + @Post(value = "/outputStream", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + @SingleResult + fun uploadOutputStream(file: StreamingFileUpload): Mono> { // <2> + val outputStream = ByteArrayOutputStream() // <3> + val uploadPublisher = file.transferTo(outputStream) // <4> + + return Mono.from(uploadPublisher) // <5> + .map { success: Boolean -> + return@map if (success) { + HttpResponse.ok("Uploaded") + } else { + HttpResponse.status(CONFLICT) + .body("Upload Failed") + } + } + } + // end::outputStream[] + +// tag::endclass[] +} +// end::endclass] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt new file mode 100644 index 00000000000..049f6a3b3e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/UploadControllerSpec.kt @@ -0,0 +1,172 @@ +package io.micronaut.docs.server.upload + +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class UploadControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test file upload"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test file upload outputstream"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/outputStream", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload with filename but no bytes"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + + "test completed file upload with no name but with bytes"() { + val body = MultipartBody.builder() + .addPart("file", "", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test completed file upload with no filename and no bytes"() { + val body = MultipartBody.builder() + .addPart("file", "", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test completed file upload with no part"() { + val body = MultipartBody.builder() + .addPart("filex", "", MediaType.APPLICATION_JSON_TYPE, ByteArray(0)) + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/completed", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val ex = shouldThrow { flowable.blockFirst() } + val response = ex.response + val embedded: Map<*, *> = response.getBody(Map::class.java).get().get("_embedded") as Map<*, *> + val message = ((embedded.get("errors") as java.util.List<*>).get(0) as Map<*, *>).get("message") + + message shouldBe "Required argument [CompletedFileUpload file] not specified" + } + + "test file bytes uploaded"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.TEXT_PLAIN_TYPE, "some data".toByteArray()) + .addPart("fileName", "bar") + .build() + + val flowable = Flux.from(client!!.exchange( + HttpRequest.POST("/upload/bytes", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + String::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe "Uploaded" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt new file mode 100644 index 00000000000..fdb5577644b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/upload/WholeBodyUploadController.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.server.upload + +// tag::class[] +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.CompletedFileUpload +import io.micronaut.http.multipart.CompletedPart +import io.micronaut.http.server.multipart.MultipartBody +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import reactor.core.publisher.Mono + +@Controller("/upload") +class WholeBodyUploadController { + + @Post(value = "/whole-body", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // <1> + fun uploadBytes(@Body body: MultipartBody): Mono { // <2> + return Mono.create { emitter -> + body.subscribe(object : Subscriber { + private var s: Subscription? = null + + override fun onSubscribe(s: Subscription) { + this.s = s + s.request(1) + } + + override fun onNext(completedPart: CompletedPart) { + val partName = completedPart.name + if (completedPart is CompletedFileUpload) { + val originalFileName = completedPart.filename + } + } + + override fun onError(t: Throwable) { + emitter.error(t) + } + + override fun onComplete() { + emitter.success("Uploaded") + } + }) + } + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt new file mode 100644 index 00000000000..0a70ad7d1ba --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/server/uris/UriTemplateTest.kt @@ -0,0 +1,19 @@ +package io.micronaut.docs.server.uris + +import io.micronaut.http.uri.UriMatchTemplate +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class UriTemplateTest { + + @Test + fun testUriTemplate() { + // tag::match[] + val template = UriMatchTemplate.of("/hello/{name}") + + assertTrue(template.match("/hello/John").isPresent) // <1> + assertEquals("/hello/John", template.expand(mapOf("name" to "John"))) // <2> + // end::match[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt new file mode 100644 index 00000000000..fc92e9bdfc9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/Cart.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.session + +import java.util.ArrayList + +class Cart { + + var items: MutableList = ArrayList() +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt new file mode 100644 index 00000000000..b940093265c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingController.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.session + +// tag::imports[] +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Post +import io.micronaut.session.Session +import io.micronaut.session.annotation.SessionValue +// end::imports[] + +// tag::class[] +@Controller("/shopping") +class ShoppingController { + + companion object { + private const val ATTR_CART = "cart" // <1> + } +// end::class[] + + // tag::view[] + @Get("/cart") + @SessionValue(ATTR_CART) // <1> + internal fun viewCart(@SessionValue cart: Cart?): Cart { // <2> + return cart ?: Cart() + } + // end::view[] + + // tag::add[] + @Post("/cart/{name}") + internal fun addItem(session: Session, name: String): Cart { // <2> + require(name.isNotBlank()) { "Name cannot be blank" } + val cart = session.get(ATTR_CART, Cart::class.java).orElseGet { // <3> + val newCart = Cart() + session.put(ATTR_CART, newCart) // <4> + newCart + } + cart.items.add(name) + return cart + } + // end::add[] + + // tag::clear[] + @Post("/cart/clear") + internal fun clearCart(session: Session?) { + session?.remove(ATTR_CART) + } + // end::clear[] + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt new file mode 100644 index 00000000000..a4d46c90e3d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/session/ShoppingControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.docs.session + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpHeaders +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux +import kotlin.test.assertNotNull + +class ShoppingControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + "testSessionValueUsedOnReturnValue" { + // tag::view[] + var response = Flux.from(client.exchange(HttpRequest.GET("/shopping/cart"), Cart::class.java)) // <1> + .blockFirst() + var cart = response.body() + + assertNotNull(response.header(HttpHeaders.AUTHORIZATION_INFO)) // <2> + assertNotNull(cart) + cart.items.isEmpty() + // end::view[] + + // tag::add[] + val sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) // <1> + + response = Flux.from(client.exchange(HttpRequest.POST("/shopping/cart/Apple", "") + .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart::class.java)) // <2> + .blockFirst() + cart = response.body() + // end::add[] + + assertNotNull(cart) + cart.items.size shouldBe 1 + + response = Flux.from(client.exchange(HttpRequest.GET("/shopping/cart") + .header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart::class.java)) + .blockFirst() + cart = response.body() + + response.header(HttpHeaders.AUTHORIZATION_INFO) + assertNotNull(cart) + + cart.items.size shouldBe 1 + cart.items[0] shouldBe "Apple" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt new file mode 100644 index 00000000000..79ae5cc15cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineClient.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.sse + +import io.micronaut.docs.streaming.Headline +import io.micronaut.http.MediaType.TEXT_EVENT_STREAM +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.sse.Event +import reactor.core.publisher.Flux + +// tag::class[] +@Client("/streaming/sse") +interface HeadlineClient { + + @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) + fun streamHeadlines(): Flux> +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt new file mode 100644 index 00000000000..ff188b4a19c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineController.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.sse + +import io.micronaut.docs.streaming.Headline +import io.micronaut.http.MediaType.TEXT_EVENT_STREAM +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.sse.Event +import reactor.core.publisher.Flux +import reactor.core.publisher.FluxSink +import java.time.Duration +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit + +@Controller("/streaming/sse") +class HeadlineController { + + // tag::streaming[] + @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) // <1> + internal fun streamHeadlines(): Flux> { + return Flux.create>( { emitter -> // <2> + val headline = Headline() + headline.text = "Latest Headline at ${ZonedDateTime.now()}" + emitter.next(Event.of(headline)) + emitter.complete() + }, FluxSink.OverflowStrategy.BUFFER) + .repeat(100) // <3> + .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // <4> + } + // end::streaming[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..87f1ef38b02 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/sse/HeadlineControllerSpec.kt @@ -0,0 +1,30 @@ +package io.micronaut.docs.sse + +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.server.EmbeddedServer + +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue + +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + init { + // tag::streamingClient[] + "test client annotations streaming" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineClient::class.java) + + val headline = headlineClient.streamHeadlines().blockFirst() + + assertNotNull(headline) + assertTrue(headline!!.data.text!!.startsWith("Latest Headline")) + } + // end::streamingClient[] + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt new file mode 100644 index 00000000000..5f2a2c5b39f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/Headline.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +class Headline { + var text: String? = null +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt new file mode 100644 index 00000000000..5e45cb07008 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineClient.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import reactor.core.publisher.Flux + +// end::imports[] + +// tag::class[] +@Client("/streaming") +interface HeadlineClient { + + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + fun streamHeadlines(): Flux // <2> +// end::class[] + + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + fun streamFlux(): Flux + +// tag::endclass[] +} +// end::endclass[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt new file mode 100644 index 00000000000..e98cf98a1e0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineController.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.time.Duration +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeUnit.SECONDS +// end::imports[] + +@Controller("/streaming") +class HeadlineController { + + // tag::streaming[] + @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // <1> + internal fun streamHeadlines(): Flux { + return Mono.fromCallable { // <2> + val headline = Headline() + headline.text = "Latest Headline at ${ZonedDateTime.now()}" + headline + }.repeat(100) // <3> + .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // <4> + } + // end::streaming[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt new file mode 100644 index 00000000000..d8542fb7ed1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineControllerSpec.kt @@ -0,0 +1,80 @@ +package io.micronaut.docs.streaming + +import io.kotest.matchers.shouldNotBe +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldStartWith +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest.GET +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.StreamingHttpClient +import io.micronaut.runtime.server.EmbeddedServer +import org.junit.Assert.fail +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +class HeadlineControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + // tag::streamingClient[] + "test client annotation streaming" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineClient::class.java) // <1> + + val firstHeadline = headlineClient.streamHeadlines().next() // <2> + + val headline = firstHeadline.block() // <3> + + headline shouldNotBe null + headline.text shouldStartWith "Latest Headline" + } + // end::streamingClient[] + + "test streaming client" { + val client = embeddedServer.applicationContext.createBean( + StreamingHttpClient::class.java, embeddedServer.url) + + // tag::streaming[] + val headlineStream = client.jsonStream( + GET("/streaming/headlines"), Headline::class.java) // <1> + val future = CompletableFuture() // <2> + headlineStream.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + s.request(1) // <3> + } + + override fun onNext(headline: Headline) { + println("Received Headline = ${headline.text!!}") + future.complete(headline) // <4> + } + + override fun onError(t: Throwable) { + future.completeExceptionally(t) // <5> + } + + override fun onComplete() { + // no-op // <6> + } + }) + // end::streaming[] + + try { + val headline = future.get(3, TimeUnit.SECONDS) + headline.text shouldStartWith "Latest Headline" + } catch (e: Throwable) { + fail("Asynchronous error occurred: " + (e.message ?: e.javaClass.simpleName)) + } + client.stop() + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt new file mode 100644 index 00000000000..f87782b7b30 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowClient.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import kotlinx.coroutines.flow.Flow +// end::imports[] + +// tag::class[] +@Client("/streaming") +interface HeadlineFlowClient { + // end::class[] + + // tag::streamingWithFlow[] + @Get(value = "/headlinesWithFlow", processes = [MediaType.APPLICATION_JSON_STREAM]) // <1> + fun streamFlow(): Flow // <2> + // tag::streamingWithFlow[] +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt new file mode 100644 index 00000000000..17bbfea7d3f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.streaming + +// tag::imports[] +import io.micronaut.http.HttpResponse +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Error +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Produces +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.time.ZonedDateTime +// end::imports[] + +@Controller("/streaming") +class HeadlineFlowController { + + // tag::streamingWithFlow[] + @Get(value = "/headlinesWithFlow", processes = [MediaType.APPLICATION_JSON_STREAM]) + internal fun streamHeadlinesWithFlow(): Flow = // <1> + flow { // <2> + repeat(100) { // <3> + with (Headline()) { + text = "Latest Headline at ${ZonedDateTime.now()}" + emit(this) // <4> + delay(1_000) // <5> + } + } + } + // end::streamingWithFlow[] + + @Get(value = "/illegal") + fun illegal(): Any = throw IllegalArgumentException() + + @Error(exception = IllegalArgumentException::class) + @Produces(MediaType.TEXT_PLAIN) + fun onIllegalArgument(e: IllegalArgumentException): Flow> = flow { + emit(HttpResponse.badRequest("illegal.argument")) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt new file mode 100644 index 00000000000..a8fe98a5d31 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/streaming/HeadlineFlowControllerSpec.kt @@ -0,0 +1,55 @@ +package io.micronaut.docs.streaming + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.annotation.Ignored +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.string.shouldStartWith +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList + +// Flow converters moved to Kotlin Module re-enable once +// new version of micronaut-kotlin-runtime is published +@Ignored +class HeadlineFlowControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url) + ) + + init { + // tag::streamingClientWithFlow[] + "test client annotation streaming with Flow" { + val headlineClient = embeddedServer + .applicationContext + .getBean(HeadlineFlowClient::class.java) + + val headline = headlineClient.streamFlow().take(1).toList().first() + + headline shouldNotBe null + headline.text shouldStartWith "Latest Headline" + } + // end::streamingClientWithFlow[] + + "test error route with Flow" { + val ex = shouldThrowExactly { + client.toBlocking().exchange(HttpRequest.GET("/streaming/illegal"), String::class.java) + } + val body = ex.response.getBody(String::class.java).get() + + ex.status shouldBe HttpStatus.BAD_REQUEST + body shouldBe "illegal.argument" + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt new file mode 100644 index 00000000000..4bf2b402cf6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/web/router/version/VersionedController.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.web.router.version + +// tag::imports[] +import io.micronaut.core.version.annotation.Version +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +// end::imports[] + +// tag::clazz[] +@Controller("/versioned") +internal class VersionedController { + + @Version("1") // <1> + @Get("/hello") + fun helloV1(): String { + return "helloV1" + } + + @Version("2") // <2> + @Get("/hello") + fun helloV2(): String { + return "helloV2" + } + // end::clazz[] + + @Version("2") + @Get("/hello") + fun duplicatedHelloV2(): String { + return "duplicatedHelloV2" + } + + @Get("/hello") + fun hello(): String { + return "hello" + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt new file mode 100644 index 00000000000..43a081412dd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/whatsNew/CacheFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.whatsNew + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +import javax.cache.CacheManager +import javax.cache.Caching +import javax.cache.configuration.MutableConfiguration +import jakarta.inject.Singleton +// end::imports[] + +// tag::class[] +@Factory +class CacheFactory { + + @Singleton + fun cacheManager(): CacheManager { + val cacheManager = Caching.getCachingProvider().cacheManager + cacheManager.createCache("my-cache", MutableConfiguration()) + return cacheManager + } +} +// end::class[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt new file mode 100644 index 00000000000..2dfb1457f14 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/docs/writable/TemplateController.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.docs.writable + +//tag::imports[] +import groovy.text.SimpleTemplateEngine +import groovy.text.Template +import io.micronaut.core.io.Writable +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.server.exceptions.HttpServerException +import java.io.Writer +//end::imports[] + +//tag::clazz[] +@Controller("/template") +class TemplateController { + + private val templateEngine = SimpleTemplateEngine() + private val template = initTemplate() // <1> + + @Get(value = "/welcome", produces = [MediaType.TEXT_PLAIN]) + internal fun render(): Writable { // <2> + return { writer: Writer -> + template.make( // <3> + mapOf( + "firstName" to "Fred", + "lastName" to "Flintstone" + ) + ).writeTo(writer) + } as Writable + } + + private fun initTemplate(): Template { + return try { + templateEngine.createTemplate( + "Dear \$firstName \$lastName. Nice to meet you." + ) + } catch (e: Exception) { + throw HttpServerException("Cannot create template") + } + } +} +//end::clazz[] diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt new file mode 100644 index 00000000000..8e85dfe9f45 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/GreetingClient.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client + +// tests that nullable can compile +// issue: https://github.com/micronaut-projects/micronaut-core/issues/1080 +@Client("/") +interface GreetingClient { + @Get("/greeting{?name}") + fun greet(name : String? ) : String +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt new file mode 100644 index 00000000000..a4eba50ab56 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClient.kt @@ -0,0 +1,19 @@ +package io.micronaut.http.client + +import io.micronaut.http.HttpResponse +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Put +import io.micronaut.http.client.annotation.Client + +@Client("/") +interface SuspendClient { + + @Put + suspend fun call(newState: String): String + + @Get + suspend fun notFound(): HttpResponse + + @Get + suspend fun notFoundWithoutHttpResponseWrapper(): String? +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt new file mode 100644 index 00000000000..891080133cf --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientController.kt @@ -0,0 +1,24 @@ +package io.micronaut.http.client + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Put +import kotlinx.coroutines.delay + +@Requires(property = "spec.name", value = "SuspendClientSpec") +@Controller +class SuspendClientController { + + @Put + fun echo(@Body body: String): String { + return body + } + + @Get + suspend fun notFound(): String? { + delay(1) + return null + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt new file mode 100644 index 00000000000..606cee07790 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/client/SuspendClientSpec.kt @@ -0,0 +1,45 @@ +package io.micronaut.http.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpStatus +import io.micronaut.runtime.server.EmbeddedServer +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class SuspendClientSpec { + + @Test + fun testSuspendClientBody() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).call("test") + } + + Assertions.assertEquals(response, "{\"newState\":\"test\"}") + } + + @Test + fun testNotFound() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).notFound() + } + + Assertions.assertEquals(response.status, HttpStatus.NOT_FOUND) + } + + @Test + fun testNotFoundWithoutHttpResponseWrapper() { + val server = ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to "SuspendClientSpec")) + val ctx = server.applicationContext + val response = runBlocking { + ctx.getBean(SuspendClient::class.java).notFoundWithoutHttpResponseWrapper() + } + + Assertions.assertNull(response) + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt new file mode 100644 index 00000000000..cf6d7eb541b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/WebSocketSuspendTest.kt @@ -0,0 +1,65 @@ +package io.micronaut.http.server + +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import io.micronaut.websocket.WebSocketClient +import io.micronaut.websocket.WebSocketSession +import io.micronaut.websocket.annotation.ClientWebSocket +import io.micronaut.websocket.annotation.OnMessage +import io.micronaut.websocket.annotation.ServerWebSocket +import jakarta.inject.Inject +import kotlinx.coroutines.delay +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import reactor.core.publisher.Flux +import spock.lang.Issue + +@MicronautTest +@Property(name = "spec.name", value = "WebSocketSuspendTest") +class WebSocketSuspendTest { + @Inject + lateinit var server: EmbeddedServer + + @Inject + lateinit var client: WebSocketClient + + @Issue("https://github.com/micronaut-projects/micronaut-core/issues/6582") + @Test + @Timeout(10) + fun test() { + val cl = Flux.from(client.connect(TestWebSocketClient::class.java, server.uri.toString() + "/demo/ws")).blockFirst()!! + cl.send("foo") + while (true) { + Thread.sleep(100) + if (cl.received == "foo") { + break + } + } + cl.close() + } + + @Requires(property = "spec.name", value = "WebSocketSuspendTest") + @ServerWebSocket("/demo/ws") + class TestWebSocketController { + @OnMessage + suspend fun messageHandler(message: String, session: WebSocketSession) { + delay(100) + session.sendSync(message) + } + } + + @Requires(property = "spec.name", value = "WebSocketSuspendTest") + @ClientWebSocket("/demo/ws") + abstract class TestWebSocketClient : AutoCloseable { + var received: String = "" + + abstract fun send(msg: String) + + @OnMessage + fun onMessage(msg: String) { + this.received = msg + } + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt new file mode 100644 index 00000000000..810c607289f --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadController.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.upload + +import io.micronaut.context.annotation.Requires +import io.micronaut.http.MediaType.MULTIPART_FORM_DATA +import io.micronaut.http.MediaType.TEXT_PLAIN +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Post +import io.micronaut.http.multipart.StreamingFileUpload +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.reduce +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import reactor.core.publisher.Flux + +@Requires(property = "spec.name", value = "KotlinUploadControllerSpec") +@Controller("/upload") +class KotlinUploadController { + + @Post(value = "/flow", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) + suspend fun uploadFlow(file: StreamingFileUpload): Int { + return file.asFlow().map { it.bytes.size }.reduce { accumulator, value -> accumulator + value } + } + + @Post(value = "/await", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) + suspend fun uploadAwaitFlux(file: StreamingFileUpload): Int { + return Flux.from(file).map { it.bytes.size }.reduce { accumulator, value -> accumulator + value }.awaitFirstOrNull() ?: 0 + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt new file mode 100644 index 00000000000..dbec91da936 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/http/server/upload/KotlinUploadControllerSpec.kt @@ -0,0 +1,59 @@ +package io.micronaut.http.server.upload + +import io.kotest.matchers.shouldBe +import io.kotest.core.spec.style.StringSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.runtime.server.EmbeddedServer +import reactor.core.publisher.Flux + +class KotlinUploadControllerSpec: StringSpec() { + + val embeddedServer = autoClose( + ApplicationContext.run(EmbeddedServer::class.java, mapOf("spec.name" to KotlinUploadControllerSpec::class.simpleName)) + ) + + val client = autoClose( + embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.getURL()) + ) + + init { + "test file upload with kotlin flow"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/flow", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + Int::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe 15 + } + + "test file upload with kotlin await"() { + val body = MultipartBody.builder() + .addPart("file", "file.json", MediaType.APPLICATION_JSON_TYPE, "{\"title\":\"Foo\"}".toByteArray()) + .build() + + val flowable = Flux.from(client.exchange( + HttpRequest.POST("/upload/await", body) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.TEXT_PLAIN_TYPE), + Int::class.java + )) + val response = flowable.blockFirst() + + response.status() shouldBe HttpStatus.OK + response.body.get() shouldBe 15 + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt new file mode 100644 index 00000000000..eb7938182f1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt new file mode 100644 index 00000000000..e6b0bfe3ab4 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/B.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import jakarta.inject.Inject + +class B @Inject +internal constructor(val a: A?) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt new file mode 100644 index 00000000000..3a512199411 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/C.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import jakarta.inject.Inject + +class C @Inject +internal constructor(val a: A) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt new file mode 100644 index 00000000000..0a880cb9a61 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/constructor/nullableinjection/ConstructorNullableInjectionSpec.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.constructor.nullableinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.DependencyInjectionException +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test + +class ConstructorNullableInjectionSpec { + + @Test + fun testNullableInjectionInConstructor() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + + context.close() + } + + @Test + fun testNormalInjectionStillFails() { + val context = BeanContext.run() + try { + context.getBean(C::class.java) + fail("Expected a DependencyInjectionException to be thrown") + } catch (e: DependencyInjectionException) {} + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt new file mode 100644 index 00000000000..49b37cb5f33 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt new file mode 100644 index 00000000000..2534aac70b3 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/B.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class B { + internal var a: A? = null + @Inject set +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt new file mode 100644 index 00000000000..262179d12da --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/field/nullableinjection/FieldNullableInjectionSpec.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.field.nullableinjection + +import io.micronaut.context.BeanContext +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +class FieldNullableInjectionSpec { + + @Test + fun testNullableFieldInjection() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + context.close() + } + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt new file mode 100644 index 00000000000..871413bc78a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/A.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +interface A diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt new file mode 100644 index 00000000000..ef4bce0bb00 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/B.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class B { + internal var a: A? = null + @Inject set +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt new file mode 100644 index 00000000000..3dc10b3e7e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import jakarta.inject.Inject +import jakarta.inject.Singleton + + +@Singleton +class C { + internal var _a: A? = null + internal var a: A + get() = _a!! + @Inject set(value) { _a = value; } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt new file mode 100644 index 00000000000..52099874c0d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/method/nullableinjection/SetterWithNullableSpec.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.method.nullableinjection + +import io.micronaut.context.BeanContext +import io.micronaut.context.exceptions.DependencyInjectionException +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.Test + +class SetterWithNullableSpec { + + @Test + fun testInjectionOfNullableObjects() { + val context = BeanContext.run() + val b = context.getBean(B::class.java) + assertNull(b.a) + context.close() + } + + @Test + fun testNormalInjectionStillFails() { + val context = BeanContext.run() + try { + context.getBean(C::class.java) + fail("Expected a DependencyInjectionException to be thrown") + } catch (e: DependencyInjectionException) {} + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt new file mode 100644 index 00000000000..1dfeed93488 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/BeanWithProperty.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.annotation.Property +import io.micronaut.core.convert.format.MapFormat +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +class BeanWithProperty { + + @set:Inject + @setparam:Property(name="app.string") + var stringParam:String ?= null + + @set:Inject + @setparam:Property(name="app.map") + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParam:Map ?= null + + @Property(name="app.string") + var stringParamTwo:String ?= null + + @Property(name="app.map") + @MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var mapParamTwo:Map ?= null +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt new file mode 100644 index 00000000000..16e6396507b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/ConfigProps.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.annotation.ConfigurationProperties +import io.micronaut.core.convert.format.MapFormat + +@ConfigurationProperties("test") +class ConfigProps { + + @setparam:MapFormat(transformation = MapFormat.MapTransformation.FLAT) + var properties: Map? = null + + var otherProperties: Map? = null + + private var setterProperties: Map? = null + + fun setSetterProperties(setterProperties: Map) { + this.setterProperties = setterProperties + } + + fun getSetterProperties() = setterProperties +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt new file mode 100644 index 00000000000..6f15c59ded0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/MapFormatSpec.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.property + +import io.micronaut.context.ApplicationContext +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertTrue + + +class MapFormatSpec { + + @Test + fun testMapFormatOnProperty() { + val context = ApplicationContext.run(mapOf("text.properties.yyy.zzz" to 3, "test.properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertEquals(config.properties?.get("yyy.xxx"), 2) + context.close() + } + + @Test + fun testMapProperty() { + val context = ApplicationContext.run(mapOf("text.other-properties.yyy.zzz" to 3, "test.other-properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertTrue(config.otherProperties?.containsKey("yyy") ?: false) + context.close() + } + + @Test + fun testMapPropertySetter() { + val context = ApplicationContext.run(mapOf("text.setter-properties.yyy.zzz" to 3, "test.setter-properties.yyy.xxx" to 2, "test.properties.yyy.yyy" to 3)) + val config = context.getBean(ConfigProps::class.java) + assertTrue(config.getSetterProperties()?.containsKey("yyy") ?: false) + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt new file mode 100644 index 00000000000..4b93a412c83 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/property/PropertyInjectSpec.kt @@ -0,0 +1,18 @@ +package io.micronaut.inject.property + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class PropertyInjectSpec { + + @Test + fun testPropertyInjection() { + val context = ApplicationContext.run(mapOf("app.string" to "Hello", "app.map.yyy.xxx" to 2, "app.map.yyy.yyy" to 3)) + val config = context.getBean(BeanWithProperty::class.java) + Assertions.assertEquals(config.stringParam, "Hello") + Assertions.assertEquals(config.mapParam?.get("yyy.xxx"), "2") + Assertions.assertEquals(config.mapParam?.get("yyy.yyy"), "3") + context.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt new file mode 100644 index 00000000000..ba117e9fa7b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/MultipleRequires.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.repeatable + +import io.micronaut.context.annotation.Requirements +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requirements(Requires(property = "foo"), Requires(property = "bar")) +class MultipleRequires { +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt new file mode 100644 index 00000000000..862060a0b71 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/repeatable/RepeatableSpec.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.repeatable + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RepeatableSpec { + + @Test + fun testBeanIsNotAvailable() { + val context = ApplicationContext.run() + assertFalse(context.containsBean(MultipleRequires::class.java)) + context.close() + } + + @Test + fun testBeanIsNotAvailable2() { + val context = ApplicationContext.run(hashMapOf("foo" to "true") as Map) + assertFalse(context.containsBean(MultipleRequires::class.java)) + context.close() + } + + fun testBeanIsAvailable() { + val context = ApplicationContext.run(hashMapOf("foo" to "true", "bar" to "y") as Map) + assertTrue(context.containsBean(MultipleRequires::class.java)) + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt new file mode 100644 index 00000000000..80033c9c4ac --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresFuture.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requires(sdk = Requires.Sdk.KOTLIN, version = "10.3.70") +class RequiresFuture diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt new file mode 100644 index 00000000000..b2a8889b158 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresOld.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton + +@Singleton +@Requires(sdk = Requires.Sdk.KOTLIN, version = "1.0.0") +class RequiresOld diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt new file mode 100644 index 00000000000..7c898ee0bb1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/requires/RequiresSdkSpec.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.requires + +import io.micronaut.context.ApplicationContext +import junit.framework.TestCase +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RequiresSdkSpec { + + @Test + fun testRequiresKotlinSDKworks() { + val context = ApplicationContext.run() + assertFalse(context.containsBean(RequiresFuture::class.java)) + assertTrue(context.containsBean(RequiresOld::class.java)) + context.close() + } + +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt new file mode 100644 index 00000000000..d1c2b774389 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/BeanIntrospectorSpec.kt @@ -0,0 +1,38 @@ +package io.micronaut.inject.visitor.beans + +import io.micronaut.core.beans.BeanIntrospector +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class BeanIntrospectorSpec { + + @Test + fun testGetIntrospection() { + val introspection = BeanIntrospector.SHARED.getIntrospection(TestBean::class.java) + + assertEquals(5, introspection.propertyNames.size) + assertTrue(introspection.getProperty("age").isPresent) + assertTrue(introspection.getProperty("name").isPresent) + + val testBean = introspection.instantiate("fred", 10, arrayOf("one")) + + assertEquals("fred", testBean.name) + assertFalse(testBean.flag) + + try { + introspection.getProperty("name").get().set(testBean, "bob") + fail("Should have failed with unsupported operation, readonly") + } catch (e: UnsupportedOperationException) { + } + + assertEquals("default", testBean.stuff) + + introspection.getProperty("stuff").get().set(testBean, "newvalue") + introspection.getProperty("flag").get().set(testBean, true) + assertEquals(true, introspection.getProperty("flag", Boolean::class.java).get().get(testBean)) + + assertEquals("newvalue", testBean.stuff) + } + + +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt new file mode 100644 index 00000000000..7fa69c4ad4e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/inject/visitor/beans/TestBean.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.inject.visitor.beans + +import io.micronaut.core.annotation.Introspected + +@Introspected +data class TestBean( + val name : String, + val age : Int, + val stringArray : Array) { + var stuff : String = "default" + var flag : Boolean = false +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt new file mode 100644 index 00000000000..076befdd85e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/MyCustomException.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.retry + +import java.lang.RuntimeException + +class MyCustomException: RuntimeException() \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt new file mode 100644 index 00000000000..8e3320cf40d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/retry/RetrySpec.kt @@ -0,0 +1,96 @@ +package io.micronaut.retry + +import io.micronaut.context.ApplicationContext +import io.micronaut.retry.annotation.Retryable +import io.micronaut.retry.event.RetryEvent +import io.micronaut.retry.event.RetryEventListener +import jakarta.inject.Singleton +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RetrySpec { + + @Test + fun testRetryWithIncludes() { + val context = ApplicationContext.run() + val counterService = context.getBean(CounterService::class.java) + + assertFailsWith(IllegalStateException::class) { + counterService.getCountIncludes(true) + } + assertEquals(counterService.countIncludes, 1) + + counterService.getCountIncludes(false) + + assertEquals(counterService.countIncludes, counterService.countThreshold) + + context.stop() + } + + @Test + fun testRetryWithExcludes() { + val context = ApplicationContext.run() + val counterService = context.getBean(CounterService::class.java) + + assertFailsWith(MyCustomException::class) { + counterService.getCountExcludes(false) + } + + assertEquals(counterService.countExcludes, 1) + + counterService.getCountExcludes(true) + + assertEquals(counterService.countExcludes, counterService.countThreshold) + + context.stop() + } + + @Singleton + class MyRetryListener : RetryEventListener { + + val events: ArrayList = ArrayList() + + fun reset() { + events.clear() + } + + override fun onApplicationEvent(event: RetryEvent) { + events.add(event) + } + } + + @Singleton + open class CounterService { + + var countIncludes = 0 + var countExcludes = 0 + var countThreshold = 3 + + @Retryable(attempts = "5", delay = "5ms", includes = [MyCustomException::class]) + open fun getCountIncludes(illegalState: Boolean): Int { + countIncludes++ + if(countIncludes < countThreshold) { + if (illegalState) { + throw IllegalStateException("Bad count") + } else { + throw MyCustomException() + } + } + return countIncludes + } + + @Retryable(attempts = "5", delay = "5ms", excludes = [MyCustomException::class]) + open fun getCountExcludes(illegalState: Boolean): Int { + countExcludes++ + if(countExcludes < countThreshold) { + if (illegalState) { + throw IllegalStateException("Bad count") + } else { + throw MyCustomException() + } + } + return countExcludes + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt new file mode 100644 index 00000000000..8f22caa04a1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerContract.kt @@ -0,0 +1,9 @@ +package io.micronaut.runtime.event + +import io.micronaut.runtime.event.annotation.EventListener + +interface EventListenerContract { + + @EventListener + fun doOnEvent(myEvent: MyEvent) +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt new file mode 100644 index 00000000000..3d3b6c54ebd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerImpl.kt @@ -0,0 +1,13 @@ +package io.micronaut.runtime.event + +import jakarta.inject.Singleton + +@Singleton +class EventListenerImpl : EventListenerContract { + + var called = false + + override fun doOnEvent(myEvent: MyEvent) { + called = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy new file mode 100644 index 00000000000..2eab565cea9 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/EventListenerSpec.groovy @@ -0,0 +1,25 @@ +package io.micronaut.runtime.event + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Test +import spock.lang.Specification + +import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertTrue + +class EventListenerSpec { + + @Test + void testImplementingAnInterfaceWithEventListener() { + ApplicationContext ctx = ApplicationContext.run() + EventListenerImpl impl = ctx.getBean(EventListenerImpl) + + assertFalse(impl.called) + + ctx.publishEvent(new MyEvent("")) + + assertTrue(impl.called) + + ctx.close() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt new file mode 100644 index 00000000000..9f9e1b0c77a --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/runtime/event/MyEvent.kt @@ -0,0 +1,5 @@ +package io.micronaut.runtime.event + +import io.micronaut.context.event.ApplicationEvent + +class MyEvent(source: Any) : ApplicationEvent(source) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt new file mode 100644 index 00000000000..d13fd9cd5ea --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/CoroutineController.kt @@ -0,0 +1,56 @@ +package io.micronaut.scheduling.instrument + +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.QueryValue +import io.micronaut.scheduling.TaskExecutors +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import jakarta.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.rx2.await +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import kotlin.coroutines.CoroutineContext + +typealias TokenDetail = String +@Controller +class Controller(@Named(TaskExecutors.IO) private val executorService: ExecutorService) : CoroutineScope { + + override val coroutineContext: CoroutineContext = object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + executorService.execute(block) + } + + } + + val stream: Observable by lazy { + requestNextToken(0).replay(1).autoConnect() + } + + fun current() = stream.take(1).singleOrError()!! + + private fun requestNextToken(idx: Long): Observable { + return Observable.just(idx).map { + Thread.sleep(5000) + "idx + $it" + }.subscribeOn(Schedulers.io()) + } + + @Get("/tryout/{times}") + fun tryout(@QueryValue("times") times: Int) = asyncResult { + (1..times).map { + async { current().await() } + }.map { + it.await() + } + } + + private fun asyncResult(block: suspend CoroutineScope.() -> T): CompletableFuture { + return async { block() }.asCompletableFuture() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt new file mode 100644 index 00000000000..bc671e1e251 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/scheduling/instrument/MultipleInvocationInstrumenterSpec.kt @@ -0,0 +1,31 @@ +package io.micronaut.scheduling.instrument + +import io.micronaut.context.annotation.Property +import io.micronaut.http.HttpRequest +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Test +import jakarta.inject.Inject +import reactor.core.publisher.Flux +import kotlin.test.assertTrue + +@MicronautTest +@Property(name = "tracing.zipkin.enabled", value = "true") +class MultipleInvocationInstrumenterSpec { + + @Inject + @field:Client("/") + lateinit var client : HttpClient; + + @Test + fun testMultipleInvocationInstrumenter() { + val map: List<*> = Flux.from(client + .retrieve( + HttpRequest.GET("/tryout/100"), + MutableList::class.java + )).blockFirst() + + assertTrue(map.isNotEmpty()) + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt new file mode 100644 index 00000000000..1b3da56f2c6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/Person.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation.validator + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank + +@Introspected +data class Person( + @NotBlank var name: String, + @Min(18) var age: Int +) \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt new file mode 100644 index 00000000000..e29239dd2b6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/io/micronaut/validation/validator/ValidatorSpec.kt @@ -0,0 +1,36 @@ +package io.micronaut.validation.validator + +import io.micronaut.context.ApplicationContext +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.fail +import javax.validation.ConstraintViolationException + +class ValidatorSpec { + + @Test + fun testValidateInstance() { + val context = ApplicationContext.run() + val validator = context.getBean(Validator::class.java) + + val person = Person("", 10) + val violations = validator.validate(person) +// TODO: currently fails because bean introspection API does not handle data classes +// assertEquals(2, violations.size) + context.close() + } + + @Test + fun testValidateNew() { + val context = ApplicationContext.run() + val validator = context.getBean(Validator::class.java).forExecutables() + + try { + val person = validator.createValid(Person::class.java, "", 10) + fail("should have failed with validation errors") + } catch (e: ConstraintViolationException) { + assertEquals(2, e.constraintViolations.size) + } + context.close() + } +} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt new file mode 100644 index 00000000000..bb3fb890cd6 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +interface Car diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt new file mode 100644 index 00000000000..5f5a3b0a583 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Convertible.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.atinject.jakartatck.auto.accessories.Cupholder +import org.atinject.jakartatck.auto.accessories.RoundThing +import org.atinject.jakartatck.auto.accessories.SpareTire +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +import jakarta.inject.Inject +import jakarta.inject.Named +import jakarta.inject.Provider +import org.junit.jupiter.api.TestInstance + +open class Convertible : Car { + + @Inject @field:Drivers internal var driversSeatA: Seat? = null + @Inject @field:Drivers internal var driversSeatB: Seat? = null + @Inject internal var spareTire: SpareTire? = null + @Inject internal var cupholder: Cupholder? = null + @Inject internal var engineProvider: Provider? = null + + private var methodWithZeroParamsInjected: Boolean = false + private var methodWithMultipleParamsInjected: Boolean = false + private var methodWithNonVoidReturnInjected: Boolean = false + + private var constructorPlainSeat: Seat? = null + private var constructorDriversSeat: Seat? = null + private var constructorPlainTire: Tire? = null + private var constructorSpareTire: Tire? = null + private var constructorPlainSeatProvider = nullProvider() + private var constructorDriversSeatProvider = nullProvider() + private var constructorPlainTireProvider = nullProvider() + private var constructorSpareTireProvider = nullProvider() + + @Inject protected var fieldPlainSeat: Seat? = null + @Inject @field:Drivers protected var fieldDriversSeat: Seat? = null + @Inject protected var fieldPlainTire: Tire? = null + @Inject @field:Named("spare") protected var fieldSpareTire: Tire? = null + @Inject protected var fieldPlainSeatProvider = nullProvider() + @Inject @field:Drivers protected var fieldDriversSeatProvider = nullProvider() + @Inject protected var fieldPlainTireProvider = nullProvider() + @Inject @field:Named("spare") protected var fieldSpareTireProvider = nullProvider() + + private var methodPlainSeat: Seat? = null + private var methodDriversSeat: Seat? = null + private var methodPlainTire: Tire? = null + private var methodSpareTire: Tire? = null + private var methodPlainSeatProvider = nullProvider() + private var methodDriversSeatProvider = nullProvider() + private var methodPlainTireProvider = nullProvider() + private var methodSpareTireProvider = nullProvider() + + @Inject internal constructor( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + constructorPlainSeat = plainSeat + constructorDriversSeat = driversSeat + constructorPlainTire = plainTire + constructorSpareTire = spareTire + constructorPlainSeatProvider = plainSeatProvider + constructorDriversSeatProvider = driversSeatProvider + constructorPlainTireProvider = plainTireProvider + constructorSpareTireProvider = spareTireProvider + } + + internal constructor() { + throw AssertionError("Unexpected call to non-injectable constructor") + } + + internal fun setSeat(unused: Seat) { + throw AssertionError("Unexpected call to non-injectable method") + } + + @Inject internal fun injectMethodWithZeroArgs() { + methodWithZeroParamsInjected = true + } + + @Inject internal fun injectMethodWithNonVoidReturn(): String { + methodWithNonVoidReturnInjected = true + return "unused" + } + + @Inject internal fun injectInstanceMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + methodWithMultipleParamsInjected = true + + methodPlainSeat = plainSeat + methodDriversSeat = driversSeat + methodPlainTire = plainTire + methodSpareTire = spareTire + methodPlainSeatProvider = plainSeatProvider + methodDriversSeatProvider = driversSeatProvider + methodPlainTireProvider = plainTireProvider + methodSpareTireProvider = spareTireProvider + } + + internal class NullProvider : Provider { + + override fun get(): T? { + return null + } + } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Tests { + + private val context = BeanContext.run() + private val car = context.getBean(Convertible::class.java) + private val cupholder = car.cupholder + private val spareTire = car.spareTire + private val plainTire = car.fieldPlainTire + private val engine = car.engineProvider!!.get() + + // smoke tests: if these fail all bets are off + + @Test + fun testFieldsInjected() { + assertTrue(cupholder != null && spareTire != null) + } + + @Test + fun testProviderReturnedValues() { + assertTrue(engine != null) + } + + // injecting different kinds of members + + @Test + fun testMethodWithZeroParametersInjected() { + assertTrue(car.methodWithZeroParamsInjected) + } + + @Test + fun testMethodWithMultipleParametersInjected() { + assertTrue(car.methodWithMultipleParamsInjected) + } + + @Test + fun testNonVoidMethodInjected() { + assertTrue(car.methodWithNonVoidReturnInjected) + } + + @Test + fun testPublicNoArgsConstructorInjected() { + assertTrue(engine!!.publicNoArgsConstructorInjected) + } + + @Test + fun testSubtypeFieldsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenFieldInjected()) + } + + @Test + fun testSubtypeMethodsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenMethodInjected()) + } + + @Test + fun testSupertypeFieldsInjected() { + assertTrue(spareTire!!.hasTireBeenFieldInjected()) + } + + @Test + fun testSupertypeMethodsInjected() { + assertTrue(spareTire!!.hasTireBeenMethodInjected()) + } + + @Test + fun testTwiceOverriddenMethodInjectedWhenMiddleLacksAnnotation() { + assertTrue(engine!!.overriddenTwiceWithOmissionInMiddleInjected) + } + + // injected values + +/* @Test + fun testQualifiersNotInheritedFromOverriddenMethod() { + assertTrue(engine!!.overriddenMethodInjected) + assertFalse(engine!!.qualifiersInheritedFromOverriddenMethod) + }*/ + + @Test + fun testConstructorInjectionWithValues() { + assertFalse(car.constructorPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithValues() { + assertFalse(car.fieldPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithValues() { + assertFalse(car.methodPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTire is SpareTire,"Expected qualified value") + } + + // injected providers + + @Test + fun testConstructorInjectionWithProviders() { + assertFalse(car.constructorPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithProviders() { + assertFalse(car.fieldPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithProviders() { + assertFalse(car.methodPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + + // singletons + + @Test + fun testConstructorInjectedProviderYieldsSingleton() { + assertSame(car.constructorPlainSeatProvider.get(), car.constructorPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testFieldInjectedProviderYieldsSingleton() { + assertSame(car.fieldPlainSeatProvider.get(), car.fieldPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testMethodInjectedProviderYieldsSingleton() { + assertSame(car.methodPlainSeatProvider.get(), car.methodPlainSeatProvider.get(),"Expected same value") + } + + @Test + fun testCircularlyDependentSingletons() { + // uses provider.get() to get around circular deps + assertSame(cupholder!!.seatProvider.get().cupholder, cupholder) + } + + + // non singletons + @Test + fun testSingletonAnnotationNotInheritedFromSupertype() { + assertNotSame(car.driversSeatA, car.driversSeatB) + } + + @Test + fun testConstructorInjectedProviderYieldsDistinctValues() { + assertNotSame(car.constructorDriversSeatProvider.get(), car.constructorDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.constructorPlainTireProvider.get(), car.constructorPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.constructorSpareTireProvider.get(), car.constructorSpareTireProvider.get(),"Expected distinct values") + } + + @Test + fun testFieldInjectedProviderYieldsDistinctValues() { + assertNotSame(car.fieldDriversSeatProvider.get(), car.fieldDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.fieldPlainTireProvider.get(), car.fieldPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.fieldSpareTireProvider.get(), car.fieldSpareTireProvider.get(),"Expected distinct values") + } + + @Test + fun testMethodInjectedProviderYieldsDistinctValues() { + assertNotSame(car.methodDriversSeatProvider.get(), car.methodDriversSeatProvider.get(),"Expected distinct values") + assertNotSame(car.methodPlainTireProvider.get(), car.methodPlainTireProvider.get(),"Expected distinct values") + assertNotSame(car.methodSpareTireProvider.get(), car.methodSpareTireProvider.get(),"Expected distinct values") + } + + + // mix inheritance + visibility + + @Test + fun testPackagePrivateMethodInjectedDifferentPackages() { + assertTrue(spareTire!!.subPackagePrivateMethodInjected) + //Not valid because in Kotlin it is an override + //assertTrue(spareTire.superPackagePrivateMethodInjected) + } + + @Test + fun testOverriddenProtectedMethodInjection() { + assertTrue(spareTire!!.subProtectedMethodInjected) + assertFalse(spareTire.superProtectedMethodInjected) + } + + @Test + fun testOverriddenPublicMethodNotInjected() { + assertTrue(spareTire!!.subPublicMethodInjected) + assertFalse(spareTire.superPublicMethodInjected) + } + + + // inject in order + + @Test + fun testFieldsInjectedBeforeMethods() { + //Added to assert that fields are injected before methods in Kotlin + assertFalse(plainTire!!.methodInjectedBeforeFields) + //Ignored because fields override in Kotlin + //assertFalse(spareTire!!.methodInjectedBeforeFields) + } + + @Test + fun testSupertypeMethodsInjectedBeforeSubtypeFields() { + // FIXME: difficult to achieve with current design without a significant rewrite or how native properties are handled +// assertFalse(spareTire!!.subtypeFieldInjectedBeforeSupertypeMethods) + } + + @Test + fun testSupertypeMethodInjectedBeforeSubtypeMethods() { + assertFalse(spareTire!!.subtypeMethodInjectedBeforeSupertypeMethods) + } + + + // necessary injections occur + + @Test + fun testPackagePrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + //Not valid because in Kotlin the method is overridden + //assertTrue(spareTire!!.subPackagePrivateMethodForOverrideInjected) + } + + + // override or similar method without @Inject + + @Test + fun testPrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.subPackagePrivateMethodForOverrideInjected) + assertFalse(engine.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testProtectedMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.protectedMethodForOverrideInjected) + } + + @Test + fun testPublicMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.publicMethodForOverrideInjected) + } + + @Test + fun testTwiceOverriddenMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.overriddenTwiceWithOmissionInSubclassInjected) + } + + @Test + fun testOverridingMixedWithPackagePrivate2() { + assertTrue(spareTire!!.spareTirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod2Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod2Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod2Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate3() { + assertFalse(spareTire!!.spareTirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod3Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod3Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod3Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate4() { + assertFalse(plainTire!!.tirePackagePrivateMethod4Injected) + //Not the same as Java because package private can be overridden by any subclass in the project + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod4Injected) + } + + // inject only once + + @Test + fun testOverriddenPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(engine!!.overriddenPackagePrivateMethodInjectedTwice) + } + + @Test + fun testSimilarPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPackagePrivateMethodInjectedTwice) + } + + @Test + fun testOverriddenProtectedMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenProtectedMethodInjectedTwice) + } + + @Test + fun testOverriddenPublicMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenPublicMethodInjectedTwice) + } + + } + + class PrivateTests { + private val context = DefaultBeanContext().start() + private val car = context.getBean(Convertible::class.java) + private val engine = car.engineProvider!!.get() + private val spareTire = car.spareTire + + @Test + fun testSupertypePrivateMethodInjected() { + assertTrue(spareTire!!.superPrivateMethodInjected) + assertTrue(spareTire.subPrivateMethodInjected) + } + + @Test + fun testPackagePrivateMethodInjectedSamePackage() { + assertTrue(engine.subPackagePrivateMethodInjected) + assertFalse(engine.superPackagePrivateMethodInjected) + } + + @Test + fun testPrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + assertTrue(spareTire!!.subPrivateMethodForOverrideInjected) + } + + @Test + fun testSimilarPrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPrivateMethodInjectedTwice) + } + } + + companion object { + + @Inject internal var staticFieldPlainSeat: Seat? = null + @Inject + @Drivers internal var staticFieldDriversSeat: Seat? = null + @Inject internal var staticFieldPlainTire: Tire? = null + @Inject + @Named("spare") internal var staticFieldSpareTire: Tire? = null + @Inject internal var staticFieldPlainSeatProvider = nullProvider() + @Inject + @Drivers internal var staticFieldDriversSeatProvider = nullProvider() + @Inject internal var staticFieldPlainTireProvider = nullProvider() + @Inject + @Named("spare") internal var staticFieldSpareTireProvider = nullProvider() + + private var staticMethodPlainSeat: Seat? = null + private var staticMethodDriversSeat: Seat? = null + private var staticMethodPlainTire: Tire? = null + private var staticMethodSpareTire: Tire? = null + private var staticMethodPlainSeatProvider = nullProvider() + private var staticMethodDriversSeatProvider = nullProvider() + private var staticMethodPlainTireProvider = nullProvider() + private var staticMethodSpareTireProvider = nullProvider() + + @Inject internal fun injectStaticMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + staticMethodPlainSeat = plainSeat + staticMethodDriversSeat = driversSeat + staticMethodPlainTire = plainTire + staticMethodSpareTire = spareTire + staticMethodPlainSeatProvider = plainSeatProvider + staticMethodDriversSeatProvider = driversSeatProvider + staticMethodPlainTireProvider = plainTireProvider + staticMethodSpareTireProvider = spareTireProvider + + } + + /** + * Returns a provider that always returns null. This is used as a default + * value to avoid null checks for omitted provider injections. + */ + private fun nullProvider(): Provider { + return NullProvider() + } + + var localConvertible = ThreadLocal() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt new file mode 100644 index 00000000000..c816f241569 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Drivers.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Drivers diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt new file mode 100644 index 00000000000..bdfb93c8128 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/DriversSeat.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.Cupholder + +import jakarta.inject.Inject + +open class DriversSeat @Inject +constructor(cupholder: Cupholder) : Seat(cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt new file mode 100644 index 00000000000..539250e801b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Engine.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import jakarta.inject.Named + +abstract class Engine { + + var publicNoArgsConstructorInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + + var overriddenTwiceWithOmissionInMiddleInjected: Boolean = false + var overriddenTwiceWithOmissionInSubclassInjected: Boolean = false + + protected var seatA: Seat? = null + protected var seatB: Seat? = null + protected var tireA: Tire? = null + protected var tireB: Tire? = null + + var overriddenPackagePrivateMethodInjectedTwice: Boolean = false + var qualifiersInheritedFromOverriddenMethod: Boolean = false + var overriddenMethodInjected: Boolean = false + + @Inject internal open fun injectPackagePrivateMethod() { + superPackagePrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + @Inject + open fun injectQualifiers(@Drivers seatA: Seat, seatB: Seat, + @Named("spare") tireA: Tire, tireB: Tire) { + overriddenMethodInjected = true + if (seatA !is DriversSeat + || seatB is DriversSeat + || tireA !is SpareTire + || tireB is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt new file mode 100644 index 00000000000..fac49e3e6e1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/FuelTank.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Singleton + +@Singleton +class FuelTank diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt new file mode 100644 index 00000000000..bf8824eee91 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/GasEngine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import jakarta.inject.Inject + +abstract class GasEngine : Engine() { + + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt new file mode 100644 index 00000000000..85121f696fb --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seat.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.Cupholder + +import jakarta.inject.Inject +import jakarta.inject.Singleton + +@Singleton +open class Seat @Inject +internal constructor(val cupholder: Cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt new file mode 100644 index 00000000000..76155816726 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Seatbelt.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +class Seatbelt diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt new file mode 100644 index 00000000000..af147c5f853 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/Tire.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.RoundThing +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import java.util.LinkedHashSet + +open class Tire @Inject +constructor(constructorInjection: FuelTank) : RoundThing() { + + internal open var constructorInjection = NEVER_INJECTED + @Inject protected open var fieldInjection = NEVER_INJECTED + internal open var methodInjection = NEVER_INJECTED + + internal var constructorInjected: Boolean = false + + var superPrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var superProtectedMethodInjected: Boolean = false + var superPublicMethodInjected: Boolean = false + var subPrivateMethodInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var subProtectedMethodInjected: Boolean = false + var subPublicMethodInjected: Boolean = false + + var superPrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + var subPrivateMethodForOverrideInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var protectedMethodForOverrideInjected: Boolean = false + var publicMethodForOverrideInjected: Boolean = false + + var methodInjectedBeforeFields: Boolean = false + var subtypeFieldInjectedBeforeSupertypeMethods: Boolean = false + var subtypeMethodInjectedBeforeSupertypeMethods: Boolean = false + var similarPrivateMethodInjectedTwice: Boolean = false + var similarPackagePrivateMethodInjectedTwice: Boolean = false + var overriddenProtectedMethodInjectedTwice: Boolean = false + var overriddenPublicMethodInjectedTwice: Boolean = false + + var tirePackagePrivateMethod2Injected: Boolean = false + + var tirePackagePrivateMethod3Injected: Boolean = false + + var tirePackagePrivateMethod4Injected: Boolean = false + + init { + this.constructorInjection = constructorInjection + } + + @Inject internal fun supertypeMethodInjection(methodInjection: FuelTank) { + if (!hasTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + if (hasSpareTireBeenFieldInjected()) { + subtypeFieldInjectedBeforeSupertypeMethods = true + } + if (hasSpareTireBeenMethodInjected()) { + subtypeMethodInjectedBeforeSupertypeMethods = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (superPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + superPrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethod() { + if (superPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + superPackagePrivateMethodInjected = true + } + + @Inject protected open fun injectProtectedMethod() { + if (superProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + superProtectedMethodInjected = true + } + + @Inject + open fun injectPublicMethod() { + if (superPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + superPublicMethodInjected = true + } + + @Inject private fun injectPrivateMethodForOverride() { + subPrivateMethodForOverrideInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject protected open fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + @Inject + open fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + fun hasTireBeenFieldInjected(): Boolean { + return fieldInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenFieldInjected(): Boolean { + return false + } + + fun hasTireBeenMethodInjected(): Boolean { + return methodInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenMethodInjected(): Boolean { + return false + } + + @Inject override fun injectPackagePrivateMethod2() { + tirePackagePrivateMethod2Injected = true + } + + @Inject override fun injectPackagePrivateMethod3() { + tirePackagePrivateMethod3Injected = true + } + + override fun injectPackagePrivateMethod4() { + tirePackagePrivateMethod4Injected = true + } + + companion object { + + val NEVER_INJECTED = FuelTank() + + protected val moreProblems: Set = LinkedHashSet() + @Inject internal var staticFieldInjection = NEVER_INJECTED + internal var staticMethodInjection = NEVER_INJECTED + var staticMethodInjectedBeforeStaticFields: Boolean = false + var subtypeStaticFieldInjectedBeforeSupertypeStaticMethods: Boolean = false + var subtypeStaticMethodInjectedBeforeSupertypeStaticMethods: Boolean = false + + @Inject internal fun supertypeStaticMethodInjection(methodInjection: FuelTank) { + if (!Tire.hasBeenStaticFieldInjected()) { + staticMethodInjectedBeforeStaticFields = true + } + if (SpareTire.hasBeenStaticFieldInjected()) { + subtypeStaticFieldInjectedBeforeSupertypeStaticMethods = true + } + if (SpareTire.hasBeenStaticMethodInjected()) { + subtypeStaticMethodInjectedBeforeSupertypeStaticMethods = true + } + staticMethodInjection = methodInjection + } + + protected fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection != NEVER_INJECTED + } + + protected fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection != NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt new file mode 100644 index 00000000000..26be32cd607 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/V8Engine.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto + +import org.atinject.jakartatck.auto.accessories.SpareTire + +import jakarta.inject.Inject +import jakarta.inject.Named + +class V8Engine : GasEngine() { + init { + publicNoArgsConstructorInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + overriddenPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + /** + * Qualifiers are swapped from how they appear in the superclass. + */ + override fun injectQualifiers(seatA: Seat, @Drivers seatB: Seat, + tireA: Tire, @Named("spare") tireB: Tire) { + overriddenMethodInjected = true + if (seatA is DriversSeat + || seatB !is DriversSeat + || tireA is SpareTire + || tireB !is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + override fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt new file mode 100644 index 00000000000..ce2986107cd --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/Cupholder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + +import org.atinject.jakartatck.auto.Seat + +import jakarta.inject.Inject +import jakarta.inject.Provider +import jakarta.inject.Singleton + +@Singleton +open class Cupholder @Inject +constructor(val seatProvider: Provider) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt new file mode 100644 index 00000000000..5e0e2c8242d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/RoundThing.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + + +import jakarta.inject.Inject + +open class RoundThing { + + var roundThingPackagePrivateMethod2Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod3Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod4Injected: Boolean = false + private set + + @Inject open internal fun injectPackagePrivateMethod2() { + roundThingPackagePrivateMethod2Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod3() { + roundThingPackagePrivateMethod3Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod4() { + roundThingPackagePrivateMethod4Injected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt new file mode 100644 index 00000000000..54521b06063 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/jakartatck/auto/accessories/SpareTire.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.jakartatck.auto.accessories + +import org.atinject.jakartatck.auto.FuelTank +import org.atinject.jakartatck.auto.Tire + +import jakarta.inject.Inject + +open class SpareTire @Inject +constructor(forSupertype: FuelTank, forSubtype: FuelTank) : Tire(forSupertype) { + + override var constructorInjection = Tire.NEVER_INJECTED + @Inject override var fieldInjection = Tire.NEVER_INJECTED + override var methodInjection = Tire.NEVER_INJECTED + + var spareTirePackagePrivateMethod2Injected: Boolean = false + private set + + var spareTirePackagePrivateMethod3Injected: Boolean = false + private set + + init { + this.constructorInjection = forSubtype + } + + @Inject internal fun subtypeMethodInjection(methodInjection: FuelTank) { + if (!hasSpareTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (subPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + subPrivateMethodInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + @Inject override fun injectProtectedMethod() { + if (subProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + subProtectedMethodInjected = true + } + + @Inject + override fun injectPublicMethod() { + if (subPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + subPublicMethodInjected = true + } + + private fun injectPrivateMethodForOverride() { + superPrivateMethodForOverrideInjected = true + } + + override fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + override fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + override fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + public override fun hasSpareTireBeenFieldInjected(): Boolean { + return fieldInjection !== Tire.NEVER_INJECTED + } + + @Override + public override fun hasSpareTireBeenMethodInjected(): Boolean { + return methodInjection !== Tire.NEVER_INJECTED + } + + @Inject override fun injectPackagePrivateMethod2() { + spareTirePackagePrivateMethod2Injected = true + } + + override fun injectPackagePrivateMethod3() { + spareTirePackagePrivateMethod3Injected = true + } + + companion object { + @Inject internal var staticFieldInjection = Tire.NEVER_INJECTED + internal var staticMethodInjection = Tire.NEVER_INJECTED + + @Inject internal fun subtypeStaticMethodInjection(methodInjection: FuelTank) { + if (!hasBeenStaticFieldInjected()) { + Tire.staticMethodInjectedBeforeStaticFields = true + } + staticMethodInjection = methodInjection + } + + fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection !== Tire.NEVER_INJECTED + } + + fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection !== Tire.NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt new file mode 100644 index 00000000000..132fb2a7202 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Car.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +interface Car diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt new file mode 100644 index 00000000000..e99cef5f60b --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Convertible.kt @@ -0,0 +1,538 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import io.micronaut.context.BeanContext +import io.micronaut.context.DefaultBeanContext +import org.atinject.javaxtck.auto.accessories.Cupholder +import org.atinject.javaxtck.auto.accessories.RoundThing +import org.atinject.javaxtck.auto.accessories.SpareTire +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider + +open class Convertible : Car { + + @Inject @field:Drivers internal var driversSeatA: Seat? = null + @Inject @field:Drivers internal var driversSeatB: Seat? = null + @Inject internal var spareTire: SpareTire? = null + @Inject internal var cupholder: Cupholder? = null + @Inject internal var engineProvider: Provider? = null + + private var methodWithZeroParamsInjected: Boolean = false + private var methodWithMultipleParamsInjected: Boolean = false + private var methodWithNonVoidReturnInjected: Boolean = false + + private var constructorPlainSeat: Seat? = null + private var constructorDriversSeat: Seat? = null + private var constructorPlainTire: Tire? = null + private var constructorSpareTire: Tire? = null + private var constructorPlainSeatProvider = nullProvider() + private var constructorDriversSeatProvider = nullProvider() + private var constructorPlainTireProvider = nullProvider() + private var constructorSpareTireProvider = nullProvider() + + @Inject protected var fieldPlainSeat: Seat? = null + @Inject @field:Drivers protected var fieldDriversSeat: Seat? = null + @Inject protected var fieldPlainTire: Tire? = null + @Inject @field:Named("spare") protected var fieldSpareTire: Tire? = null + @Inject protected var fieldPlainSeatProvider = nullProvider() + @Inject @field:Drivers protected var fieldDriversSeatProvider = nullProvider() + @Inject protected var fieldPlainTireProvider = nullProvider() + @Inject @field:Named("spare") protected var fieldSpareTireProvider = nullProvider() + + private var methodPlainSeat: Seat? = null + private var methodDriversSeat: Seat? = null + private var methodPlainTire: Tire? = null + private var methodSpareTire: Tire? = null + private var methodPlainSeatProvider = nullProvider() + private var methodDriversSeatProvider = nullProvider() + private var methodPlainTireProvider = nullProvider() + private var methodSpareTireProvider = nullProvider() + + @Inject internal constructor( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + constructorPlainSeat = plainSeat + constructorDriversSeat = driversSeat + constructorPlainTire = plainTire + constructorSpareTire = spareTire + constructorPlainSeatProvider = plainSeatProvider + constructorDriversSeatProvider = driversSeatProvider + constructorPlainTireProvider = plainTireProvider + constructorSpareTireProvider = spareTireProvider + } + + internal constructor() { + throw AssertionError("Unexpected call to non-injectable constructor") + } + + internal fun setSeat(unused: Seat) { + throw AssertionError("Unexpected call to non-injectable method") + } + + @Inject internal fun injectMethodWithZeroArgs() { + methodWithZeroParamsInjected = true + } + + @Inject internal fun injectMethodWithNonVoidReturn(): String { + methodWithNonVoidReturnInjected = true + return "unused" + } + + @Inject internal fun injectInstanceMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + methodWithMultipleParamsInjected = true + + methodPlainSeat = plainSeat + methodDriversSeat = driversSeat + methodPlainTire = plainTire + methodSpareTire = spareTire + methodPlainSeatProvider = plainSeatProvider + methodDriversSeatProvider = driversSeatProvider + methodPlainTireProvider = plainTireProvider + methodSpareTireProvider = spareTireProvider + } + + internal class NullProvider : Provider { + + override fun get(): T? { + return null + } + } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Tests { + + private val context = BeanContext.run() + private val car = context.getBean(Convertible::class.java) + private val cupholder = car.cupholder + private val spareTire = car.spareTire + private val plainTire = car.fieldPlainTire + private val engine = car.engineProvider!!.get() + + // smoke tests: if these fail all bets are off + + @Test + fun testFieldsInjected() { + assertTrue(cupholder != null && spareTire != null) + } + + @Test + fun testProviderReturnedValues() { + assertTrue(engine != null) + } + + // injecting different kinds of members + + @Test + fun testMethodWithZeroParametersInjected() { + assertTrue(car.methodWithZeroParamsInjected) + } + + @Test + fun testMethodWithMultipleParametersInjected() { + assertTrue(car.methodWithMultipleParamsInjected) + } + + @Test + fun testNonVoidMethodInjected() { + assertTrue(car.methodWithNonVoidReturnInjected) + } + + @Test + fun testPublicNoArgsConstructorInjected() { + assertTrue(engine!!.publicNoArgsConstructorInjected) + } + + @Test + fun testSubtypeFieldsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenFieldInjected()) + } + + @Test + fun testSubtypeMethodsInjected() { + assertTrue(spareTire!!.hasSpareTireBeenMethodInjected()) + } + + @Test + fun testSupertypeFieldsInjected() { + assertTrue(spareTire!!.hasTireBeenFieldInjected()) + } + + @Test + fun testSupertypeMethodsInjected() { + assertTrue(spareTire!!.hasTireBeenMethodInjected()) + } + + @Test + fun testTwiceOverriddenMethodInjectedWhenMiddleLacksAnnotation() { + assertTrue(engine!!.overriddenTwiceWithOmissionInMiddleInjected) + } + + // injected values + +/* @Test + fun testQualifiersNotInheritedFromOverriddenMethod() { + assertTrue(engine!!.overriddenMethodInjected) + assertFalse(engine!!.qualifiersInheritedFromOverriddenMethod) + }*/ + + @Test + fun testConstructorInjectionWithValues() { + assertFalse(car.constructorPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithValues() { + assertFalse(car.fieldPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTire is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithValues() { + assertFalse(car.methodPlainSeat is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTire is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeat is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTire is SpareTire,"Expected qualified value") + } + + // injected providers + + @Test + fun testConstructorInjectionWithProviders() { + assertFalse(car.constructorPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.constructorPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.constructorDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.constructorSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testFieldInjectionWithProviders() { + assertFalse(car.fieldPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.fieldPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.fieldDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.fieldSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + @Test + fun testMethodInjectionWithProviders() { + assertFalse(car.methodPlainSeatProvider.get() is DriversSeat,"Expected unqualified value") + assertFalse(car.methodPlainTireProvider.get() is SpareTire,"Expected unqualified value") + assertTrue(car.methodDriversSeatProvider.get() is DriversSeat,"Expected qualified value") + assertTrue(car.methodSpareTireProvider.get() is SpareTire,"Expected qualified value") + } + + + // singletons + + @Test + fun testConstructorInjectedProviderYieldsSingleton() { + assertSame(car.constructorPlainSeatProvider.get(), car.constructorPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testFieldInjectedProviderYieldsSingleton() { + assertSame(car.fieldPlainSeatProvider.get(), car.fieldPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testMethodInjectedProviderYieldsSingleton() { + assertSame( + car.methodPlainSeatProvider.get(), car.methodPlainSeatProvider.get(), "Expected same value") + } + + @Test + fun testCircularlyDependentSingletons() { + // uses provider.get() to get around circular deps + assertSame(cupholder!!.seatProvider.get().cupholder, cupholder) + } + + + // non singletons + @Test + fun testSingletonAnnotationNotInheritedFromSupertype() { + assertNotSame(car.driversSeatA, car.driversSeatB) + } + + @Test + fun testConstructorInjectedProviderYieldsDistinctValues() { + assertNotSame(car.constructorDriversSeatProvider.get(), car.constructorDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.constructorPlainTireProvider.get(), car.constructorPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.constructorSpareTireProvider.get(), car.constructorSpareTireProvider.get(), "Expected distinct values") + } + + @Test + fun testFieldInjectedProviderYieldsDistinctValues() { + assertNotSame(car.fieldDriversSeatProvider.get(), car.fieldDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.fieldPlainTireProvider.get(), car.fieldPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.fieldSpareTireProvider.get(), car.fieldSpareTireProvider.get(), "Expected distinct values") + } + + @Test + fun testMethodInjectedProviderYieldsDistinctValues() { + assertNotSame(car.methodDriversSeatProvider.get(), car.methodDriversSeatProvider.get(), "Expected distinct values") + assertNotSame(car.methodPlainTireProvider.get(), car.methodPlainTireProvider.get(), "Expected distinct values") + assertNotSame(car.methodSpareTireProvider.get(), car.methodSpareTireProvider.get(), "Expected distinct values") + } + + + // mix inheritance + visibility + @Test + fun testPackagePrivateMethodInjectedDifferentPackages() { + assertTrue(spareTire!!.subPackagePrivateMethodInjected) + //Not valid because in Kotlin it is an override + //assertTrue(spareTire.superPackagePrivateMethodInjected) + } + + @Test + fun testOverriddenProtectedMethodInjection() { + assertTrue(spareTire!!.subProtectedMethodInjected) + assertFalse(spareTire.superProtectedMethodInjected) + } + + @Test + fun testOverriddenPublicMethodNotInjected() { + assertTrue(spareTire!!.subPublicMethodInjected) + assertFalse(spareTire.superPublicMethodInjected) + } + + + // inject in order + + @Test + fun testFieldsInjectedBeforeMethods() { + //Added to assert that fields are injected before methods in Kotlin + assertFalse(plainTire!!.methodInjectedBeforeFields) + //Ignored because fields override in Kotlin + //assertFalse(spareTire!!.methodInjectedBeforeFields) + } + + @Test + fun testSupertypeMethodsInjectedBeforeSubtypeFields() { + // FIXME: difficult to achieve with current design without a significant rewrite or how native properties are handled +// assertFalse(spareTire!!.subtypeFieldInjectedBeforeSupertypeMethods) + } + + @Test + fun testSupertypeMethodInjectedBeforeSubtypeMethods() { + assertFalse(spareTire!!.subtypeMethodInjectedBeforeSupertypeMethods) + } + + + // necessary injections occur + + @Test + fun testPackagePrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + //Not valid because in Kotlin the method is overridden + //assertTrue(spareTire!!.subPackagePrivateMethodForOverrideInjected) + } + + + // override or similar method without @Inject + + @Test + fun testPrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.subPackagePrivateMethodForOverrideInjected) + assertFalse(engine.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testPackagePrivateMethodNotInjectedWhenSupertypeHasAnnotatedSimilarMethod() { + assertFalse(spareTire!!.superPackagePrivateMethodForOverrideInjected) + } + + @Test + fun testProtectedMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.protectedMethodForOverrideInjected) + } + + @Test + fun testPublicMethodNotInjectedWhenOverrideNotAnnotated() { + assertFalse(spareTire!!.publicMethodForOverrideInjected) + } + + @Test + fun testTwiceOverriddenMethodNotInjectedWhenOverrideLacksAnnotation() { + assertFalse(engine!!.overriddenTwiceWithOmissionInSubclassInjected) + } + + @Test + fun testOverridingMixedWithPackagePrivate2() { + assertTrue(spareTire!!.spareTirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod2Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod2Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod2Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod2Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate3() { + assertFalse(spareTire!!.spareTirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((spareTire as Tire).tirePackagePrivateMethod3Injected) + assertFalse((spareTire as RoundThing).roundThingPackagePrivateMethod3Injected) + + assertTrue(plainTire!!.tirePackagePrivateMethod3Injected) + //Not valid in Kotlin because the method is overridden + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod3Injected) + } + + @Test + fun testOverridingMixedWithPackagePrivate4() { + assertFalse(plainTire!!.tirePackagePrivateMethod4Injected) + //Not the same as Java because package private can be overridden by any subclass in the project + //assertTrue((plainTire as RoundThing).roundThingPackagePrivateMethod4Injected) + } + + // inject only once + + @Test + fun testOverriddenPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(engine!!.overriddenPackagePrivateMethodInjectedTwice) + } + + @Test + fun testSimilarPackagePrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPackagePrivateMethodInjectedTwice) + } + + @Test + fun testOverriddenProtectedMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenProtectedMethodInjectedTwice) + } + + @Test + fun testOverriddenPublicMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.overriddenPublicMethodInjectedTwice) + } + + } + + class PrivateTests { + private val context = DefaultBeanContext().start() + private val car = context.getBean(Convertible::class.java) + private val engine = car.engineProvider!!.get() + private val spareTire = car.spareTire + + @Test + fun testSupertypePrivateMethodInjected() { + assertTrue(spareTire!!.superPrivateMethodInjected) + assertTrue(spareTire.subPrivateMethodInjected) + } + + @Test + fun testPackagePrivateMethodInjectedSamePackage() { + assertTrue(engine.subPackagePrivateMethodInjected) + assertFalse(engine.superPackagePrivateMethodInjected) + } + + @Test + fun testPrivateMethodInjectedEvenWhenSimilarMethodLacksAnnotation() { + assertTrue(spareTire!!.subPrivateMethodForOverrideInjected) + } + + @Test + fun testSimilarPrivateMethodInjectedOnlyOnce() { + assertFalse(spareTire!!.similarPrivateMethodInjectedTwice) + } + } + + companion object { + + @Inject internal var staticFieldPlainSeat: Seat? = null + @Inject + @Drivers internal var staticFieldDriversSeat: Seat? = null + @Inject internal var staticFieldPlainTire: Tire? = null + @Inject + @Named("spare") internal var staticFieldSpareTire: Tire? = null + @Inject internal var staticFieldPlainSeatProvider = nullProvider() + @Inject + @Drivers internal var staticFieldDriversSeatProvider = nullProvider() + @Inject internal var staticFieldPlainTireProvider = nullProvider() + @Inject + @Named("spare") internal var staticFieldSpareTireProvider = nullProvider() + + private var staticMethodPlainSeat: Seat? = null + private var staticMethodDriversSeat: Seat? = null + private var staticMethodPlainTire: Tire? = null + private var staticMethodSpareTire: Tire? = null + private var staticMethodPlainSeatProvider = nullProvider() + private var staticMethodDriversSeatProvider = nullProvider() + private var staticMethodPlainTireProvider = nullProvider() + private var staticMethodSpareTireProvider = nullProvider() + + @Inject internal fun injectStaticMethodWithManyArgs( + plainSeat: Seat, + @Drivers driversSeat: Seat, + plainTire: Tire, + @Named("spare") spareTire: Tire, + plainSeatProvider: Provider, + @Drivers driversSeatProvider: Provider, + plainTireProvider: Provider, + @Named("spare") spareTireProvider: Provider) { + staticMethodPlainSeat = plainSeat + staticMethodDriversSeat = driversSeat + staticMethodPlainTire = plainTire + staticMethodSpareTire = spareTire + staticMethodPlainSeatProvider = plainSeatProvider + staticMethodDriversSeatProvider = driversSeatProvider + staticMethodPlainTireProvider = plainTireProvider + staticMethodSpareTireProvider = spareTireProvider + + } + + /** + * Returns a provider that always returns null. This is used as a default + * value to avoid null checks for omitted provider injections. + */ + private fun nullProvider(): Provider { + return NullProvider() + } + + var localConvertible = ThreadLocal() + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt new file mode 100644 index 00000000000..5c34e0e605d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Drivers.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Drivers diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt new file mode 100644 index 00000000000..e77ebe6b5d7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/DriversSeat.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.Cupholder + +import javax.inject.Inject + +open class DriversSeat @Inject +constructor(cupholder: Cupholder) : Seat(cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt new file mode 100644 index 00000000000..5cf9f9a16f2 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Engine.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import javax.inject.Named + +abstract class Engine { + + var publicNoArgsConstructorInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + + var overriddenTwiceWithOmissionInMiddleInjected: Boolean = false + var overriddenTwiceWithOmissionInSubclassInjected: Boolean = false + + protected var seatA: Seat? = null + protected var seatB: Seat? = null + protected var tireA: Tire? = null + protected var tireB: Tire? = null + + var overriddenPackagePrivateMethodInjectedTwice: Boolean = false + var qualifiersInheritedFromOverriddenMethod: Boolean = false + var overriddenMethodInjected: Boolean = false + + @Inject internal open fun injectPackagePrivateMethod() { + superPackagePrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + @Inject + open fun injectQualifiers(@Drivers seatA: Seat, seatB: Seat, + @Named("spare") tireA: Tire, tireB: Tire) { + overriddenMethodInjected = true + if (seatA !is DriversSeat + || seatB is DriversSeat + || tireA !is SpareTire + || tireB is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + open fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt new file mode 100644 index 00000000000..bd3446a7584 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/FuelTank.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Singleton + +@Singleton +class FuelTank diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt new file mode 100644 index 00000000000..47e99df64f1 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/GasEngine.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import javax.inject.Inject + +abstract class GasEngine : Engine() { + + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt new file mode 100644 index 00000000000..74eaf9dc040 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seat.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.Cupholder + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +open class Seat @Inject +internal constructor(val cupholder: Cupholder) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt new file mode 100644 index 00000000000..20d7344a83c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Seatbelt.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +class Seatbelt diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt new file mode 100644 index 00000000000..ae612990bd0 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/Tire.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.RoundThing +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import java.util.LinkedHashSet + +open class Tire @Inject +constructor(constructorInjection: FuelTank) : RoundThing() { + + internal open var constructorInjection = NEVER_INJECTED + @Inject protected open var fieldInjection = NEVER_INJECTED + internal open var methodInjection = NEVER_INJECTED + + internal var constructorInjected: Boolean = false + + var superPrivateMethodInjected: Boolean = false + var superPackagePrivateMethodInjected: Boolean = false + var superProtectedMethodInjected: Boolean = false + var superPublicMethodInjected: Boolean = false + var subPrivateMethodInjected: Boolean = false + var subPackagePrivateMethodInjected: Boolean = false + var subProtectedMethodInjected: Boolean = false + var subPublicMethodInjected: Boolean = false + + var superPrivateMethodForOverrideInjected: Boolean = false + var superPackagePrivateMethodForOverrideInjected: Boolean = false + var subPrivateMethodForOverrideInjected: Boolean = false + var subPackagePrivateMethodForOverrideInjected: Boolean = false + var protectedMethodForOverrideInjected: Boolean = false + var publicMethodForOverrideInjected: Boolean = false + + var methodInjectedBeforeFields: Boolean = false + var subtypeFieldInjectedBeforeSupertypeMethods: Boolean = false + var subtypeMethodInjectedBeforeSupertypeMethods: Boolean = false + var similarPrivateMethodInjectedTwice: Boolean = false + var similarPackagePrivateMethodInjectedTwice: Boolean = false + var overriddenProtectedMethodInjectedTwice: Boolean = false + var overriddenPublicMethodInjectedTwice: Boolean = false + + var tirePackagePrivateMethod2Injected: Boolean = false + + var tirePackagePrivateMethod3Injected: Boolean = false + + var tirePackagePrivateMethod4Injected: Boolean = false + + init { + this.constructorInjection = constructorInjection + } + + @Inject internal fun supertypeMethodInjection(methodInjection: FuelTank) { + if (!hasTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + if (hasSpareTireBeenFieldInjected()) { + subtypeFieldInjectedBeforeSupertypeMethods = true + } + if (hasSpareTireBeenMethodInjected()) { + subtypeMethodInjectedBeforeSupertypeMethods = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (superPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + superPrivateMethodInjected = true + } + + @Inject internal open fun injectPackagePrivateMethod() { + if (superPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + superPackagePrivateMethodInjected = true + } + + @Inject protected open fun injectProtectedMethod() { + if (superProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + superProtectedMethodInjected = true + } + + @Inject + open fun injectPublicMethod() { + if (superPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + superPublicMethodInjected = true + } + + @Inject private fun injectPrivateMethodForOverride() { + subPrivateMethodForOverrideInjected = true + } + + @Inject internal open fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject protected open fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + @Inject + open fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + fun hasTireBeenFieldInjected(): Boolean { + return fieldInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenFieldInjected(): Boolean { + return false + } + + fun hasTireBeenMethodInjected(): Boolean { + return methodInjection != NEVER_INJECTED + } + + protected open fun hasSpareTireBeenMethodInjected(): Boolean { + return false + } + + @Inject override fun injectPackagePrivateMethod2() { + tirePackagePrivateMethod2Injected = true + } + + @Inject override fun injectPackagePrivateMethod3() { + tirePackagePrivateMethod3Injected = true + } + + override fun injectPackagePrivateMethod4() { + tirePackagePrivateMethod4Injected = true + } + + companion object { + + val NEVER_INJECTED = FuelTank() + + protected val moreProblems: Set = LinkedHashSet() + @Inject internal var staticFieldInjection = NEVER_INJECTED + internal var staticMethodInjection = NEVER_INJECTED + var staticMethodInjectedBeforeStaticFields: Boolean = false + var subtypeStaticFieldInjectedBeforeSupertypeStaticMethods: Boolean = false + var subtypeStaticMethodInjectedBeforeSupertypeStaticMethods: Boolean = false + + @Inject internal fun supertypeStaticMethodInjection(methodInjection: FuelTank) { + if (!Tire.hasBeenStaticFieldInjected()) { + staticMethodInjectedBeforeStaticFields = true + } + if (SpareTire.hasBeenStaticFieldInjected()) { + subtypeStaticFieldInjectedBeforeSupertypeStaticMethods = true + } + if (SpareTire.hasBeenStaticMethodInjected()) { + subtypeStaticMethodInjectedBeforeSupertypeStaticMethods = true + } + staticMethodInjection = methodInjection + } + + protected fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection != NEVER_INJECTED + } + + protected fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection != NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt new file mode 100644 index 00000000000..c93c1151954 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/V8Engine.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto + +import org.atinject.javaxtck.auto.accessories.SpareTire + +import javax.inject.Inject +import javax.inject.Named + +class V8Engine : GasEngine() { + init { + publicNoArgsConstructorInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + overriddenPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + /** + * Qualifiers are swapped from how they appear in the superclass. + */ + override fun injectQualifiers(seatA: Seat, @Drivers seatB: Seat, + tireA: Tire, @Named("spare") tireB: Tire) { + overriddenMethodInjected = true + if (seatA is DriversSeat + || seatB !is DriversSeat + || tireA is SpareTire + || tireB !is SpareTire) { + qualifiersInheritedFromOverriddenMethod = true + } + } + + override fun injectPackagePrivateMethodForOverride() { + subPackagePrivateMethodForOverrideInjected = true + } + + @Inject + override fun injectTwiceOverriddenWithOmissionInMiddle() { + overriddenTwiceWithOmissionInMiddleInjected = true + } + + override fun injectTwiceOverriddenWithOmissionInSubclass() { + overriddenTwiceWithOmissionInSubclassInjected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt new file mode 100644 index 00000000000..c1a1167ac29 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/Cupholder.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + +import org.atinject.javaxtck.auto.Seat + +import javax.inject.Inject +import javax.inject.Provider +import javax.inject.Singleton + +@Singleton +open class Cupholder @Inject +constructor(val seatProvider: Provider) diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt new file mode 100644 index 00000000000..aa6e8d89a27 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/RoundThing.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + + +import javax.inject.Inject + +open class RoundThing { + + var roundThingPackagePrivateMethod2Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod3Injected: Boolean = false + private set + + var roundThingPackagePrivateMethod4Injected: Boolean = false + private set + + @Inject open internal fun injectPackagePrivateMethod2() { + roundThingPackagePrivateMethod2Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod3() { + roundThingPackagePrivateMethod3Injected = true + } + + @Inject open internal fun injectPackagePrivateMethod4() { + roundThingPackagePrivateMethod4Injected = true + } +} diff --git a/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt new file mode 100644 index 00000000000..a85c6fb262c --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/kotlin/org/atinject/javaxtck/auto/accessories/SpareTire.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.atinject.javaxtck.auto.accessories + +import org.atinject.javaxtck.auto.FuelTank +import org.atinject.javaxtck.auto.Tire + +import javax.inject.Inject + +open class SpareTire @Inject +constructor(forSupertype: FuelTank, forSubtype: FuelTank) : Tire(forSupertype) { + + override var constructorInjection = Tire.NEVER_INJECTED + @Inject override var fieldInjection = Tire.NEVER_INJECTED + override var methodInjection = Tire.NEVER_INJECTED + + var spareTirePackagePrivateMethod2Injected: Boolean = false + private set + + var spareTirePackagePrivateMethod3Injected: Boolean = false + private set + + init { + this.constructorInjection = forSubtype + } + + @Inject internal fun subtypeMethodInjection(methodInjection: FuelTank) { + if (!hasSpareTireBeenFieldInjected()) { + methodInjectedBeforeFields = true + } + this.methodInjection = methodInjection + } + + @Inject private fun injectPrivateMethod() { + if (subPrivateMethodInjected) { + similarPrivateMethodInjectedTwice = true + } + subPrivateMethodInjected = true + } + + @Inject override fun injectPackagePrivateMethod() { + if (subPackagePrivateMethodInjected) { + similarPackagePrivateMethodInjectedTwice = true + } + subPackagePrivateMethodInjected = true + } + + @Inject override fun injectProtectedMethod() { + if (subProtectedMethodInjected) { + overriddenProtectedMethodInjectedTwice = true + } + subProtectedMethodInjected = true + } + + @Inject + override fun injectPublicMethod() { + if (subPublicMethodInjected) { + overriddenPublicMethodInjectedTwice = true + } + subPublicMethodInjected = true + } + + private fun injectPrivateMethodForOverride() { + superPrivateMethodForOverrideInjected = true + } + + override fun injectPackagePrivateMethodForOverride() { + superPackagePrivateMethodForOverrideInjected = true + } + + override fun injectProtectedMethodForOverride() { + protectedMethodForOverrideInjected = true + } + + override fun injectPublicMethodForOverride() { + publicMethodForOverrideInjected = true + } + + public override fun hasSpareTireBeenFieldInjected(): Boolean { + return fieldInjection !== Tire.NEVER_INJECTED + } + + @Override + public override fun hasSpareTireBeenMethodInjected(): Boolean { + return methodInjection !== Tire.NEVER_INJECTED + } + + @Inject override fun injectPackagePrivateMethod2() { + spareTirePackagePrivateMethod2Injected = true + } + + override fun injectPackagePrivateMethod3() { + spareTirePackagePrivateMethod3Injected = true + } + + companion object { + @Inject internal var staticFieldInjection = Tire.NEVER_INJECTED + internal var staticMethodInjection = Tire.NEVER_INJECTED + + @Inject internal fun subtypeStaticMethodInjection(methodInjection: FuelTank) { + if (!hasBeenStaticFieldInjected()) { + Tire.staticMethodInjectedBeforeStaticFields = true + } + staticMethodInjection = methodInjection + } + + fun hasBeenStaticFieldInjected(): Boolean { + return staticFieldInjection !== Tire.NEVER_INJECTED + } + + fun hasBeenStaticMethodInjected(): Boolean { + return staticMethodInjection !== Tire.NEVER_INJECTED + } + } +} diff --git a/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties new file mode 100644 index 00000000000..1fb30cae2d7 --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_en.properties @@ -0,0 +1,2 @@ +hello=Hello +hello.name=Hello {0} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties new file mode 100644 index 00000000000..31ecb39a52e --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/io/micronaut/docs/i18n/messages_es.properties @@ -0,0 +1,2 @@ +hello=Hola +hello.name=Hola {0} \ No newline at end of file diff --git a/test-suite-kotlin-ksp/src/test/resources/logback.xml b/test-suite-kotlin-ksp/src/test/resources/logback.xml new file mode 100644 index 00000000000..afaebf8e17d --- /dev/null +++ b/test-suite-kotlin-ksp/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt index 69b4fccf5ba..3dc10b3e7e1 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/inject/method/nullableinjection/C.kt @@ -16,8 +16,10 @@ package io.micronaut.inject.method.nullableinjection import jakarta.inject.Inject +import jakarta.inject.Singleton +@Singleton class C { internal var _a: A? = null internal var a: A diff --git a/validation/build.gradle b/validation/build.gradle index 457feca1d29..a2100d9e15e 100644 --- a/validation/build.gradle +++ b/validation/build.gradle @@ -35,6 +35,8 @@ dependencies { } //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] +//compileTestGroovy.groovyOptions.fork = true + spotless { java {