package ansible import ansible.AnsibleModule._ import argonaut.Argonaut._ import better.files._ import scala.annotation.{StaticAnnotation, compileTimeOnly} import scala.language.experimental.macros import scala.reflect.macros.whitebox @compileTimeOnly("enable macro paradise to expand macro annotations") class expand extends StaticAnnotation { def macroTransform(annottees: Any*): Any = macro Expander.expand_impl } object Expander { val tmpDir = System.getProperty("java.io.tmpdir") def expand_impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val gen = new CodeGen(c) val files = (tmpDir / "parsed_modules").listRecursively.filter(_.extension.contains(".json")).toList val ansibleModules = files.map { f => f.contentAsString.decodeValidation[AnsibleModule].fold(err => throw new Exception(s"Failed reading the json representation of the Ansible module: ${f.path}, error: $err"), identity ) } def generateModule(m: AnsibleModule): (ModuleDef, ClassDef) = { val moduleTypeName = TypeName(camelize(m.name)) val moduleTermName = TermName(camelize(m.name)) val sortedOptions = m.options.sortBy(_.required)(Ordering[Boolean].reverse) val (enumTypes, enumValues, fields) = sortedOptions.foldLeft((List.empty[ClassDef], List.empty[ModuleDef], Vector.empty[ValDef])) { case ((enumTypes, enumValues, fields), option) => option match { case o: BooleanOption => (enumTypes, enumValues, fields :+ gen.boolVal(o).asInstanceOf[ValDef]) case o: StringOption => (enumTypes, enumValues, fields :+ gen.stringVal(o).asInstanceOf[ValDef]) case o: EnumOption => (gen.enumClassDef(o).asInstanceOf[ClassDef] :: enumTypes, gen.enumObjectDef(o).asInstanceOf[ModuleDef] :: enumValues, fields :+ gen.enumVal(o, m.name).asInstanceOf[ValDef]) } } val jsonEncoderVal = gen.moduleObjectJsonEncoder(m).asInstanceOf[ValDef] val enumSetters = m.enumOptions.map(gen.enumSetterDef(m.name)).asInstanceOf[List[DefDef]] val objectDef = q""" object $moduleTermName { $jsonEncoderVal ..$enumTypes ..$enumValues } """.asInstanceOf[ModuleDef] val classDef = q""" case class $moduleTypeName(..$fields) extends ansible.Module { override def call = ModuleCall(this.asJson) ..$enumSetters } """.asInstanceOf[ClassDef] (objectDef, classDef) } annottees.map(_.tree) match { case List(q"trait $_") => val (objectDefs, classDefs) = ansibleModules.map(m => generateModule(m)).unzip c.Expr[Any]( q""" import argonaut._ import Argonaut._ ..$objectDefs ..$classDefs """) case _ => c.abort(c.enclosingPosition, "@expand should annotate a trait") } } private def camelize(str: String): String = str.split('_').map { s => if (s.isEmpty) "" else s.head.toUpper.toString + s.tail.toLowerCase }.mkString }