1. sbt-datatype

sbt-datatype 

sbt-datatype 是一個程式碼產生函式庫和 sbt 自動外掛程式,可產生可成長的資料類型,並協助開發人員避免二進位相容性中斷。

與標準 Scala case 類別不同,此函式庫產生的資料類型 (或偽 case 類別) 允許開發人員在定義的資料類型中新增欄位,而不會中斷二進位相容性,同時提供 (幾乎) 與一般 case 類別相同的功能。唯一的差異在於資料類型不會產生 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",
  )

資料類型綱要 

資料類型能夠產生三種型別

  1. 記錄
  2. 介面
  3. 列舉

記錄 

記錄會對應至 Java 或 Scala classes,相當於 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 classes 或 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
}

使用資料類型保留二進位相容性 

透過使用 sincedefault 參數,可以成長現有的資料類型,同時與已針對較舊版本資料類型定義編譯的類別保持二進位相容性。

請考慮以下資料類型的初始版本

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

產生的程式碼可以在 Scala 程式中使用以下程式碼

val greeting = Greeting("hello")

現在假設您想要延伸資料類型,將日期包含至 Greetings。可以據此修改資料類型

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

不幸的是,使用 Greeting 的程式碼將不再編譯,並且已針對先前版本的資料類型編譯的類別將會因為 NoSuchMethodError 而崩潰。

若要規避此問題並允許您成長資料類型,可以在資料類型定義中指出欄位存在的版本 sincedefault

{
  "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。它決定程式碼將在其中產生的套件。

doc

將會伴隨產生元素的 Javadoc。

fields

僅適用於 protocolrecord,它會描述構成產生實體的所有欄位。

types

對於 protocol,它會定義延伸自它的子 protocols 和 records。

對於 enumeration,它會定義列舉的值。

since

此參數僅適用於 field。它會指出欄位已新增至其父 protocolrecord 的版本。

定義此參數時,也必須定義 default

default

此參數僅適用於 field。它會指出此欄位的預設值應為何,以防具有針對此資料類型較舊版本編譯的類別使用此欄位。

它必須包含在父 protocolrecordtarget 語言中有效的運算式。

fieldtype

它會指出欄位的基礎類型為何。

請一律使用您想要在 Scala 中看到的類型。例如,如果您的欄位將包含整數值,請使用 Int 而不是 Java 的 int。如果可以使用 Java 的基本類型,則 datatype 會自動使用它們。

對於非基本類型,建議撰寫完整限定的類型。

其他定義的 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)? }