1. sbt-datatype

sbt-datatype 

sbt-datatypeは、拡張可能なデータ型を生成し、バイナリ互換性の破損を防ぐのに役立つコード生成ライブラリとsbt自動プラグインです。

標準のScalaケースクラスとは異なり、このライブラリによって生成されるデータ型(または擬似ケースクラス)では、バイナリ互換性を損なうことなく、定義済みのデータ型に新しいフィールドを追加できます。(ほぼ)プレーンなケースクラスと同じ機能を提供します。唯一の違いは、データ型が`unapply`または`copy`メソッドを生成しないことです。これらはバイナリ互換性を破るためです。

さらに、sbt-datatypeは、様々なJSONバックエンドに対して動作するsjson-newのJSONコーデックも生成できます。

私たちのプラグインは、Apache Avroで定義された形式に基づく`JSON`オブジェクトの形式のデータ型スキーマを入力として受け取り、対応するJavaまたはScalaコードと、生成されたクラスがデータ型の以前のバージョンとバイナリ互換性を維持できるようにするボイラープレートコードを生成します。

ライブラリと自動プラグインのソースコードは、GitHubで見つけることができます

プラグインの使用 

ビルドでプラグインを有効にするには、次の行を`project/datatype.sbt`に追加します。

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

データ型定義は、デフォルトで`src/main/datatype`と`src/test/datatype`に配置する必要があります。ビルドは次のように構成する必要があります。

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

データ型スキーマ 

データ型は、3種類の型を生成できます。

  1. レコード
  2. インターフェース
  3. 列挙型

レコード 

レコードは、JavaまたはScalaの`class`にマップされ、Scalaの標準的なケースクラスに対応します。

{
  "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 class`にマップされます。それらは他のインターフェースまたはレコードによって拡張できます。

{
  "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`という名前の抽象クラスと、`Greeting`を拡張する`SimpleGreeting`という名前のクラスが生成されます。

さらに、インターフェースは`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のケースオブジェクトにマップされます。

{
  "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
}

バイナリ互換性を維持するためのデータ型の使い方 

`since`パラメータと`default`パラメータを使用することで、データ型定義の以前のバージョンに対してコンパイルされたクラスとのバイナリ互換性を維持しながら、既存のデータ型を拡張することができます。

データ型の最初のバージョンを次に示します。

{
  "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)

プロトコル、レコードなどの既存のパラメータ 

スキーマ定義のすべての要素は、生成されたコードに影響を与えるいくつかのパラメータを受け入れます。これらのパラメータは、スキーマのすべてのノードで使用できるわけではありません。パラメータをノードに定義できるかどうかを確認するには、構文概要を参照してください。

name

このパラメータは、フィールド、レコード、フィールドなどの名前を定義します。

target

このパラメータは、コードがJavaまたはScalaで生成されるかどうかを決定します。

namespace

このパラメータは、`Definition`のみに存在します。コードが生成されるパッケージを決定します。

doc

生成された要素に付随するJavadocです。

fields

`protocol`または`record`の場合のみ、生成されたエンティティを構成するすべてのフィールドを記述します。

types

`protocol`の場合、それを拡張する子`protocol`と`record`を定義します。

`enumeration`の場合、列挙型の値を定義します。

since

このパラメータは、`field`のみに存在します。フィールドが親`protocol`または`record`に追加されたバージョンを示します。

このパラメータが定義されている場合、`default`も定義する必要があります。

default

このパラメータは、`field`のみに存在します。このフィールドのデフォルト値を指定します。データ型の以前のバージョンに対してコンパイルされたクラスで使用される場合です。

親`protocol`または`record`の`target`言語で有効な式を含んでいる必要があります。

`field`の`type`

フィールドの基になる型を示します。

Scalaで使用する型は、常に表示したい型を使用してください。例えば、フィールドに整数値が含まれる場合は、JavaのintではなくIntを使用してください。datatypeは、利用可能な場合は自動的にJavaのプリミティブ型を使用します。

プリミティブ型以外の型については、完全修飾型を使用することをお勧めします。

その他の定義のtype

これは、生成したいエンティティの種類(protocolrecordenumeration)を単に示すものです。

設定 

この場所は、ビルド定義で新しい場所を設定することで変更できます。

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

このプラグインは、Scalaコード生成のためのその他の設定を提供します。

  1. Compile / generateDatatypes / datatypeScalaFileNames この設定は、生成された各Scala定義のファイル名を決定する関数Definition => Fileを受け付けます。
  2. Compile / generateDatatypes / datatypeScalaSealInterfaces この設定はブール値を受け付け、インターフェースを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)? }