違禁品是一種描述您的資料類型和 API 的描述語言,目前目標是 Java 和 Scala。
您描述 API 的類型和欄位,違禁品將會
違禁品也使您能夠隨著時間的推移發展 API。
要為您的建置啟用違禁品外掛程式,請將以下程式碼放在 project/contraband.sbt
中
addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "X.Y.Z")
您的違禁品模式應放在 src/main/contraband
和 src/test/contraband
中。以下是您的建置應如何設定
lazy val library = (project in file("library")).
enablePlugins(ContrabandPlugin).
settings(
name := "foo library"
)
違禁品在 Lightbend 訂閱下不支援。
此頁面描述違禁品類型系統,該系統基於 GraphQL 類型系統。
違禁品可用於存取現有的 JSON 架構 API,或實作您自己的服務。
由於我們不想依賴特定的程式語言語法來談論違禁品模式,因此我們將擴展 GraphQL 的模式語言。
違禁品模式應以檔案副檔名 *.contra
儲存。
違禁品模式最基本的組成部分是記錄類型,它僅代表您可以從服務中獲取的物件類型,以及它擁有哪些欄位。在違禁品模式語言中,我們可以這樣表示它
package com.example
@target(Scala)
## Character represents the characters in Star Wars.
type Character {
name: String!
appearsIn: [com.example.Episode]!
}
讓我們複習一下,以便我們有一個共同的詞彙
com.example
是此模式的套件名稱。此套件名稱將用於產生的程式碼。@target(Scala)
是套件的註釋。這表示程式碼產生預設將以 Scala 為目標。##
表示記錄類型的文件註解。Character
是一種違禁品記錄類型,表示它是一種帶有一些欄位的類型。您模式中的大多數類型將是記錄類型。在 Java 和 Scala 中,它被編碼為類別。name
和 appearsIn
是 Character
類型上的欄位。這表示 name
和 appearsIn
是 Character
類型的 JSON 物件中唯一可以出現的欄位。String
是內建的純量類型之一。String!
表示該欄位是必要的,表示服務保證在您查詢此欄位時總是會提供一個值。在模式語言中,我們將用驚嘆號表示這些欄位。[Episode]!
表示 Episode
記錄的列表。由於它也是必要的,因此您在查詢 appearsIn
欄位時始終可以預期獲得一個列表(包含零個或多個項目)。現在您知道違禁品記錄類型是什麼樣子,以及如何閱讀違禁品模式語言的基本知識。
為了啟用模式演進,違禁品記錄中的欄位可以宣告加入時的版本
package com.example
@target(Scala)
type Greeting {
value: String!
x: Int @since("0.2.0")
}
這表示 value
欄位從一開始 ("0.0.0"
) 就存在,但可選的 x
欄位是從版本 "0.2.0"
加入的。違禁品將產生多個建構子來維持二進位相容性。
由於 Int
是可選的,因此 None
用作 x
的預設值。若要提供其他預設值,您可以按如下所示編寫
package com.example
@target(Scala)
type Greeting {
value: String!
x: Int = 0 @since("0.2.0")
p: Person = { name: "Foo" } @since("0.2.0")
z: Person = raw"Person(\"Foo\")"
}
請注意,0
將自動以選項包裝。
違禁品開箱即用提供一組預設純量類型
String
Boolean
Byte
Char
Int
Long
Short
Double
您也可以使用 Java 和 Scala 類別名稱,例如 java.io.File
。
如果您使用類別名稱(例如 java.io.File
),您還必須提供應如何序列化和還原序列化類型。
也稱為 Enums,列舉類型是一種特殊的純量,它限制於一組特定的允許值。這讓您可以
以下是 enum 定義在違禁品模式語言中的樣子
package com.example
@target(Scala)
## Star Wars trilogy.
enum Episode {
NewHope
Empire
Jedi
}
這表示無論我們在模式中使用 Episode
類型,我們都希望它是 NewHope
、Empire
或 Jedi
中的其中一個。
記錄類型和 enum 是您可以在違禁品中定義的唯一類型。但是當您在模式的其他部分中使用類型時,您可以套用其他類型修飾符,這些修飾符會影響這些值的驗證。讓我們看一個範例
package com.example
@target(Scala)
## Character represents the characters in Star Wars.
type Character {
name: String!
appearsIn: [com.example.Episode]!
friends: lazy [com.example.Character]
}
在這裡,我們使用 String
類型,並在類型名稱後新增驚嘆號 !
,將其標記為必要類型。
列表的工作方式類似:我們可以使用類型修飾符將類型標記為列表,這表示此欄位將傳回該類型的列表。在模式語言中,這是透過將類型括在方括號 [
和 ]
中來表示的。
惰性類型會延遲欄位的初始化,直到第一次使用為止。在模式語言中,這是透過關鍵字 lazy
表示的。
與許多類型系統一樣,違禁品支援介面。介面是一種抽象類型,其中包含類型必須包含才能實作介面的一組特定欄位。
例如,您可以有一個介面 Character
,代表《星際大戰》三部曲中的任何角色
package com.example
@target(Scala)
## Character represents the characters in Star Wars.
interface Character {
name: String!
appearsIn: [com.example.Episode]!
friends: lazy [com.example.Character]
}
這表示實作 Character
的任何類型都需要有這些確切的欄位。
例如,以下是一些可以實作 Character
的類型
package com.example
@target(Scala)
type Human implements Character {
name: String!
appearsIn: [com.example.Episode]!
friends: lazy [com.example.Character]
starships: [com.example.Starship]
totalCredits: Int
}
type Droid implements Character {
name: String!
appearsIn: [com.example.Episode]!
friends: lazy [com.example.Character]
primaryFunction: String
}
您可以看到這兩種類型都具有 Character
介面的所有欄位,但也引入了額外的欄位 totalCredits
、starships
和 primaryFunction
,這些欄位是特定於該特定角色類型的。
除了欄位外,介面還可以宣告訊息。
package com.example
@target(Scala)
## Starship represents the starships in Star Wars.
interface Starship {
name: String!
length(unit: com.example.LengthUnit): Double
}
這表示實作 Starship
的任何類型都需要同時具有確切的欄位和訊息。
作為將 Scala 或 Java 程式碼注入產生程式碼的逃生方法,違禁品提供了特殊的註解表示法。
## Example of an interface
interface IntfExample {
field: Int
#x // Some extra code
#xinterface Interface1
#xinterface Interface2
#xtostring return "custom";
#xcompanion // Some extra companion code
#xcompanioninterface CompanionInterface1
#xcompanioninterface CompanionInterface2
}
#x
將程式碼注入產生類別的主體中。#xinterface
新增額外的父類別。#xtostring
用於提供自訂的 toString
方法。#xcompanion
將程式碼注入產生類別的伴隨物件中。#xcompanioninterface
將額外的父類別新增至伴隨物件。此頁面描述如何將違禁品類型系統編碼在 Java 和 Scala 中。
記錄類型會對應到 Java 或 Scala 類別,對應到 Scala 中的標準案例類別。
雖然標準案例類別很方便開始,但無法新增新的欄位而不破壞二進位相容性。違禁品記錄(或偽案例類別)可讓您新增新的欄位而不破壞二進位相容性,同時提供(幾乎)與純案例類別相同的功能。
package com.example
@target(Scala)
type Person {
name: String!
age: Int
}
此模式將產生以下 Scala 類別
/**
* This code is generated using [[https://sbt.dev.org.tw/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package com.example
final class Person private (
val name: String,
val age: Option[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: Option[Int] = age): Person = {
new Person(name, age)
}
def withName(name: String): Person = {
copy(name = name)
}
def withAge(age: Option[Int]): Person = {
copy(age = age)
}
def withAge(age: Int): Person = {
copy(age = Option(age))
}
}
object Person {
def apply(name: String, age: Option[Int]): Person = new Person(name, age)
def apply(name: String, age: Int): Person = new Person(name, Option(age))
}
與標準案例類別不同,違禁品記錄不實作 unapply
或公開的 copy
方法,這無法以二進位相容的方式演進。
它會針對每個欄位產生 withX(...)
方法,而不是 copy
。
> val x = Person("Alice", 20)
> x.withAge(21)
以下是它產生的 Java 程式碼(將目標註釋變更為 Java
之後)
/**
* This code is generated using [[https://sbt.dev.org.tw/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package com.example;
public final class Person implements java.io.Serializable {
public static Person create(String _name, java.util.Optional<Integer> _age) {
return new Person(_name, _age);
}
public static Person of(String _name, java.util.Optional<Integer> _age) {
return new Person(_name, _age);
}
public static Person create(String _name, int _age) {
return new Person(_name, _age);
}
public static Person of(String _name, int _age) {
return new Person(_name, _age);
}
private String name;
private java.util.Optional<Integer> age;
protected Person(String _name, java.util.Optional<Integer> _age) {
super();
name = _name;
age = _age;
}
protected Person(String _name, int _age) {
super();
name = _name;
age = java.util.Optional.<Integer>ofNullable(_age);
}
public String name() {
return this.name;
}
public java.util.Optional<Integer> age() {
return this.age;
}
public Person withName(String name) {
return new Person(name, age);
}
public Person withAge(java.util.Optional<Integer> age) {
return new Person(name, age);
}
public Person withAge(int age) {
return new Person(name, java.util.Optional.<Integer>ofNullable(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().equals(o.age());
}
}
public int hashCode() {
return 37 * (37 * (37 * (17 + "com.example.Person".hashCode()) + name().hashCode()) + age().hashCode());
}
public String toString() {
return "Person(" + "name: " + name() + ", " + "age: " + age() + ")";
}
}
將 JsonCodecPlugin
新增至子專案將會為違禁品類型產生 sjson-new JSON 編碼。
lazy val root = (project in file(".")).
enablePlugins(ContrabandPlugin, JsonCodecPlugin).
settings(
scalaVersion := "2.11.8",
libraryDependencies += "com.eed3si9n" %% "sjson-new-scalajson" % contrabandSjsonNewVersion.value
)
sjson-new 是一種編碼工具組,可讓您定義支援 Spray JSON 的 AST、SLIP-28 Scala JSON 和 MessagePack 作為後端的程式碼。
可以使用 @codecPackage
指令指定編碼器的套件名稱。
package com.example
@target(Scala)
@codecPackage("com.example.codec")
@codecTypeField("type")
@fullCodec("CustomJsonProtocol")
type Person {
name: String!
age: Int
}
JsonFormat 特徵將在 com.example.codec
套件下產生,以及混合所有特徵的完整編碼器,名為 CustomJsonProtocol
。
以下是如何使用產生的 JSON 編碼器
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)
使用 @generateCodec(false)
註釋跳過某些類型的編碼器產生。
interface MiddleInterface implements InterfaceExample
@generateCodec(false)
{
field: Int
}