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種類の型を生成できます。
レコードは、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()"
}
]
}
]
}
これで、データ型の以前の定義に対してコンパイルされたコードは依然として実行されます。
サブプロジェクトに`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`言語で有効な式を含んでいる必要があります。
フィールドの基になる型を示します。
Scalaで使用する型は、常に表示したい型を使用してください。例えば、フィールドに整数値が含まれる場合は、Javaのint
ではなくInt
を使用してください。datatype
は、利用可能な場合は自動的にJavaのプリミティブ型を使用します。
プリミティブ型以外の型については、完全修飾型を使用することをお勧めします。
type
これは、生成したいエンティティの種類(protocol
、record
、enumeration
)を単に示すものです。
この場所は、ビルド定義で新しい場所を設定することで変更できます。
datatypeSource in generateDatatypes := file("some/location")
このプラグインは、Scalaコード生成のためのその他の設定を提供します。
Compile / generateDatatypes / datatypeScalaFileNames
この設定は、生成された各Scala定義のファイル名を決定する関数Definition => File
を受け付けます。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)? }