Skip to content

Commit

Permalink
Add stricter checking to Groovy function definitions and routing
Browse files Browse the repository at this point in the history
  • Loading branch information
graemerocher committed May 31, 2018
1 parent b407a10 commit 2bcf3f3
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public Object get(String key) {
}

@Override
public String getName() {
public final String getName() {
return NameUtils.hyphenate(getClass().getSimpleName());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,14 @@ class FunctionTransform implements ASTTransformation {
for (node in source.getAST().classes) {
if (node.isScript()) {
node.setSuperClass(ClassHelper.makeCached(FunctionScript))
MethodNode functionMethod = node.methods.find() { method -> !method.isAbstract() && !method.isStatic() && method.isPublic() && method.name != 'run' }
List<MethodNode> methods = node.methods.findAll() { method ->
!method.isAbstract() && !method.isStatic() && method.isPublic() && method.name != 'run' && !(NameUtils.isSetterName(method.name) && node.getField(NameUtils.getPropertyNameForSetter(method.name))) && method.declaringClass.name != FunctionScript.name
}
if(methods.size() > 1) {
AstMessageUtils.error(source, node, "Function ["+node.name+"] must have exactly one public method that represents the function")
return
}
MethodNode functionMethod = methods[0]
if (functionMethod == null) {
AstMessageUtils.error(source, node, "Function must have at least one public method")
} else {
Expand Down Expand Up @@ -205,6 +212,7 @@ class FunctionTransform implements ASTTransformation {
functionName -= '-function'

functionBean.setMember("value", constX(functionName))
functionBean.setMember("method", constX(functionMethod.name))
node.addAnnotation(functionBean)
node.addConstructor(
applicationContextConstructor
Expand All @@ -218,16 +226,7 @@ class FunctionTransform implements ASTTransformation {
}
} else {
if (argLength == 0) {
def returnType = ClassHelper.getWrapper(functionMethod.returnType.plainNodeReference)
node.addInterface(GenericsUtils.makeClassSafeWithGenerics(
ClassHelper.make(Supplier).plainNodeReference,
new GenericsType(returnType)
))
def mn = new MethodNode("get", Modifier.PUBLIC, functionMethod.returnType.plainNodeReference, AstUtils.ZERO_PARAMETERS, null, stmt(
callX(varX("this"), functionMethod.getName())
))
mn.addAnnotation(new AnnotationNode(AstUtils.INTERNAL_ANNOTATION))
node.addMethod(mn)
implementSupplier(functionMethod, node)
} else {
if (argLength == 1) {
implementFunction(functionMethod, node)
Expand All @@ -244,6 +243,19 @@ class FunctionTransform implements ASTTransformation {
}
}

protected void implementSupplier(MethodNode functionMethod, ClassNode node) {
def returnType = ClassHelper.getWrapper(functionMethod.returnType.plainNodeReference)
node.addInterface(GenericsUtils.makeClassSafeWithGenerics(
ClassHelper.make(Supplier).plainNodeReference,
new GenericsType(returnType)
))
def mn = new MethodNode("get", Modifier.PUBLIC, returnType, AstUtils.ZERO_PARAMETERS, null, stmt(
callX(varX("this"), functionMethod.getName())
))
mn.addAnnotation(new AnnotationNode(AstUtils.INTERNAL_ANNOTATION))
node.addMethod(mn)
}

protected void implementConsumer(MethodNode functionMethod, ClassNode classNode) {
implementFunction(functionMethod, classNode, Consumer, ClassHelper.VOID_TYPE, "accept")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.micronaut.function.groovy

import io.micronaut.context.ApplicationContext
import io.micronaut.function.FunctionBean
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
Expand All @@ -24,6 +25,7 @@ import io.micronaut.http.client.HttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.reactivex.Flowable
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import spock.lang.Ignore
import spock.lang.Shared
import spock.lang.Specification
Expand Down Expand Up @@ -55,10 +57,73 @@ int round(float value) {
''')

expect:
functionClass.getAnnotation(FunctionBean).method() == 'round'
functionClass.main(['-d','1.6f'] as String[])
TestFunctionExitHandler.lastError == null
}

void 'test parse supplier'() {
given:
CompilerConfiguration configuration = new CompilerConfiguration()
configuration.optimizationOptions['micronaut.function.compile'] = true
GroovyClassLoader gcl = new GroovyClassLoader(FunctionTransformSpec.classLoader, configuration)

Class functionClass = gcl.parseClass('''
int val() {
return 10
}
''')

expect:
functionClass.getAnnotation(FunctionBean).method() == 'val'
functionClass.main([] as String[])
TestFunctionExitHandler.lastError == null
}

void 'test parse two functions'() {
given:
CompilerConfiguration configuration = new CompilerConfiguration()
configuration.optimizationOptions['micronaut.function.compile'] = true
GroovyClassLoader gcl = new GroovyClassLoader(FunctionTransformSpec.classLoader, configuration)

when:
Class functionClass = gcl.parseClass('''
int round(float value) {
Math.round(value)
}
int round2(float value) {
Math.round(value)
}
''')

then:
def e = thrown(MultipleCompilationErrorsException)
e.message.contains("must have exactly one public method that represents the function")
}

void 'test parse function and field'() {
given:
CompilerConfiguration configuration = new CompilerConfiguration()
configuration.optimizationOptions['micronaut.function.compile'] = true
GroovyClassLoader gcl = new GroovyClassLoader(FunctionTransformSpec.classLoader, configuration)

when:
Class functionClass = gcl.parseClass('''
import groovy.transform.EqualsAndHashCode
import groovy.transform.Field
import io.micronaut.core.convert.*
@Field ConversionService conversionService
int round(float value) {
Math.round(value)
}
''')

then:
functionClass
}

//TODO: Fix me and remove @Ignore
@Ignore
void 'test parse JSON marshalling function'() {
Expand Down
1 change: 1 addition & 0 deletions function-web/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ dependencies {

testCompile project(":http-client")
testCompile project(":inject-groovy")
testCompile project(":inject-java")
testCompile project(":http-server-netty")
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,35 +88,44 @@ public AnnotatedFunctionRouteBuilder(
@SuppressWarnings("unchecked")
@Override
public void process(BeanDefinition<?> beanDefinition, ExecutableMethod<?, ?> method) {
FunctionBean annotation = method.getAnnotation(FunctionBean.class);
FunctionBean annotation = beanDefinition.getAnnotation(FunctionBean.class);
if (annotation != null) {
String functionName = annotation.value();
String functionPath = functionName;
String methodName = method.getMethodName();
Class<?> declaringType = method.getDeclaringType();
if (StringUtils.isEmpty(functionPath)) {
String typeName = declaringType.getSimpleName();
if (typeName.contains("$")) {
// generated lambda
functionPath = contextPath + NameUtils.hyphenate(method.getMethodName());
} else {
functionPath = contextPath + NameUtils.hyphenate(typeName);
}
} else {
functionPath = contextPath + functionPath;
}
String functionName = annotation.value();

UriRoute route = null;
if (Stream.of(java.util.function.Function.class, Consumer.class, BiFunction.class, BiConsumer.class).anyMatch(type -> type.isAssignableFrom(declaringType))) {

String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
route = POST(functionPath, method);
} else if (Supplier.class.isAssignableFrom(declaringType)) {
} else if (Supplier.class.isAssignableFrom(declaringType) && methodName.equals("get")) {
String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
route = GET(functionPath, method);
} else {
String functionMethod = annotation.method();
if (StringUtils.isNotEmpty(functionMethod)) {
if (functionMethod.equals(methodName)) {
int argCount = method.getArguments().length;
if (argCount < 3) {
String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
if (argCount == 0) {
route = GET(functionPath, method);
}
else {
route = POST(functionPath, method);
}
}
}
}
}

if (route != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Created Route to Function: {}", route);
}

String functionPath = resolveFunctionPath(methodName, declaringType, functionName);
availableFunctions.put(functionName, URI.create(functionPath));
Class[] argumentTypes = method.getArgumentTypes();
int argCount = argumentTypes.length;
Expand All @@ -133,6 +142,22 @@ public void process(BeanDefinition<?> beanDefinition, ExecutableMethod<?, ?> met
}
}

private String resolveFunctionPath(String methodName, Class<?> declaringType, String functionName) {
String functionPath = functionName;
if (StringUtils.isEmpty(functionPath)) {
String typeName = declaringType.getSimpleName();
if (typeName.contains("$")) {
// generated lambda
functionPath = contextPath + NameUtils.hyphenate(methodName);
} else {
functionPath = contextPath + NameUtils.hyphenate(typeName);
}
} else {
functionPath = contextPath + functionPath;
}
return functionPath;
}

/**
* A map of available functions with the key being the function name and the value being the function URI.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,12 @@ class WebFunctionSpec extends Specification {

@FunctionBean("supplier/string")
static class StringSupplier implements Supplier<String> {

String getValue() {
return "value"
}
@Override
String get() {
return "value"
return getValue()
}
}

Expand Down
15 changes: 15 additions & 0 deletions function/src/main/java/io/micronaut/function/FunctionBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Executable;

import javax.inject.Singleton;
Expand All @@ -43,5 +44,19 @@
/**
* @return An optional ID of the function which may or may not be used depending on the target platform
*/
@AliasFor(member = "name")
String value() default "";

/**
* @return An optional ID of the function which may or may not be used depending on the target platform
*/
@AliasFor(member = "value")
String name() default "";

/**
* The method name of a function within the class that is the function to invoke. The method should take no more than two arguments
*
* @return The method name
*/
String method() default "";
}
2 changes: 1 addition & 1 deletion travis-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fi
if [[ $EXIT_STATUS -ne 0 ]]; then

./gradlew aggregateReports

git clone https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -b gh-pages gh-pages --single-branch > /dev/null

cd gh-pages
Expand Down

0 comments on commit 2bcf3f3

Please sign in to comment.