走私 

走私是一种用于描述数据类型和 API 的语言,目前目标是 Java 和 Scala。

您可以描述 API 的类型和字段,走私将

走私还使您能够随着时间的推移来演变 API。

设置 

要为您的构建启用走私插件,请在 project/contraband.sbt 中添加以下行

addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "X.Y.Z")

您的走私模式应放置在 src/main/contrabandsrc/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]!
}

让我们仔细看看它,以便我们可以有一个共同的词汇表

现在您知道了走私记录类型的样貌,以及如何阅读走私模式语言的基本知识。

since 注释 

为了启用模式演变,走私记录中的字段可以声明添加它的版本

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 将自动用选项包装。

标量类型 

走私开箱即用地提供了一组默认标量类型

您还可以使用 Java 和 Scala 类名,例如 java.io.File

如果您使用 java.io.File 等类名,则还必须提供如何序列化和反序列化该类型的说明。

枚举类型 

也称为枚举,枚举类型是一种特殊的标量类型,它被限制为一组特定的允许值。这使您能够

  1. 验证此类型的所有参数是否都是允许的值之一。
  2. 通过类型系统传达字段始终将是一组有限的值之一。

以下是在走私模式语言中枚举定义可能的样子

package com.example
@target(Scala)

## Star Wars trilogy.
enum Episode {
  NewHope
  Empire
  Jedi
}

这意味着无论我们在模式中使用 Episode 类型,我们都希望它恰好是 NewHopeEmpireJedi 之一。

必需类型 

记录类型和枚举是您可以在走私中定义的唯一类型。但是,当您在模式的其他部分使用类型时,您可以应用额外的类型修饰符,这些修饰符会影响对这些值的验证。让我们看一个例子

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 接口中的所有字段,但还带来了 totalCreditsstarshipsprimaryFunction 等额外字段,这些字段是特定于该特定角色类型的。

消息 

除了字段之外,接口还可以声明消息。

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
}

代码生成 

本页介绍走私类型系统如何在 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() + ")";
    }
}

JSON 编解码器生成 

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
}