diff --git a/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTest.scala b/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTest.scala index d705df0d4..785c9a60a 100644 --- a/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTest.scala +++ b/src/it/scala/temple/generate/docker/DockerGeneratorIntegrationTest.scala @@ -48,6 +48,7 @@ class DockerGeneratorIntegrationTest extends HadolintSpec with Matchers with Bef val templefile = Templefile( "ExampleProject", ProjectBlock(), + targets = Map(), services = Map("ComplexService" -> DockerGeneratorIntegrationTestData.sampleService), ) diff --git a/src/main/scala/temple/DSL/semantics/Analyzer.scala b/src/main/scala/temple/DSL/semantics/Analyzer.scala index fe66e2f5e..cf2e6457b 100644 --- a/src/main/scala/temple/DSL/semantics/Analyzer.scala +++ b/src/main/scala/temple/DSL/semantics/Analyzer.scala @@ -181,6 +181,11 @@ object Analyzer { registerKeywordWithContext("writable", "by", TokenArgType)(Writable) } + /** A parser of Metadata items that can occur in target blocks */ + private val parseTargetMetadata = new MetadataParser[TargetMetadata] { + registerKeywordWithContext("language", TokenArgType)(TargetLanguage) + } + /** * Parse a service block from a list of entries into the distinct attributes, metadatas and structs * @@ -236,6 +241,9 @@ object Analyzer { private def parseProjectBlock(entries: Seq[Entry])(implicit context: SemanticContext): ProjectBlock = ProjectBlock(parseMetadataBlock(entries, parseProjectMetadata)) + private def parseTargetBlock(entries: Seq[Entry])(implicit context: SemanticContext): TargetBlock = + TargetBlock(parseMetadataBlock(entries, parseTargetMetadata)) + private def parseStructBlock(entries: Seq[Entry])(implicit context: SemanticContext): StructBlock = { val attributes = mutable.LinkedHashMap[String, AbstractAttribute]() val metadata = parseBlockWithMetadata(entries, parseStructMetadata) { @@ -249,13 +257,15 @@ object Analyzer { /** * Turns an AST of a Templefile into a semantic representation * @param templefile the AST parsed from the Templefile - * @return A semantic representation of the project, as well as all the services, defined in the Templefile + * @return A semantic representation of the project, as well as all the targets and services, defined in the + * Templefile * @throws SemanticParsingException when there is no project information, as well as when any of the definitions are * malformed */ private[semantics] def parseSemantics(templefile: syntax.Templefile): Templefile = { var projectNameBlock: Option[(String, ProjectBlock)] = None + val targets = mutable.LinkedHashMap[String, TargetBlock]() val services = mutable.LinkedHashMap[String, ServiceBlock]() templefile.foreach { @@ -268,6 +278,8 @@ object Analyzer { projectNameBlock.fold { projectNameBlock = Some(key -> parseProjectBlock(entries)) } { case (str, _) => context.fail(s"Second project found in addition to $str,") } + // TODO: error message + case "target" => targets.safeInsert(key -> parseTargetBlock(entries)) case _ => context.fail(s"Unknown block type") } @@ -277,13 +289,14 @@ object Analyzer { throw new SemanticParsingException("Temple file has no project block") } - Templefile(projectName, projectBlock, services.to(ListMap)) + Templefile(projectName, projectBlock, targets.to(ListMap), services.to(ListMap)) } /** * Turns an AST of a Templefile into a semantic representation and validates it * @param templefileAST the AST parsed from the Templefile - * @return A semantic representation of the project, as well as all the services, defined in the Templefile + * @return A semantic representation of the project, as well as all the targets and services, defined in the + * Templefile * @throws SemanticParsingException when there is no project information, as well as when any of the definitions are * malformed or the contents is not valid */ diff --git a/src/main/scala/temple/DSL/semantics/ServiceRenamer.scala b/src/main/scala/temple/DSL/semantics/ServiceRenamer.scala index 2a2affb5b..5c4c38e51 100644 --- a/src/main/scala/temple/DSL/semantics/ServiceRenamer.scala +++ b/src/main/scala/temple/DSL/semantics/ServiceRenamer.scala @@ -9,6 +9,12 @@ case class ServiceRenamer(renamingMap: Map[String, String]) { private def rename(string: String): String = renamingMap.getOrElse(string, { throw new NoSuchElementException(s"Key $string missing from renaming map") }) + private def renameTargetBlock(block: TargetBlock): TargetBlock = + // currently `identity`, as language is the only metadata + TargetBlock(block.metadata.map { + case language: Metadata.TargetLanguage => language + }) + private def renameProjectBlock(block: ProjectBlock): ProjectBlock = // currently `identity`, as no metadata contains a service name ProjectBlock( @@ -23,6 +29,9 @@ case class ServiceRenamer(renamingMap: Map[String, String]) { }, ) + private def renameTargetBlocks(targets: Map[String, TargetBlock]): Map[String, TargetBlock] = + targets.view.mapValues(renameTargetBlock).toMap + private def renameServiceMetadata(metadata: Metadata.ServiceMetadata): Metadata.ServiceMetadata = metadata match { // rename any services referenced in #uses case Metadata.Uses(services) => Metadata.Uses(services.map(rename)) @@ -81,6 +90,7 @@ case class ServiceRenamer(renamingMap: Map[String, String]) { Templefile( templefile.projectName, renameProjectBlock(templefile.projectBlock), + renameTargetBlocks(templefile.targets), renameServiceBlocks(templefile.services), ) } diff --git a/src/main/scala/temple/DSL/semantics/Validator.scala b/src/main/scala/temple/DSL/semantics/Validator.scala index 6a8757366..58fb61eda 100644 --- a/src/main/scala/temple/DSL/semantics/Validator.scala +++ b/src/main/scala/temple/DSL/semantics/Validator.scala @@ -143,6 +143,7 @@ private class Validator private (templefile: Templefile) { errors += context.errorMessage(s"Multiple occurrences of ${classTag[T].runtimeClass.getSimpleName} metadata") metadata foreach { + case _: Metadata.TargetLanguage => assertUnique[Metadata.TargetLanguage]() case _: Metadata.ServiceLanguage => assertUnique[Metadata.ServiceLanguage]() case _: Metadata.Database => assertUnique[Metadata.Database]() case _: Metadata.AuthMethod => @@ -220,8 +221,8 @@ private class Validator private (templefile: Templefile) { case FloatType(_, _, _) => // all good } - private def validateBlockOfMetadata[T <: Metadata](block: TempleBlock[T], context: SemanticContext): Unit = - validateMetadata(block.metadata, context) + private def validateBlockOfMetadata[T <: Metadata](target: TempleBlock[T], context: SemanticContext): Unit = + validateMetadata(target.metadata, context) private val referenceCycles: Set[Set[String]] = { val graph = templefile.providedBlockNames.map { blockName => @@ -238,13 +239,17 @@ private class Validator private (templefile: Templefile) { newServices = templefile.services.transform { case (name, service) => validateService(name, service, context :+ name) } + templefile.targets.foreach { + case (name, block) => validateBlockOfMetadata(block, context :+ name) + } validateBlockOfMetadata(templefile.projectBlock, context :+ s"${templefile.projectName} project") val rootNames: Seq[(String, String)] = Seq(templefile.projectName -> "project") ++ templefile.services.keys.map(_ -> "service") ++ - templefile.structNames.map(_ -> "struct") + templefile.structNames.map(_ -> "struct") ++ + templefile.targets.keys.map(_ -> "target") val duplicates = rootNames .groupBy(_._1) .collect { case (name, repeats) if repeats.sizeIs > 1 => (name, repeats.map(_._2)) } @@ -256,7 +261,7 @@ private class Validator private (templefile: Templefile) { val suffix = if (duplicates.sizeIs == 1) s"duplicate found: $duplicateString" else s"duplicates found: $duplicateString" - errors += context.errorMessage(s"Project, services and structs must be globally unique, $suffix") + errors += context.errorMessage(s"Project, targets, services and structs must be globally unique, $suffix") } rootNames.collect { case (name, location) if !name.head.isUpper => diff --git a/src/main/scala/temple/DSL/syntax/DSLRootItem.scala b/src/main/scala/temple/DSL/syntax/DSLRootItem.scala index bbcd46f6a..88cd851c5 100644 --- a/src/main/scala/temple/DSL/syntax/DSLRootItem.scala +++ b/src/main/scala/temple/DSL/syntax/DSLRootItem.scala @@ -2,7 +2,7 @@ package temple.DSL.syntax import temple.utils.StringUtils.indent -/** An item at the root of the Templefile, e.g. services */ +/** An item at the root of the Templefile, e.g. services and targets */ case class DSLRootItem(key: String, tag: String, entries: Seq[Entry]) extends Entry(s"$tag block ($key)") { override def toString: String = { diff --git a/src/main/scala/temple/ast/Metadata.scala b/src/main/scala/temple/ast/Metadata.scala index bb8754b9f..08672ea6b 100644 --- a/src/main/scala/temple/ast/Metadata.scala +++ b/src/main/scala/temple/ast/Metadata.scala @@ -3,14 +3,26 @@ package temple.ast import temple.collection.enumeration._ import temple.errors.ErrorHandlingContext -/** A piece of metadata modifying a service/project block */ +/** A piece of metadata modifying a service/project/target block */ sealed trait Metadata object Metadata { + sealed trait TargetMetadata extends Metadata sealed trait ProjectMetadata extends Metadata sealed trait ServiceMetadata extends Metadata sealed trait StructMetadata extends ServiceMetadata + sealed abstract class TargetLanguage private (name: String, aliases: String*) + extends EnumEntry(name, aliases) + with TargetMetadata + + object TargetLanguage extends Enum[TargetLanguage] { + val values: IndexedSeq[TargetLanguage] = findValues + + case object Swift extends TargetLanguage("Swift") + case object JavaScript extends TargetLanguage("JavaScript", "js") + } + sealed abstract class ServiceLanguage private (name: String, aliases: String*) extends EnumEntry(name, aliases) with ServiceMetadata diff --git a/src/main/scala/temple/ast/TargetBlock.scala b/src/main/scala/temple/ast/TargetBlock.scala new file mode 100644 index 000000000..af93c6621 --- /dev/null +++ b/src/main/scala/temple/ast/TargetBlock.scala @@ -0,0 +1,8 @@ +package temple.ast + +import temple.ast.Metadata.TargetMetadata + +/** A block describing one client to generate code for */ +case class TargetBlock( + metadata: Seq[TargetMetadata] = Nil, +) extends TempleBlock[TargetMetadata] diff --git a/src/main/scala/temple/ast/Templefile.scala b/src/main/scala/temple/ast/Templefile.scala index e19771ef9..3be490a18 100644 --- a/src/main/scala/temple/ast/Templefile.scala +++ b/src/main/scala/temple/ast/Templefile.scala @@ -12,10 +12,11 @@ import scala.reflect.ClassTag case class Templefile( projectName: String, projectBlock: ProjectBlock = ProjectBlock(), + targets: Map[String, TargetBlock] = Map(), services: Map[String, ServiceBlock] = Map(), ) extends TempleNode { // Inform every child node of their parent, so that they can access the project information - for (block <- Iterator(projectBlock) ++ services.valuesIterator) { + for (block <- Iterator(projectBlock) ++ targets.valuesIterator ++ services.valuesIterator) { block.setParent(this) } diff --git a/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala b/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala index 55ad9adfc..e44ccc03d 100644 --- a/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala +++ b/src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala @@ -397,6 +397,28 @@ class SemanticAnalyzerTest extends FlatSpec with Matchers { parseSemantics(Seq(DSLRootItem("Test", "project", Seq()), DSLRootItem("other", "badItem", Seq()))) } should have message "Unknown block type in other badItem" } + + it should "parse target blocks correctly" in { + noException should be thrownBy { + parseSemantics( + mkTemplefileAST(DSLRootItem("mobile", "target", Seq(Entry.Metadata("language", Args(Seq(TokenArg("swift"))))))), + ) + } + + the[SemanticParsingException] thrownBy { + parseSemantics( + mkTemplefileAST(DSLRootItem("mobile", "target", Seq(Entry.Metadata("badKey", Args(Seq(TokenArg("swift"))))))), + ) + } should have message "No valid metadata badKey in mobile target" + + the[SemanticParsingException] thrownBy { + parseSemantics( + mkTemplefileAST( + DSLRootItem("mobile", "target", Seq(Entry.Attribute("field", syntax.AttributeType.Primitive("int")))), + ), + ) + } should have message "Found unexpected attribute: `field: int;` in mobile target" + } } object SemanticAnalyzerTest { diff --git a/src/test/scala/temple/DSL/semantics/TempleBlockTest.scala b/src/test/scala/temple/DSL/semantics/TempleBlockTest.scala index f2b775966..5d5a1e322 100644 --- a/src/test/scala/temple/DSL/semantics/TempleBlockTest.scala +++ b/src/test/scala/temple/DSL/semantics/TempleBlockTest.scala @@ -12,6 +12,10 @@ class TempleBlockTest extends FlatSpec with Matchers { val projectBlock = ProjectBlock(Seq(Metadata.Database.Postgres)) projectBlock.lookupLocalMetadata[Metadata.ServiceLanguage] shouldBe None projectBlock.lookupLocalMetadata[Metadata.Database] shouldBe Some(Metadata.Database.Postgres) + + val targetBlock = TargetBlock(Seq(Metadata.TargetLanguage.JavaScript)) + targetBlock.lookupLocalMetadata[Metadata.TargetLanguage] shouldBe Some(Metadata.TargetLanguage.JavaScript) + targetBlock.lookupLocalMetadata[Metadata.Database] shouldBe None } it should "fail to lookupMetadata without being in a project file" in { diff --git a/src/test/scala/temple/DSL/semantics/ValidatorTest.scala b/src/test/scala/temple/DSL/semantics/ValidatorTest.scala index 6f60b744b..ce6e2b9fa 100644 --- a/src/test/scala/temple/DSL/semantics/ValidatorTest.scala +++ b/src/test/scala/temple/DSL/semantics/ValidatorTest.scala @@ -63,13 +63,14 @@ class ValidatorTest extends FlatSpec with Matchers { ), ), ) shouldBe Set( - "Project, services and structs must be globally unique, duplicate found: User (service, struct)", + "Project, targets, services and structs must be globally unique, duplicate found: User (service, struct)", ) validationErrors( Templefile( "Box", projectBlock = ProjectBlock(Seq(Database.Postgres)), + targets = Map("User" -> TargetBlock(Seq(TargetLanguage.JavaScript))), services = Map( "User" -> ServiceBlock( attributes = Map( @@ -89,7 +90,7 @@ class ValidatorTest extends FlatSpec with Matchers { ), ), ) shouldBe Set( - "Project, services and structs must be globally unique, duplicates found: Box (project, struct), User (service, struct)", + "Project, targets, services and structs must be globally unique, duplicates found: Box (project, struct), User (service, struct, target)", ) validationErrors( @@ -105,7 +106,7 @@ class ValidatorTest extends FlatSpec with Matchers { ), ), ) shouldBe Set( - "Project, services and structs must be globally unique, duplicate found: User (project, service)", + "Project, targets, services and structs must be globally unique, duplicate found: User (project, service)", ) } @@ -466,7 +467,7 @@ class ValidatorTest extends FlatSpec with Matchers { ) } should have message { """An error was encountered while validating the Templefile - |Project, services and structs must be globally unique, duplicate found: User (service, struct)""".stripMargin + |Project, targets, services and structs must be globally unique, duplicate found: User (service, struct)""".stripMargin } the[SemanticParsingException] thrownBy { diff --git a/src/test/scala/temple/builder/BuilderTestData.scala b/src/test/scala/temple/builder/BuilderTestData.scala index daaef911a..7c6aa38dc 100644 --- a/src/test/scala/temple/builder/BuilderTestData.scala +++ b/src/test/scala/temple/builder/BuilderTestData.scala @@ -44,6 +44,7 @@ object BuilderTestData { val simpleTemplefile: Templefile = Templefile( "TestProject", ProjectBlock(Seq(ServiceLanguage.Go)), + targets = Map(), services = Map("TestService" -> sampleService), ) @@ -79,6 +80,7 @@ object BuilderTestData { val complexTemplefile: Templefile = Templefile( "TestComplexProject", ProjectBlock(Seq(ServiceLanguage.Go)), + targets = Map(), services = Map("TestComplexService" -> sampleComplexService), ) }