Skip to content

Commit

Permalink
Merge pull request #38 from dwijnand/copies
Browse files Browse the repository at this point in the history
Add support for public, overloaded, bincompat copy
  • Loading branch information
eed3si9n authored Oct 26, 2016
2 parents 3955b66 + 3311302 commit 496af5a
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 85 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ lazy val library = project.
pluginSettings,
name := "datatype",
description := "Code generation library to generate growable datatypes.",
libraryDependencies ++= jsonDependencies ++ Seq(scalaTest % Test)
libraryDependencies ++= jsonDependencies ++ Seq(scalaTest % Test),
libraryDependencies += "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" % Test
)
4 changes: 1 addition & 3 deletions library/src/main/scala/CodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ object $name {
}"""
}

def bq(id: String): String =
if (ScalaKeywords.values(id)) s"`$id`"
else id
def bq(id: String): String = if (ScalaKeywords.values(id)) s"`$id`" else id
}

134 changes: 64 additions & 70 deletions library/src/main/scala/ScalaCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol


override def generate(s: Schema): ListMap[File, String] =
s.definitions.toList map (generate (s, _, None, Nil)) reduce (_ merge _) mapV (_.indented)
s.definitions map (generate (s, _, None, Nil)) reduce (_ merge _) mapV (_.indented)

override def generateEnum(s: Schema, e: Enumeration): ListMap[File, String] = {
val values =
Expand All @@ -46,82 +46,50 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol
override def generateRecord(s: Schema, r: Record, parent: Option[Interface], superFields: List[Field]): ListMap[File, String] = {
val allFields = superFields ++ r.fields

val alternativeCtors =
genAlternativeConstructors(r.since, allFields) mkString EOL

// If there are no fields, we still need an `apply` method with an empty parameter list.
val applyOverloads =
if (allFields.isEmpty) {
s"def apply(): ${r.name} = new ${r.name}()"
} else {
perVersionNumber(r.since, allFields) { (provided, byDefault) =>
val applyParameters =
provided map genParam mkString ", "

val ctorCallArguments =
allFields map {
case f if provided contains f => bq(f.name)
case f if byDefault contains f => f.default getOrElse sys.error(s"Need a default value for field ${f.name}.")
} mkString ", "

s"def apply($applyParameters): ${r.name} = new ${r.name}($ctorCallArguments)"
} mkString EOL
}

val ctorParameters =
genCtorParameters(r, allFields) mkString ","

val ctorParameters = genCtorParameters(r, allFields) mkString ","
val superCtorArguments = superFields map (_.name) mkString ", "

val extendsCode =
parent map (p => s"extends ${fullyQualifiedName(p)}($superCtorArguments)") getOrElse "extends Serializable"

val lazyMembers =
genLazyMembers(r.fields) mkString EOL
val extendsCode = (parent
map (p => s"extends ${fullyQualifiedName(p)}($superCtorArguments)")
getOrElse "extends Serializable"
)

val code =
s"""${genPackage(r)}
|${genDoc(r.doc)}
|final class ${r.name}($ctorParameters) $extendsCode {
| ${r.extra mkString EOL}
| $alternativeCtors
| $lazyMembers
| ${genAlternativeConstructors(r.since, allFields) mkString EOL}
| ${genLazyMembers(r.fields) mkString EOL}
| ${genEquals(r, superFields)}
| ${genHashCode(r, superFields)}
| ${genToString(r, superFields)}
| ${genCopy(r, superFields)}
| ${genCopyOverloads(r, allFields) mkString EOL}
| ${genWith(r, superFields)}
|}
|
|object ${r.name} {
| $applyOverloads
| ${genApplyOverloads(r, allFields) mkString EOL}
|}""".stripMargin

ListMap(genFile(r) -> code)
}

override def generateInterface(s: Schema, i: Interface, parent: Option[Interface], superFields: List[Field]): ListMap[File, String] = {
val allFields = superFields ++ i.fields
val alternativeCtors =
genAlternativeConstructors(i.since, allFields) mkString EOL

val ctorParameters =
genCtorParameters(i, allFields) mkString ", "

val superCtorArguments =
superFields map (_.name) mkString ", "

val extendsCode =
parent map (p => s"extends ${fullyQualifiedName(p)}($superCtorArguments)") getOrElse "extends Serializable"

val lazyMembers =
genLazyMembers(i.fields) mkString EOL
val classDef = if (sealProtocols) "sealed abstract class" else "abstract class"
val ctorParameters = genCtorParameters(i, allFields) mkString ", "
val superCtorArguments = superFields map (_.name) mkString ", "

val messages =
genMessages(i.messages) mkString EOL
val extendsCode = (parent
map (p => s"extends ${fullyQualifiedName(p)}($superCtorArguments)")
getOrElse "extends Serializable"
)

val classDef =
if (sealProtocols) "sealed abstract class" else "abstract class"
val alternativeCtors = genAlternativeConstructors(i.since, allFields) mkString EOL
val lazyMembers = genLazyMembers(i.fields) mkString EOL
val messages = genMessages(i.messages) mkString EOL

val code =
s"""${genPackage(i)}
Expand All @@ -136,11 +104,12 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol
| ${genToString(i, superFields)}
|}""".stripMargin

ListMap(genFile(i) -> code) :: (i.children map (generate(s, _, Some(i), superFields ++ i.fields))) reduce (_ merge _)
val childrenCode = i.children map (generate(s, _, Some(i), superFields ++ i.fields))
ListMap(genFile(i) -> code) :: childrenCode reduce (_ merge _)
}

private def genDoc(doc: List[String]) = doc match {
case Nil => ""
case Nil => ""
case l :: Nil => s"/** $l */"
case lines =>
val doc = lines map (l => s" * $l") mkString EOL
Expand Down Expand Up @@ -224,11 +193,27 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol
}
}

private def genApplyOverloads(r: Record, allFields: List[Field]): List[String] =
if (allFields.isEmpty) { // If there are no fields, we still need an `apply` method with an empty parameter list
List(s"def apply(): ${r.name} = new ${r.name}()")
} else {
perVersionNumber(r.since, allFields) { (provided, byDefault) =>
val applyParameters = provided map genParam mkString ", "

val ctorCallArguments =
allFields map {
case f if provided contains f => bq(f.name)
case f if byDefault contains f => f.default getOrElse sys.error(s"Need a default value for field ${f.name}.")
} mkString ", "

s"def apply($applyParameters): ${r.name} = new ${r.name}($ctorCallArguments)"
}
}

private def genAlternativeConstructors(since: VersionNumber, allFields: List[Field]) =
perVersionNumber(since, allFields) {
case (provided, byDefault) if byDefault.nonEmpty => // Don't duplicate up-to-date constructor
val ctorParameters =
provided map genParam mkString ", "
val ctorParameters = provided map genParam mkString ", "
val thisCallArguments =
allFields map {
case f if provided contains f => bq(f.name)
Expand All @@ -246,15 +231,13 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol
// Non-lazy fields that belong to `cl` are made val parameters.
private def genCtorParameters(cl: ClassLike, allFields: List[Field]): List[String] =
allFields map {
case f if cl.fields.contains(f) && f.tpe.lzy =>
EOL + "_" + genParam(f)
case f if cl.fields.contains(f) && f.tpe.lzy => EOL + "_" + genParam(f)

case f if cl.fields.contains(f) =>
s"""$EOL${genDoc(f.doc)}
|val ${genParam(f)}""".stripMargin

case f =>
EOL + genParam(f)
case f => EOL + genParam(f)
}

private def genLazyMembers(fields: List[Field]): List[String] =
Expand Down Expand Up @@ -283,17 +266,28 @@ class ScalaCodeGen(scalaArray: String, genFile: Definition => File, sealProtocol
path + d.name
}

private def genPackage(d: Definition): String =
d.namespace map (ns => s"package $ns") getOrElse ""
private def genPackage(d: Definition): String = d.namespace map (ns => s"package $ns") getOrElse ""

private def genCopy(r: Record, superFields: List[Field]) = {
val allFields = superFields ++ r.fields
val params = allFields map (f => s"${bq(f.name)}: ${genRealTpe(f.tpe, isParam = true)} = ${bq(f.name)}") mkString ", "
val constructorCall = allFields map (f => bq(f.name)) mkString ", "
s"""private[this] def copy($params): ${r.name} = {
| new ${r.name}($constructorCall)
|}""".stripMargin
}
private def genCopyOverloads(r: Record, allFields: List[Field]) =
if (allFields.isEmpty) { // If there are no fields, we still need an `copy` method with an empty parameter list
List(s"def copy(): ${r.name} = new ${r.name}()")
} else {
perVersionNumber(r.since, allFields) { (provided, byDefault) =>
def genParam(f: Field) = {
val name = bq(f.name)
val tpe = genRealTpe(f.tpe, isParam = true)
if (byDefault.isEmpty) // when all fields are provided, then given them their default value
s"$name: $tpe = $name"
else
s"$name: $tpe"
}
val params = provided map genParam mkString ", "
val constructorCall = allFields map (f => bq(f.name)) mkString ", "
s"""def copy($params): ${r.name} = {
| new ${r.name}($constructorCall)
|}""".stripMargin
}
}

private def genWith(r: Record, superFields: List[Field]) = {
def capitalize(s: String) = { val (fst, rst) = s.splitAt(1) ; fst.toUpperCase + rst }
Expand Down
40 changes: 40 additions & 0 deletions library/src/test/scala/CodecCodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,46 @@ class CodecCodeGenSpec extends GCodeGenSpec("Codec") {
|}""".stripMargin.unindent
}

override def recordGrowZeroToOneToTwoFields: Unit = {
val gen = new CodecCodeGen(codecParents, instantiateJavaLazy, javaOption, scalaArray, formatsForType, Nil)
val record = Record parse growableZeroToOneToTwoFieldsExample
val code = gen generate record

val obtained = code.head._2.unindent
val expected =
"""/**
| * This code is generated using sbt-datatype.
| */
|
|// DO NOT EDIT MANUALLY
|package generated
|import _root_.sjsonnew.{ deserializationError, serializationError, Builder, JsonFormat, Unbuilder }
|trait FooFormats { self: sjsonnew.BasicJsonProtocol =>
| implicit lazy val FooFormat: JsonFormat[_root_.Foo] = new JsonFormat[_root_.Foo] {
| override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): _root_.Foo = {
| jsOpt match {
| case Some(js) =>
| unbuilder.beginObject(js)
| val x = unbuilder.readField[Int]("x")
| val y = unbuilder.readField[Int]("y")
| unbuilder.endObject()
| new _root_.Foo(x, y)
| case None =>
| deserializationError("Expected JsObject but found None")
| }
| }
| override def write[J](obj: _root_.Foo, builder: Builder[J]): Unit = {
| builder.beginObject()
| builder.addField("x", obj.x)
| builder.addField("y", obj.y)
| builder.endObject()
| }
| }
|}""".stripMargin.unindent
TestUtils.printUnifiedDiff(expected, obtained)
obtained should contain theSameElementsAs expected
}

override def schemaGenerateTypeReferences = {
val schema = Schema parse primitiveTypesExample
val gen = new CodecCodeGen(codecParents, instantiateJavaLazy, javaOption, scalaArray, formatsForType, schema :: Nil)
Expand Down
2 changes: 2 additions & 0 deletions library/src/test/scala/GCodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ abstract class GCodeGenSpec(language: String) extends FlatSpec with Matchers {

"generate(Record)" should "generate a simple record" in recordGenerateSimple
it should "grow a record from 0 to 1 field" in recordGrowZeroToOneField
it should "grow a record from 0 to 1 to 2 fields" in recordGrowZeroToOneToTwoFields

"generate(Schema)" should "generate a complete schema" in schemaGenerateComplete
it should "generate and indent a complete schema" in schemaGenerateCompletePlusIndent
Expand All @@ -39,4 +40,5 @@ abstract class GCodeGenSpec(language: String) extends FlatSpec with Matchers {
def schemaGenerateTypeReferences: Unit
def schemaGenerateTypeReferencesNoLazy: Unit
def recordGrowZeroToOneField: Unit
def recordGrowZeroToOneToTwoFields: Unit
}
60 changes: 60 additions & 0 deletions library/src/test/scala/JavaCodeGenSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,66 @@ class JavaCodeGenSpec extends GCodeGenSpec("Java") {
).toList
}

override def recordGrowZeroToOneToTwoFields = {
val record = Record parse growableZeroToOneToTwoFieldsExample
val code = new JavaCodeGen("com.example.MyLazy", "com.example.MyOption") generate record

val obtained = code mapValues (_.unindent)
val expected =
Map(
new File("Foo.java") ->
"""public final class Foo implements java.io.Serializable {
| private int x;
| private int y;
| public Foo() {
| super();
| x = 0;
| y = 0;
| }
| public Foo(int _x) {
| super();
| x = _x;
| y = 0;
| }
| public Foo(int _x, int _y) {
| super();
| x = _x;
| y = _y;
| }
| public int x() {
| return this.x;
| }
| public int y() {
| return this.y;
| }
| public Foo withX(int x) {
| return new Foo(x, y);
| }
| public Foo withY(int y) {
| return new Foo(x, y);
| }
| public boolean equals(Object obj) {
| if (this == obj) {
| return true;
| } else if (!(obj instanceof Foo)) {
| return false;
| } else {
| Foo o = (Foo)obj;
| return (x() == o.x()) && (y() == o.y());
| }
| }
| public int hashCode() {
| return 37 * (37 * (17 + (new Integer(x())).hashCode()) + (new Integer(y())).hashCode());
| }
| public String toString() {
| return "Foo(" + "x: " + x() + ", " + "y: " + y() + ")";
| }
|}""".stripMargin.unindent
).toList
TestUtils.printUnifiedDiff(expected.flatMap(_._2), obtained.toList.flatMap(_._2))
obtained should contain theSameElementsAs expected
}

override def schemaGenerateTypeReferences = {
val schema = Schema parse primitiveTypesExample
val code = new JavaCodeGen("com.example.MyLazy", "com.example.MyOption") generate schema
Expand Down
Loading

0 comments on commit 496af5a

Please sign in to comment.