1. sbt-datatype

sbt-datatype 

sbt-datatype 是一个代码生成库和一个 sbt 自动插件,它生成可增长的数据类型并帮助开发人员避免二进制兼容性问题。

与标准的 Scala case 类不同,此库生成的数据类型(或伪 case 类)允许开发人员向定义的数据类型添加新字段,而不会破坏二进制兼容性,同时提供(几乎)与普通 case 类相同的功能。唯一的区别是 datatype 不会生成 unapplycopy 方法,因为它们会破坏二进制兼容性。

此外,sbt-datatype 还能够为 sjson-new 生成 JSON 编解码器,它可以与各种 JSON 后端一起使用。

我们的插件将以 JSON 对象形式的数据类型模式作为输入,其格式基于 Apache Avro 定义的格式,并生成相应的 Java 或 Scala 代码,以及允许生成的类保持与数据类型先前版本的二进制兼容性的样板代码。

库和自动插件的源代码 可以在 GitHub 上找到

使用插件 

要为您的构建启用插件,请将以下行放在 project/datatype.sbt

addSbtPlugin("org.scala-sbt" % "sbt-datatype" % "0.2.2")

您的数据类型定义默认应放在 src/main/datatypesrc/test/datatype 中。以下是您应该如何配置构建

lazy val library = (project in file("library"))
  .enablePlugins(DatatypePlugin)
  .settings(
    name := "foo library",
  )

数据类型模式 

Datatype 能够生成三种类型的类型

  1. 记录
  2. 接口
  3. 枚举

记录 

记录映射到 Java 或 Scala class,对应于 Scala 中的标准 case 类。

{
  "types": [
    {
      "name": "Person",
      "type": "record",
      "target": "Scala",
      "fields": [
        {
          "name": "name",
          "type": "String"
        },
        {
          "name": "age",
          "type": "int"
        }
      ]
    }
  ]
}

此模式将生成以下 Scala 类

final class Person(
  val name: String,
  val age: Int) extends Serializable {
  override def equals(o: Any): Boolean = o match {
    case x: Person => (this.name == x.name) && (this.age == x.age)
    case _ => false
  }
  override def hashCode: Int = {
    37 * (37 * (17 + name.##) + age.##)
  }
  override def toString: String = {
    "Person(" + name + ", " + age + ")"
  }
  private[this] def copy(name: String = name, age: Int = age): Person = {
    new Person(name, age)
  }
  def withName(name: String): Person = {
    copy(name = name)
  }
  def withAge(age: Int): Person = {
    copy(age = age)
  }
}
object Person {
  def apply(name: String, age: Int): Person = new Person(name, age)
}

或以下 Java 代码(在将 target 属性更改为 "Java" 后)

public final class Person implements java.io.Serializable {
    private String name;
    private int age;
    public Person(String _name, int _age) {
        super();
        name = _name;
        age = _age;
    }
    public String name() {
        return this.name;
    }
    public int age() {
        return this.age;
    }
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Person)) {
            return false;
        } else {
            Person o = (Person)obj;
            return name().equals(o.name()) && (age() == o.age());
        }
    }
    public int hashCode() {
        return 37 * (37 * (17 + name().hashCode()) + (new Integer(age())).hashCode());
    }
    public String toString() {
        return "Person("  + "name: " + name() + ", " + "age: " + age() + ")";
    }
}

接口 

接口映射到 Java abstract class 或 Scala abstract classes。它们可以被其他接口或记录扩展。

{
  "types": [
    {
      "name": "Greeting",
      "namespace": "com.example",
      "target": "Scala",
      "type": "interface",
      "fields": [
        {
          "name": "message",
          "type": "String"
        }
      ],
      "types": [
        {
          "name": "SimpleGreeting",
          "namespace": "com.example",
          "target": "Scala",
          "type": "record"
        }
      ]
    }
  ]
}

这将生成名为 Greeting 的抽象类和一个名为 SimpleGreeting 的类,该类扩展了 Greeting

此外,接口可以定义 messages,这将生成抽象方法声明。

{
  "types": [
    {
      "name": "FooService",
      "target": "Scala",
      "type": "interface",
      "messages": [
        {
          "name": "doSomething",
          "response": "int*",
          "request": [
            {
              "name": "arg0",
              "type": "int*",
              "doc": [
                "The first argument of the message.",
              ]
            }
          ]
        }
      ]
    }
  ]
}

枚举 

枚举映射到 Java 枚举或 Scala case 对象。

{
  "types": [
    {
      "name": "Weekdays",
      "type": "enum",
      "target": "Java",
      "symbols": [
        "Monday", "Tuesday", "Wednesday", "Thursday",
        "Friday", "Saturday", "Sunday"
      ]
    }
  ]
}

此模式将生成以下 Java 代码

public enum Weekdays {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

或以下 Scala 代码(在将 target 属性更改为后)

sealed abstract class Weekdays extends Serializable
object Weekdays {
  case object Monday extends Weekdays
  case object Tuesday extends Weekdays
  case object Wednesday extends Weekdays
  case object Thursday extends Weekdays
  case object Friday extends Weekdays
  case object Saturday extends Weekdays
  case object Sunday extends Weekdays
}

使用 datatype 保持二进制兼容性 

通过使用 sincedefault 参数,可以扩展现有数据类型,同时保持与针对您数据类型定义的早期版本编译的类的二进制兼容性。

考虑以下数据类型的初始版本

{
  "types": [
    {
      "name": "Greeting",
      "type": "record",
      "target": "Scala",
      "fields": [
        {
          "name": "message",
          "type": "String"
        }
      ]
    }
  ]
}

生成的代码可以在使用以下代码的 Scala 程序中使用

val greeting = Greeting("hello")

现在假设您想扩展数据类型,在 Greeting 中包含日期。数据类型可以相应地修改

{
  "types": [
    {
      "name": "Greeting",
      "type": "record",
      "target": "Scala",
      "fields": [
        {
          "name": "message",
          "type": "String"
        },
        {
          "name": "date",
          "type": "java.util.Date"
        }
      ]
    }
  ]
}

不幸的是,使用 Greeting 的代码将不再编译,并且针对数据类型先前版本编译的类将因 NoSuchMethodError 而崩溃。

为了规避这个问题并允许您扩展数据类型,可以指定字段存在以来的版本 since 和数据类型定义中的 default

{
  "types": [
    {
      "name": "Greeting",
      "type": "record",
      "target": "Scala",
      "fields": [
        {
          "name": "message",
          "type": "String"
        },
        {
          "name": "date",
          "type": "java.util.Date",
          "since": "0.2.0",
          "default": "new java.util.Date()"
        }
      ]
    }
  ]
}

现在,针对数据类型先前定义编译的代码仍将运行。

JSON 编解码器生成 

JsonCodecPlugin 添加到子项目将为数据类型生成 sjson-new JSON 代码。

lazy val root = (project in file("."))
  .enablePlugins(DatatypePlugin, JsonCodecPlugin)
  .settings(
    scalaVersion := "2.11.8",
    libraryDependencies += "com.eed3si9n" %% "sjson-new-scalajson" % "0.4.1"
  )

codecNamespace 可用于指定编解码器的包名。

{
  "codecNamespace": "com.example.codec",
  "fullCodec": "CustomJsonProtocol",
  "types": [
    {
      "name": "Person",
      "namespace": "com.example",
      "type": "record",
      "target": "Scala",
      "fields": [
        {
          "name": "name",
          "type": "String"
        },
        {
          "name": "age",
          "type": "int"
        }
      ]
    }
  ]
}

JsonFormat 特征将在 com.example.codec 包下生成,以及一个包含所有特征的完整编解码器,名为 CustomJsonProtocol

scala> import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter, Parser }
import sjsonnew.support.scalajson.unsafe.{Converter, CompactPrinter, Parser}

scala> import com.example.codec.CustomJsonProtocol._
import com.example.codec.CustomJsonProtocol._

scala> import com.example.Person
import com.example.Person

scala> val p = Person("Bob", 20)
p: com.example.Person = Person(Bob, 20)

scala> val j = Converter.toJsonUnsafe(p)
j: scala.json.ast.unsafe.JValue = JObject([Lscala.json.ast.unsafe.JField;@6731ad72)

scala> val s = CompactPrinter(j)
s: String = {"name":"Bob","age":20}

scala> val x = Parser.parseUnsafe(s)
x: scala.json.ast.unsafe.JValue = JObject([Lscala.json.ast.unsafe.JField;@7331f7f8)

scala> val q = Converter.fromJsonUnsafe[Person](x)
q: com.example.Person = Person(Bob, 20)

scala> assert(p == q)

协议、记录等的现有参数 

模式定义的所有元素都接受许多参数,这些参数将影响生成的代码。这些参数并非对模式的每个节点都可用。请参阅语法摘要以查看节点是否可以定义参数。

名称

此参数定义字段、记录、字段等的名称。

目标

此参数决定代码是在 Java 中生成还是在 Scala 中生成。

命名空间

此参数仅存在于 Definition 中。它确定将生成代码的包。

文档

将与生成的元素一起提供的 Javadoc。

字段

仅针对 protocolrecord,它描述构成生成实体的所有字段。

类型

对于 protocol,它定义扩展它的子 protocolrecord

对于 enumeration,它定义枚举的值。

此参数仅存在于 field 中。它指示在哪个版本中将字段添加到其父 protocolrecord 中。

定义此参数时,还必须定义 default

默认

此参数仅存在于 field 中。它指示如果该字段被针对此数据类型的早期版本编译的类使用,则此字段的默认值应是什么。

它必须包含在父 protocolrecordtarget 语言中有效的表达式。

fieldtype

它指示字段的基础类型是什么。

始终使用您希望在 Scala 中看到的类型。例如,如果您的字段将包含整数值,请使用 Int 而不是 Java 的 intdatatype 将自动使用 Java 的原始类型(如果可用)。

对于非原始类型,建议编写完全限定的类型。

其他定义的 type

它只是指示您想要生成的实体类型:protocolrecordenumeration

设置 

可以在您的构建定义中设置新位置,从而更改此位置

datatypeSource in generateDatatypes := file("some/location")

插件公开了其他用于 Scala 代码生成的设置

  1. Compile / generateDatatypes / datatypeScalaFileNames 此设置接受函数 Definition => File,它将确定每个生成的 Scala 定义的文件名。
  2. Compile / generateDatatypes / datatypeScalaSealInterfaces 此设置接受布尔值,并将确定接口是 seal 还是不 seal

语法摘要 

Schema           := {   "types": [ Definition* ]
                     (, "codecNamespace": string constant)?
                     (, "fullCodec": string constant)? }

Definition       := Record | Interface | Enumeration

Record           := {   "name": ID,
                        "type": "record",
                        "target": ("Scala" | "Java")
                     (, "namespace": string constant)?
                     (, "doc": string constant)?
                     (, "fields": [ Field* ])? }

Interface        := {   "name": ID,
                        "type": "interface",
                        "target": ("Scala" | "Java")
                     (, "namespace": string constant)?
                     (, "doc": string constant)?
                     (, "fields": [ Field* ])?
                     (, "messages": [ Message* ])?
                     (, "types": [ Definition* ])? }

Enumeration      := {   "name": ID,
                        "type": "enum",
                        "target": ("Scala" | "Java")
                     (, "namespace": string constant)?
                     (, "doc": string constant)?
                     (, "symbols": [ Symbol* ])? }

Symbol           := ID
                  | {   "name": ID
                     (, "doc": string constant)? }

Field            := {   "name": ID,
                        "type": ID
                     (, "doc": string constant)?
                     (, "since": version number string)?
                     (, "default": string constant)? }

Message          := {   "name": ID,
                        "response": ID
                     (, "request": [ Request* ])?
                     (, "doc": string constant)? }

Request          := {   "name": ID,
                        "type": ID
                     (, "doc": string constant)? }