走私是一种用于描述数据类型和 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
等类名,则还必须提供如何序列化和反序列化该类型的说明。
也称为枚举,枚举类型是一种特殊的标量类型,它被限制为一组特定的允许值。这使您能够
以下是在走私模式语言中枚举定义可能的样子
package com.example
@target(Scala)
## Star Wars trilogy.
enum Episode {
NewHope
Empire
Jedi
}
这意味着无论我们在模式中使用 Episode
类型,我们都希望它恰好是 NewHope
、Empire
或 Jedi
之一。
记录类型和枚举是您可以在走私中定义的唯一类型。但是,当您在模式的其他部分使用类型时,您可以应用额外的类型修饰符,这些修饰符会影响对这些值的验证。让我们看一个例子
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.scala-lang.org.cn/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.scala-lang.org.cn/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
}