sbt-datatype 是一個程式碼產生函式庫和 sbt 自動外掛程式,可產生可成長的資料類型,並協助開發人員避免二進位相容性中斷。
與標準 Scala case 類別不同,此函式庫產生的資料類型 (或偽 case 類別) 允許開發人員在定義的資料類型中新增欄位,而不會中斷二進位相容性,同時提供 (幾乎) 與一般 case 類別相同的功能。唯一的差異在於資料類型不會產生 unapply
或 copy
方法,因為它們會中斷二進位相容性。
此外,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/datatype
和 src/test/datatype
中。以下說明如何組態您的建置
lazy val library = (project in file("library"))
.enablePlugins(DatatypePlugin)
.settings(
name := "foo library",
)
資料類型能夠產生三種型別
記錄會對應至 Java 或 Scala class
es,相當於 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
es 或 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
}
透過使用 since
和 default
參數,可以成長現有的資料類型,同時與已針對較舊版本資料類型定義編譯的類別保持二進位相容性。
請考慮以下資料類型的初始版本
{
"types": [
{
"name": "Greeting",
"type": "record",
"target": "Scala",
"fields": [
{
"name": "message",
"type": "String"
}
]
}
]
}
產生的程式碼可以在 Scala 程式中使用以下程式碼
val greeting = Greeting("hello")
現在假設您想要延伸資料類型,將日期包含至 Greeting
s。可以據此修改資料類型
{
"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)
綱要定義的所有元素都接受許多參數,這些參數將會影響產生的程式碼。這些參數並非適用於綱要的每個節點。請參閱語法摘要,以查看是否可以為節點定義參數。
名稱
此參數定義欄位、記錄、欄位等的名稱。
目標
此參數決定程式碼將在 Java 還是 Scala 中產生。
命名空間
此參數僅適用於 Definition
。它決定程式碼將在其中產生的套件。
doc
將會伴隨產生元素的 Javadoc。
fields
僅適用於 protocol
或 record
,它會描述構成產生實體的所有欄位。
types
對於 protocol
,它會定義延伸自它的子 protocol
s 和 record
s。
對於 enumeration
,它會定義列舉的值。
since
此參數僅適用於 field
。它會指出欄位已新增至其父 protocol
或 record
的版本。
定義此參數時,也必須定義 default
。
default
此參數僅適用於 field
。它會指出此欄位的預設值應為何,以防具有針對此資料類型較舊版本編譯的類別使用此欄位。
它必須包含在父 protocol
或 record
的 target
語言中有效的運算式。
field
的 type
它會指出欄位的基礎類型為何。
請一律使用您想要在 Scala 中看到的類型。例如,如果您的欄位將包含整數值,請使用 Int
而不是 Java 的 int
。如果可以使用 Java 的基本類型,則 datatype
會自動使用它們。
對於非基本類型,建議撰寫完整限定的類型。
type
它只會指出您想要產生的實體種類:protocol
、record
或 enumeration
。
可以透過在建置定義中設定新的位置來變更此位置
datatypeSource in generateDatatypes := file("some/location")
此外掛程式會公開其他用於 Scala 程式碼產生的設定
Compile / generateDatatypes / datatypeScalaFileNames
此設定會接受一個函式 Definition => File
,其將決定每個產生 Scala 定義的檔案名稱。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)? }