Skip to content

Commit

Permalink
Macros initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rsvato committed Aug 19, 2013
1 parent f89ecd8 commit 84c7576
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.griddynamics.genesis.template.dsl.groovy.transformations;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;

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, ElementType.METHOD})
@GroovyASTTransformationClass({"com.griddynamics.genesis.template.dsl.groovy.transformations.MacroASTTransformation"})
public @interface MacroExpand {
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import com.griddynamics.genesis.annotation.RemoteGateway
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import support.VariablesSupport
import com.griddynamics.genesis.template.dsl.groovy.transformations.{PhaseContainer, Context}
import com.griddynamics.genesis.template.dsl.groovy.transformations.{MacroExpand, PhaseContainer, Context}

@RemoteGateway("groovy template service")
class GroovyTemplateService(val templateRepoService : TemplateRepoService,
Expand Down Expand Up @@ -147,8 +147,11 @@ class GroovyTemplateService(val templateRepoService : TemplateRepoService,

try {
val compilerConfiguration = new CompilerConfiguration()
compilerConfiguration.addCompilationCustomizers(new ASTTransformationCustomizer(classOf[Context]),
new ASTTransformationCustomizer(classOf[PhaseContainer]))
compilerConfiguration.addCompilationCustomizers(
new ASTTransformationCustomizer(classOf[MacroExpand]),
new ASTTransformationCustomizer(classOf[Context]),
new ASTTransformationCustomizer(classOf[PhaseContainer])
)
val groovyShell = new GroovyShell(binding, compilerConfiguration)
groovyShell.evaluate(body)
projectId.foreach (evaluateIncludes(_, templateDecl.includes, groovyShell))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.griddynamics.genesis.template.dsl.groovy.transformations

import org.codehaus.groovy.transform.{ASTTransformation, GroovyASTTransformation}
import org.codehaus.groovy.control.{SourceUnit, CompilePhase}
import com.griddynamics.genesis.util.Logging
import org.codehaus.groovy.ast.{CodeVisitorSupport, ASTNode}
import org.codehaus.groovy.ast.expr._
import scala.collection.mutable
import org.codehaus.groovy.ast.stmt.{ExpressionStatement, Statement, BlockStatement}
import java.util
import scala.collection.JavaConversions._

@GroovyASTTransformation(phase = CompilePhase.CONVERSION)
class MacroASTTransformation extends ASTTransformation with Logging {
def visit(nodes: Array[ASTNode], source: SourceUnit) {
log.trace(s"Inside a macros transformer: ${nodes.length}")
val methods = source.getAST.getStatementBlock.getStatements
val it = methods.iterator().next()
val collector = new MacroCollector
it.visit(new NameMatchArgumentTransformer("template", collector))
if (collector.macros.size > 0) {
methods.iterator().next().visit(new MacroExpandVisitor(collector.macros.toMap))
}
}
}

class MacroExpandVisitor(val macrodefs: Map[String, ClosureExpression]) extends CodeVisitorSupport with Logging {

val nameExtract: PartialFunction[Expression, String] = {
case call if call.isInstanceOf[MethodCallExpression] => call.asInstanceOf[MethodCallExpression].getMethodAsString
}

def replaceStatement(statement: BlockStatement, expression: ExpressionStatement): Statement = {
val initialStatements: util.List[Statement] = statement.getStatements
val position = initialStatements.indexOf(expression)
expression.getExpression match {
case mce: MethodCallExpression =>
val key: String = nameExtract(expression.getExpression)
log.trace(s"Expanding call of $key at position $position")
macrodefs.get(key).map(e => {
initialStatements.set(position, e.getCode)
}) orElse(throw new IllegalArgumentException(s"Macro $key not found anywhere in template"))
case other =>
throw new IllegalArgumentException("Macro call must have a form of function call: " + expression.getExpression)
}
val cleared = initialStatements.filterNot(_.getStatementLabel == "macro")
new BlockStatement(cleared, statement.getVariableScope)
}

override def visitClosureExpression(call: ClosureExpression) {
call.getCode match {
case bs: BlockStatement => {
bs.getStatements.foreach( statement =>
statement match {
case es: ExpressionStatement => {
if (es.getStatementLabel == "macro") {
call.setCode(replaceStatement(bs, es))
} else {
es.getExpression match {
case call: MethodCallExpression => {
this.visitMethodCallExpression(call)
}
case other =>
}
}
}
case other =>
}
)
}
case other =>
}
}



override def visitMethodCallExpression(call: MethodCallExpression) {
call.getArguments match {
case ale: ArgumentListExpression => {
ale.visit(this)
}
case _ =>
}
}
}

class MacroCollector extends ExpressionTransformer with Logging {
var macros: mutable.Map[String, ClosureExpression] = new mutable.HashMap[String, ClosureExpression]()
def transform(expression: Expression) : Expression = {
expression match {
case a: ClosureExpression => {
a.getCode match {
case block: BlockStatement => {
val statements: util.List[Statement] = block.getStatements
val seq: Seq[Statement] = statements.toSeq
val macroDefs: Seq[Statement] = seq.filter(evalStatement)
if (macroDefs.size > 0) {
val rest: Seq[Statement] = seq.diff(macroDefs)
val content = new BlockStatement(rest.toArray, block.getVariableScope)
a.setCode(content)
}
macroDefs.foreach(m => {
m match {
case statement: ExpressionStatement => {
statement.getExpression match {
case mce: MethodCallExpression => saveExpr(mce)
case other =>
}
}
case other =>
}
})
a
}
case x => a
}

}
case x => x
}
}

def saveExpr(expression: MethodCallExpression) {
def parseArgurmentList(mapExpr: NamedArgumentListExpression): (String, ClosureExpression) = {
val top = mapExpr.getMapEntryExpressions.iterator().next()
val key: String = top.getKeyExpression.asInstanceOf[ConstantExpression].getValue.toString
val closure = top.getValueExpression.asInstanceOf[ClosureExpression]
(key, closure)
}
val (name, body) = expression.getArguments match {
case mapExpr: NamedArgumentListExpression =>
parseArgurmentList(mapExpr)
case tuple: TupleExpression =>
parseArgurmentList(tuple.getExpression(0).asInstanceOf[NamedArgumentListExpression])
}
macros(name) = body
}

def evalStatement(statement: Statement): Boolean = {
statement match {
case es: ExpressionStatement => {
es.getExpression match {
case mce: MethodCallExpression => {
mce.getMethodAsString == "defmacro"
}
case other => false
}
}
case other => {
false
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ class PhaseContainerASTTransformation extends ASTTransformation with Logging{
def visit(nodes: Array[ASTNode], source: SourceUnit) {
val methods = source.getAST.getStatementBlock.getStatements
val it = methods.iterator().next()
it.visit(new PhaseEraser)
it.visit(new NameMatchArgumentTransformer("steps", new PhaseTransformer))
}
}

class PhaseEraser extends CodeVisitorSupport with Logging {
class NameMatchArgumentTransformer(name: String, transformer: ExpressionTransformer) extends CodeVisitorSupport with Logging {
override def visitMethodCallExpression(call: MethodCallExpression) {
if (call.getMethodAsString == "steps") {
val expr = call.getArguments.transformExpression(new PhaseTransformer)
if (call.getMethodAsString == name) {
val expr = call.getArguments.transformExpression(transformer)
call.setArguments(expr)
} else {
call.getArguments.visit(this)
Expand Down
43 changes: 43 additions & 0 deletions backend/src/test/resources/groovy/Macros.genesis
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package groovy
template {
name("Macros")
version("0.1")
createWorkflow("create")
destroyWorkflow("destroy")


defmacro "create_steps": {
teststep {
text = "test from macro"
}
}

workflow("create") {
steps {
teststep {
text = "test input"
}
teststep {
text = "another input"
}
}
}

workflow("macros") {
steps {
teststep {
text = "It was here"
}
macro:create_steps()
}
}

workflow("destroy") {
steps {
teststep {
phase = "undeploy"
text = "destroy"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.griddynamics.genesis.service.impl

import org.scalatest.junit.AssertionsForJUnit
import org.scalatest.mock.MockitoSugar
import org.springframework.core.convert.support.DefaultConversionService
import com.griddynamics.genesis.template.ListVarDSFactory
import com.griddynamics.genesis.util.{Logging, IoUtil}
import org.mockito.{Matchers, Mockito}
import org.mockito.Mockito._
import scala.Some
import org.junit.Test
import com.griddynamics.genesis.template.support.DatabagDataSourceFactory
import com.griddynamics.genesis.cache.NullCacheManager
import com.griddynamics.genesis.template.VersionedTemplate
import com.griddynamics.genesis.api
import com.griddynamics.genesis.plugin.StepBuilder


class MacrosTest extends AssertionsForJUnit with MockitoSugar with DSLTestUniverse with Logging {
val templateService = new GroovyTemplateService(templateRepoService,
List(new DoNothingStepBuilderFactory), new DefaultConversionService,
Seq(new ListVarDSFactory, new DependentListVarDSFactory,
new DatabagDataSourceFactory(databagRepository)), databagRepository, configService, NullCacheManager)

val body = IoUtil.streamAsString(classOf[GroovyTemplateServiceTest].getResourceAsStream("/groovy/Macros.genesis"))

Mockito.when(templateRepository.listSources()).thenReturn(Map(VersionedTemplate("1") -> body))
when(configService.get(Matchers.any(), Matchers.any())).thenReturn(Some(new api.Configuration(Some(0), "", 0, None, Map())))

@Test
def testPhaseApplied() {
val createWorkflow = templateService.findTemplate(0, "Macros", "0.1", 0).flatMap(_.getWorkflow("macros")).get
val steps = createWorkflow.embody(Map())
expectResult(2)(steps.regular.size)
val initialPhase: Option[StepBuilder] = steps.regular.find(_.phase == "auto_0")
assert(initialPhase.isDefined)
assert(initialPhase.get.getPrecedingPhases.isEmpty)
}
}

0 comments on commit 84c7576

Please sign in to comment.