-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In 1.x Grgit, operations were made available to the user via the MOP with a methodMissing implementation. This meant that you couldn't get IDE support to see that you were calling a valid method. There's now an ASTTransformation that generates four methods for each operation that take different arguments: - no args - map - consumer - closure These should provide more static help in the IDE and generally increase consistency. This resolves #85.
- Loading branch information
1 parent
5f5a960
commit f7fbff0
Showing
40 changed files
with
414 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
src/main/groovy/org/ajoberstar/grgit/internal/OpSyntax.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Copyright 2012-2015 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES 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.ajoberstar.grgit.internal | ||
|
||
import java.util.concurrent.Callable | ||
import java.util.function.Consumer | ||
|
||
import groovy.transform.PackageScope | ||
|
||
class OpSyntax { | ||
static def noArgOperation(Class<Callable> opClass, Object[] classArgs) { | ||
def op = opClass.newInstance(classArgs) | ||
return op.call() | ||
} | ||
|
||
static def mapOperation(Class<Callable> opClass, Object[] classArgs, Map args) { | ||
def op = opClass.newInstance(classArgs) | ||
|
||
args.forEach { key, value -> | ||
op[key] = value | ||
} | ||
|
||
return op.call() | ||
} | ||
|
||
static def consumerOperation(Class<Callable> opClass, Object[] classArgs, Consumer arg) { | ||
def op = opClass.newInstance(classArgs) | ||
arg.accept(op) | ||
return op.call() | ||
} | ||
|
||
static def closureOperation(Class<Callable> opClass, Object[] classArgs, Closure closure) { | ||
def op = opClass.newInstance(classArgs) | ||
|
||
Object originalDelegate = closure.delegate | ||
closure.delegate = op | ||
closure.resolveStrategy = Closure.DELEGATE_FIRST | ||
closure.call() | ||
closure.delegate = originalDelegate | ||
|
||
return op.call() | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/groovy/org/ajoberstar/grgit/internal/Operation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright 2012-2015 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES 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.ajoberstar.grgit.internal; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Retention(RetentionPolicy.SOURCE) | ||
@Target(ElementType.TYPE) | ||
public @interface Operation { | ||
String value(); | ||
} |
33 changes: 33 additions & 0 deletions
33
src/main/groovy/org/ajoberstar/grgit/internal/WithOperations.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright 2012-2015 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES 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.ajoberstar.grgit.internal; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
import java.util.concurrent.Callable; | ||
|
||
import org.codehaus.groovy.transform.GroovyASTTransformationClass; | ||
|
||
@Retention(RetentionPolicy.SOURCE) | ||
@Target(ElementType.TYPE) | ||
@GroovyASTTransformationClass("org.ajoberstar.grgit.internal.WithOperationsASTTransformation") | ||
public @interface WithOperations { | ||
Class<? extends Callable<?>>[] staticOperations() default {}; | ||
|
||
Class<? extends Callable<?>>[] instanceOperations() default {}; | ||
} |
231 changes: 231 additions & 0 deletions
231
src/main/groovy/org/ajoberstar/grgit/internal/WithOperationsASTTransformation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
/* | ||
* Copyright 2012-2015 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES 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.ajoberstar.grgit.internal; | ||
|
||
import java.lang.reflect.Modifier; | ||
import java.lang.reflect.ParameterizedType; | ||
import java.lang.reflect.Type; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.function.Consumer; | ||
|
||
import org.codehaus.groovy.ast.ASTNode; | ||
import org.codehaus.groovy.ast.AnnotatedNode; | ||
import org.codehaus.groovy.ast.AnnotationNode; | ||
import org.codehaus.groovy.ast.ClassHelper; | ||
import org.codehaus.groovy.ast.ClassNode; | ||
import org.codehaus.groovy.ast.FieldNode; | ||
import org.codehaus.groovy.ast.GenericsType; | ||
import org.codehaus.groovy.ast.MethodNode; | ||
import org.codehaus.groovy.ast.Parameter; | ||
import org.codehaus.groovy.ast.expr.ArgumentListExpression; | ||
import org.codehaus.groovy.ast.expr.ArrayExpression; | ||
import org.codehaus.groovy.ast.expr.ClassExpression; | ||
import org.codehaus.groovy.ast.expr.Expression; | ||
import org.codehaus.groovy.ast.expr.FieldExpression; | ||
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; | ||
import org.codehaus.groovy.ast.expr.VariableExpression; | ||
import org.codehaus.groovy.ast.stmt.ExpressionStatement; | ||
import org.codehaus.groovy.ast.stmt.Statement; | ||
import org.codehaus.groovy.control.CompilePhase; | ||
import org.codehaus.groovy.control.SourceUnit; | ||
import org.codehaus.groovy.transform.AbstractASTTransformation; | ||
import org.codehaus.groovy.transform.GroovyASTTransformation; | ||
|
||
import groovy.lang.Closure; | ||
|
||
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) | ||
public class WithOperationsASTTransformation extends AbstractASTTransformation { | ||
|
||
@Override | ||
public void visit(ASTNode[] nodes, SourceUnit source) { | ||
AnnotationNode annotation = (AnnotationNode) nodes[0]; | ||
AnnotatedNode parent = (AnnotatedNode) nodes[1]; | ||
|
||
if (parent instanceof ClassNode) { | ||
ClassNode clazz = (ClassNode) parent; | ||
List<ClassNode> staticOps = getClassList(annotation, "staticOperations"); | ||
List<ClassNode> instanceOps = getClassList(annotation, "instanceOperations"); | ||
|
||
staticOps.forEach( | ||
op -> { | ||
makeMethods(clazz, op, true); | ||
}); | ||
instanceOps.forEach( | ||
op -> { | ||
makeMethods(clazz, op, false); | ||
}); | ||
} | ||
} | ||
|
||
private void makeMethods(ClassNode targetClass, ClassNode opClass, boolean isStatic) { | ||
AnnotationNode annotation = | ||
opClass | ||
.getAnnotations(classFromType(Operation.class)) | ||
.stream() | ||
.findFirst() | ||
.orElseThrow( | ||
() -> | ||
new IllegalArgumentException( | ||
"Class is not annotated with @Operation: " + opClass)); | ||
String opName = getMemberStringValue(annotation, "value"); | ||
ClassNode opReturn = opClass.getDeclaredMethod("call", new Parameter[] {}).getReturnType(); | ||
|
||
targetClass.addMethod(makeNoArgMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeMapMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeConsumerMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
targetClass.addMethod(makeClosureMethod(targetClass, opName, opClass, opReturn, isStatic)); | ||
} | ||
|
||
private MethodNode makeNoArgMethod( | ||
ClassNode targetClass, | ||
String opName, | ||
ClassNode opClass, | ||
ClassNode opReturn, | ||
boolean isStatic) { | ||
Parameter[] parms = new Parameter[] {}; | ||
|
||
Statement code = | ||
new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"noArgOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression( | ||
classFromType(Object.class), opConstructorParms(targetClass, isStatic))))); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeMapMethod( | ||
ClassNode targetClass, | ||
String opName, | ||
ClassNode opClass, | ||
ClassNode opReturn, | ||
boolean isStatic) { | ||
ClassNode parmType = classFromType(Map.class); | ||
GenericsType[] generics = genericsFromTypes(String.class, Object.class); | ||
parmType.setGenericsTypes(generics); | ||
Parameter[] parms = new Parameter[] {new Parameter(parmType, "args")}; | ||
|
||
Statement code = | ||
new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"mapOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression( | ||
classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("args")))); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeConsumerMethod( | ||
ClassNode targetClass, | ||
String opName, | ||
ClassNode opClass, | ||
ClassNode opReturn, | ||
boolean isStatic) { | ||
ClassNode parmType = classFromType(Consumer.class); | ||
GenericsType[] generics = new GenericsType[] {new GenericsType(opReturn)}; | ||
parmType.setGenericsTypes(generics); | ||
Parameter[] parms = new Parameter[] {new Parameter(parmType, "arg")}; | ||
|
||
Statement code = | ||
new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"consumerOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression( | ||
classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("arg")))); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
private MethodNode makeClosureMethod( | ||
ClassNode targetClass, | ||
String opName, | ||
ClassNode opClass, | ||
ClassNode opReturn, | ||
boolean isStatic) { | ||
ClassNode parmType = classFromType(Closure.class); | ||
Parameter[] parms = new Parameter[] {new Parameter(parmType, "arg")}; | ||
|
||
Statement code = | ||
new ExpressionStatement( | ||
new StaticMethodCallExpression( | ||
classFromType(OpSyntax.class), | ||
"closureOperation", | ||
new ArgumentListExpression( | ||
new ClassExpression(opClass), | ||
new ArrayExpression( | ||
classFromType(Object.class), opConstructorParms(targetClass, isStatic)), | ||
new VariableExpression("arg")))); | ||
|
||
return new MethodNode(opName, modifiers(isStatic), opReturn, parms, new ClassNode[] {}, code); | ||
} | ||
|
||
public ClassNode classFromType(Type type) { | ||
if (type instanceof Class) { | ||
Class<?> clazz = (Class<?>) type; | ||
if (clazz.isPrimitive()) { | ||
return ClassHelper.make(clazz); | ||
} else { | ||
return ClassHelper.makeWithoutCaching(clazz, false); | ||
} | ||
} else if (type instanceof ParameterizedType) { | ||
ParameterizedType ptype = (ParameterizedType) type; | ||
ClassNode base = classFromType(ptype.getRawType()); | ||
GenericsType[] generics = genericsFromTypes(ptype.getActualTypeArguments()); | ||
base.setGenericsTypes(generics); | ||
return base; | ||
} else { | ||
throw new IllegalArgumentException("Unsupported type: " + type.getClass()); | ||
} | ||
} | ||
|
||
public GenericsType[] genericsFromTypes(Type... types) { | ||
return Arrays.stream(types) | ||
.map(this::classFromType) | ||
.map(GenericsType::new) | ||
.toArray(size -> new GenericsType[size]); | ||
} | ||
|
||
public List<Expression> opConstructorParms(ClassNode targetClass, boolean isStatic) { | ||
if (isStatic) { | ||
return Collections.emptyList(); | ||
} else { | ||
FieldNode repo = targetClass.getField("repository"); | ||
return Arrays.asList(new FieldExpression(repo)); | ||
} | ||
} | ||
|
||
public int modifiers(boolean isStatic) { | ||
int modifiers = Modifier.PUBLIC | Modifier.FINAL; | ||
if (isStatic) { | ||
modifiers |= Modifier.STATIC; | ||
} | ||
return modifiers; | ||
} | ||
} |
Oops, something went wrong.