密輸品 

Contrabandは、現在JavaとScalaをターゲットとしたデータ型とAPIのための記述言語です。

APIの型とフィールドを記述すると、Contrabandは

Contrabandを使用すると、APIを時間とともに進化させることもできます。

設定 

ビルドでContrabandプラグインを有効にするには、次の行を`project/contraband.sbt`に追加します。

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

Contrabandスキーマは、`src/main/contraband`と`src/test/contraband`に配置する必要があります。ビルドの設定方法は次のとおりです。

lazy val library = (project in file("library")).
  enablePlugins(ContrabandPlugin).
  settings(
    name := "foo library"
  )

注記 

ContrabandはLightbendサブスクリプションではサポートされていません。

スキーマと型 

このページでは、GraphQLの型システムに基づいたContrabandの型システムについて説明します。

Contrabandは、既存のJSONベースのAPIへのアクセス、または独自のサービスの実装に使用できます。

Contrabandスキーマ言語 

特定のプログラミング言語の構文に依存したくないため、Contrabandスキーマについて説明するには、GraphQLのスキーマ言語を拡張します。

Contrabandスキーマは、ファイル拡張子`*.contra`で保存する必要があります。

レコード型とフィールド 

Contrabandスキーマの最も基本的なコンポーネントはレコード型であり、これはサービスから取得できるオブジェクトの種類とそのフィールドを表します。Contrabandスキーマ言語では、次のように表現できます。

package com.example
@target(Scala)

## Character represents the characters in Star Wars.
type Character {
  name: String!
  appearsIn: [com.example.Episode]!
}

共有語彙を持つために、それを詳しく見ていきましょう。

これで、Contrabandレコード型とその外観、およびContrabandスキーマ言語の基本的な読み方が分かりました。

`since`アノテーション 

スキーマの進化を可能にするため、Contrabandレコードのフィールドは、追加されたバージョンを宣言できます。

package com.example
@target(Scala)

type Greeting {
  value: String!
  x: Int @since("0.2.0")
}

これは、`value`フィールドは最初から(`"0.0.0"`)存在していましたが、オプションの`x`フィールドはバージョン`"0.2.0"`以降に追加されたことを意味します。Contrabandは、バイナリ互換性を維持するために複数のコンストラクタを生成します。

`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`は自動的にオプションでラップされます。

スカラー型 

Contrabandには、すぐに使用できるデフォルトのスカラー型のセットが用意されています。

`java.io.File`などのJavaとScalaのクラス名も使用できます。

`java.io.File`などのクラス名を使用する場合は、型のシリアル化とデシリアル化の方法も指定する必要があります。

列挙型 

Enumとも呼ばれる列挙型は、特定の許可された値のセットに制限された特殊な種類のスカラーです。これにより、

  1. この型の引数が許可された値のいずれかであることを検証できます。
  2. フィールドが常に有限の値セットのいずれかであることを、型システムを通じて伝達できます。

Contrabandスキーマ言語でのenum定義は次のようになります。

package com.example
@target(Scala)

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

これは、スキーマで`Episode`型を使用する場合は常に、`NewHope`、`Empire`、または`Jedi`のいずれかであることを意味します。

必須型 

レコード型とenumは、Contrabandで定義できる唯一の種類の型です。ただし、スキーマの他の部分で型を使用する場合は、これらの値の検証に影響を与える追加の型修飾子を使用できます。例を見てみましょう。

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`で示されます。

インターフェース 

多くの型システムと同様に、Contrabandはインターフェースをサポートしています。インターフェースは、型がインターフェースを実装するために含める必要がある特定のフィールドセットを含む抽象型です。

たとえば、スターウォーズ三部作のあらゆるキャラクターを表す`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コードを挿入するためのエスケープハッチとして、Contrabandは特別なコメント表記を提供します。

## 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
}

コード生成 

このページでは、Contrabandの型システムがJavaとScalaでどのようにエンコードされるかを説明します。

レコード型 

レコード型は、Scalaの標準的なケースクラスに対応するJavaまたはScalaクラスにマップされます。

標準的なケースクラスは便利ですが、バイナリ互換性を壊さずに新しいフィールドを追加することはできません。Contrabandレコード(または疑似ケースクラス)を使用すると、プレーンなケースクラスとほぼ同じ機能を提供しながら、バイナリ互換性を壊さずに新しいフィールドを追加できます。

package com.example
@target(Scala)

type Person {
  name: String!
  age: Int
}

このスキーマは、次のScalaクラスを生成します。

/**
 * This code is generated using [[https://sbt.dokyumento.jp/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))
}

標準的なケースクラスとは異なり、Contrabandレコードは`unapply`またはパブリック`copy`メソッドを実装しません。これらはバイナリ互換性のある方法で進化させることができません。

`copy`の代わりに、各フィールドに対して`withX(...)`メソッドを生成します。

> val x = Person("Alice", 20)
> x.withAge(21)

これが、ターゲットアノテーションを`Java`に変更した後に生成されるJavaコードです。

/**
 * This code is generated using [[https://sbt.dokyumento.jp/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`を追加すると、Contraband型用の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
}

すべてのトレイトをミックスインする完全なコーデック`CustomJsonProtocol`とともに、`com.example.codec`パッケージ下にJsonFormatトレイトが生成されます。

生成された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
}