Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class DockerGeneratorIntegrationTest extends HadolintSpec with Matchers with Bef
val templefile = Templefile(
"ExampleProject",
ProjectBlock(),
targets = Map(),
services = Map("ComplexService" -> DockerGeneratorIntegrationTestData.sampleService),
)

Expand Down
19 changes: 16 additions & 3 deletions src/main/scala/temple/DSL/semantics/Analyzer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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")
}
Expand All @@ -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
*/
Expand Down
10 changes: 10 additions & 0 deletions src/main/scala/temple/DSL/semantics/ServiceRenamer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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))
Expand Down Expand Up @@ -81,6 +90,7 @@ case class ServiceRenamer(renamingMap: Map[String, String]) {
Templefile(
templefile.projectName,
renameProjectBlock(templefile.projectBlock),
renameTargetBlocks(templefile.targets),
renameServiceBlocks(templefile.services),
)
}
13 changes: 9 additions & 4 deletions src/main/scala/temple/DSL/semantics/Validator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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 =>
Expand All @@ -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)) }
Expand All @@ -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 =>
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/temple/DSL/syntax/DSLRootItem.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
14 changes: 13 additions & 1 deletion src/main/scala/temple/ast/Metadata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/main/scala/temple/ast/TargetBlock.scala
Original file line number Diff line number Diff line change
@@ -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]
3 changes: 2 additions & 1 deletion src/main/scala/temple/ast/Templefile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
22 changes: 22 additions & 0 deletions src/test/scala/temple/DSL/semantics/SemanticAnalyzerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions src/test/scala/temple/DSL/semantics/TempleBlockTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions src/test/scala/temple/DSL/semantics/ValidatorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)",
)
}

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/test/scala/temple/builder/BuilderTestData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object BuilderTestData {
val simpleTemplefile: Templefile = Templefile(
"TestProject",
ProjectBlock(Seq(ServiceLanguage.Go)),
targets = Map(),
services = Map("TestService" -> sampleService),
)

Expand Down Expand Up @@ -79,6 +80,7 @@ object BuilderTestData {
val complexTemplefile: Templefile = Templefile(
"TestComplexProject",
ProjectBlock(Seq(ServiceLanguage.Go)),
targets = Map(),
services = Map("TestComplexService" -> sampleComplexService),
)
}