🐧

Smithyを調べてみた①

2024/05/09に公開

JINSでITアーキテクト/テックリードしている@Takuma3atoです。今回は、前から少し気になっていたSmithyについて書きます。

Smithyについて

Smithyは、AWSが開発を進めているインターフェース定義言語(IDL)です。現在も開発されていて頻繁にアップデートがなされています。IDLとしてのバージョンは2系が最新のようです。(Changelogのバージョンの値は関係なさそう)
https://aws.amazon.com/jp/blogs/developer/introducing-smithy-idl-2-0/

Smithyについての記事を2つに分けて投稿します。1回目はSmithyのモデル定義について、2回目は定義したモデルをビルドするところについて書きたいと思います。今回は1回目です。

とりあえずGemini Advancedに聞いてみる

もう少しSmithyの概要を知りたいので、Gemini Advancedに聞いてみました。

//プロンプト
インターフェース定義言語に分類される Smithy について、どのような機能があるのか、箇条書きでいくつか列挙してください。
//回答抜粋
1. 宣言的なインターフェース定義
サービスの構造、操作、データ型などを宣言的に定義
コード生成、ドキュメント生成、テスト生成などの自動化に貢献

2. プラットフォーム非依存性
AWS 以外のプラットフォームでも利用可能
言語やフレームワークに依存せず、汎用的に利用可能

3. 拡張性
プラグインシステムにより、機能を拡張
独自のドメインモデルやルールを追加

4. 検証
定義されたインターフェースに対する検証機能を提供
定義ミスや矛盾を検知し、開発効率を向上

5. コード生成
さまざまな言語でコードを自動生成
開発環境やプラットフォームに合わせて、適切なコードを生成

6. ドキュメント生成
API ドキュメントを自動生成
最新のインターフェース定義に基づいた、正確なドキュメントを生成

7. テスト生成
ユニットテストや統合テストを自動生成
インターフェース定義に基づいた、網羅的なテストを生成

なるほど。これは期待できそうなツールです。
ところで、体系立ててた情報を出力できるから生成系AIは良いですね。

Getting started

公式ドキュメントに沿って、準備してみます。

1.天候サービスの作成

「都市の天候情報を提供する」というサービスを作ってみます。このサービスには、 CityForecastなどのリソースがあり、ListCities, GetCity, GetForecast, GetCurrentTimeなどのオペレーションを定義します。

1-1.下準備をする

まずは作業の資材置き場を準備します。smithy-quickstartというディレクトリの下にmodelというサブディレクトリを作成します。このmodelにsmithyのリソースを配置していきます。

# 作成したい資材置き場
smithy-quickstart/
└── model/
    └── weather.smithy
# 必要なコマンドをまとめて発行
mkdir -p smithy-quickstart/model \
&& touch smithy-quickstart/model/weather.smithy \
&& cd smithy-quickstart

きちんと作成されたことをtreeコマンドで確認します。
treeコマンドは、Macには標準では入っていないので、予めbrew install tree でインストールしてください。

smithy-quickstart$ tree
.
└── model
    └── weather.smithy

次に、作成したweather.smithyに対して、namespaceserviceを用いて、サービスとしての最初を記述します。

model/weather.smithy
$version: "2"
namespace example.weather

// ダブルスラッシュは、smithyファイル内でコメントアウトとして使用します
/// トリプルスラッシュは、shapes(次に記載)に対するドキュメンテーションコメントとして使用します
service Weather {
    version: "2006-03-01"
}

1-2.リソースを定義する

SmithyはShapeと呼ばれるモデルを定義する宣言によって構成されていきます。まず初めに定義するのはサービスのShapeです。

Shapeは以下のフォーマットで記述します。

{型} {名前} { 
  {プロパティ名}: {値}
}

以下に記載しているresourceは、Smithyで定義される基本的なモジュールで、データや機能を表します。
@patternは、traitの一つで、shapeに対する追加情報を付与できます。

model/weather.smithy (追記1)
$version: "2"
namespace example.weather

// ダブルスラッシュは、smithyファイル内でコメントアウトとして使用します
/// トリプルスラッシュは、shapesに対するドキュメンテーションコメントとして使用します
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
}

resource City {
    identifiers: { cityId: CityId }
    read: GetCity
    list: ListCities
}

// "pattern" は、トレイトです。
@pattern("^[A-Za-z0-9 ]+$")
string CityId

さらに記述していきます。Weatherサービスは、都市の天候情報を提供するので多数の都市が含まれます。その為Cityリソースは識別子(identifier)を定義して一意性を保つようにします。各Cityには単一のForecastが存在するので、これをCityリソース内のresourcesプロパティに追加します。
人が認知しやすい情報構成でリソースを定義していくので分かり易いですね。

model/weather.smithy (追記2)
resource City {
    identifiers: { cityId: CityId }
    read: GetCity
    list: ListCities
    resources: [
        Forecast
    ]
}

resource Forecast {
    identifiers: { cityId: CityId }
    read: GetForecast
}

続けます。
リソースの状態(リソースが持っているデータや設定等)は、そのプロパティ(リソースの要素)によって表されます。Cityリソースは「coordinates(座標)プロパティ」を持ち、Forecastリソースは「chanceOfRain(降水確率)プロパティ」を持ちます。

model/weather.smithy (ここまでの全体)
resource City {
    identifiers: { cityId: CityId }
    properties: { coordinates: CityCoordinates }
    read: GetCity
    list: ListCities
    resources: [
        Forecast
    ]
}

structure CityCoordinates {
    @required
    latitude: Float

    @required
    longitude: Float
}


structure GetCityOutput for City {
    $coordinates
}

resource Forecast {
    identifiers: { cityId: CityId }
    properties: { chanceOfRain: Float }
    read: GetForecast
}

structure GetForecastOutput for Forecast {
    $chanceOfRain
}

1-3.オペレーション(操作)を定義する

resourcesのプロパティの内、putcreatereadupdatedeletelistについては、resourcesのライフサイクル操作の定義に使用します。このライフサイクル操作とは、明確に定義されたセマンティクスを使用し、リソースの状態を読み取ったりする標準的なメソッドを指します。リソースに対する操作を行う為の入力値と出力値は、リソースのプロパティまたは識別子に対応します。

以下は、Cityに対してreadを定義したコード例です。

model/weather.smithy(追記3)
@readonly
operation GetCity {
    input := for City {
        // "cityId" provides the identifier for the resource and
        // has to be marked as required.
        @required
        $cityId
    }

    output := for City {
        // "required" is used on output to indicate if the service
        // will always provide a value for the member.
        // "notProperty" indicates that top-level input member "name"
        // is not bound to any resource property.
        @required
        @notProperty
        name: String

        @required
        $coordinates
    }

    errors: [
        NoSuchResource
    ]
}

// "error" is a trait that is used to specialize
// a structure as an error.
@error("client")
structure NoSuchResource {
    @required
    resourceType: String
}

以下は、Forecastに対してreadを定義したコード例です。

model/weather.smithy(追記4)
@readonly
operation GetForecast {
    // "cityId" provides the only identifier for the resource since
    // a Forecast doesn't have its own.
    input := for Forecast {
        @required
        $cityId
    }

    output := for Forecast {
        $chanceOfRain
    }
}

1-4.リソースをリスト化する

Weatherサービスには多くのCityリソースが含まれますので、listライフサイクル操作にてサービス内のすべての都市を一覧表示できるようにします。
ListCities操作はページネーションに対応しています。リストのサイズが増加する場合に備えてリソースを一覧表示するAPIにページネーションを追加することは、一般的に良い方法です。
CitySummaryの構造は、Cityリソースへの参照を定義します。 これにより、Smithyのモデルを扱う、コード生成ツールや検証ツールは、サービス内のリソースの関係を深く理解できるようになります。

model/weather.smithy(追記5)
/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
}

// The paginated trait indicates that the operation may
// return truncated results. Applying this trait to the service
// sets default pagination configuration settings on each operation.
@paginated(items: "items")
@readonly
operation ListCities {
    input := {
        nextToken: String
        pageSize: Integer
    }

    output := {
        nextToken: String

        @required
        items: CitySummaries
    }
}

// CitySummaries is a list of CitySummary structures.
list CitySummaries {
    member: CitySummary
}

// CitySummary contains a reference to a City.
@references([
    {
        resource: City
    }
])
structure CitySummary {
    @required
    cityId: CityId

    @required
    name: String
}

Smithyは、ライフサイクル操作に無い操作もサポートしています。operationsプロパティを使用すると、特別なライフサイクル指定なしで、任意のリソースまたはサービスのシェイプに操作を追加できます。
以下の操作は、Weatherサービスから現在時刻を取得しているコード例です。

model/weather.smithy(追記6)
/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
    operations: [
        GetCurrentTime
    ]
}

@readonly
operation GetCurrentTime {
    output := {
        @required
        time: Timestamp
    }
}

1-5.モデルをビルドする

次に上記で定義したモデルをビルドし、実際に使えるアーティファクトを生成しますが、方法としては、Smithy CLIをインストールして行うか、Gradleで実施します。今回は、Smithy CLIで行いたいと思います。詳細は次の章で記載します。

参考までに、この章で作成したモデル全体を記載します。

model/weather.smithyの全体
$version: "2"

namespace example.weather

/// Provides weather forecasts.
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
    version: "2006-03-01"
    resources: [
        City
    ]
    operations: [
        GetCurrentTime
    ]
}

resource City {
    identifiers: { cityId: CityId }
    properties: { coordinates: CityCoordinates }
    read: GetCity
    list: ListCities
    resources: [
        Forecast
    ]
}

resource Forecast {
    identifiers: { cityId: CityId }
    properties: { chanceOfRain: Float }
    read: GetForecast
}

// "pattern" is a trait.
@pattern("^[A-Za-z0-9 ]+$")
string CityId

@readonly
operation GetCity {
    input := for City {
        // "cityId" provides the identifier for the resource and
        // has to be marked as required.
        @required
        $cityId
    }

    output := for City {
        // "required" is used on output to indicate if the service
        // will always provide a value for the member.
        // "notProperty" indicates that top-level input member "name"
        // is not bound to any resource property.
        @required
        @notProperty
        name: String

        @required
        $coordinates
    }

    errors: [
        NoSuchResource
    ]
}

// This structure is nested within GetCityOutput.
structure CityCoordinates {
    @required
    latitude: Float

    @required
    longitude: Float
}

// "error" is a trait that is used to specialize
// a structure as an error.
@error("client")
structure NoSuchResource {
    @required
    resourceType: String
}

// The paginated trait indicates that the operation may
// return truncated results.
@readonly
@paginated(items: "items")
operation ListCities {
    input := {
        nextToken: String
        pageSize: Integer
    }

    output := {
        nextToken: String

        @required
        items: CitySummaries
    }
}

// CitySummaries is a list of CitySummary structures.
list CitySummaries {
    member: CitySummary
}

// CitySummary contains a reference to a City.
@references([
    {
        resource: City
    }
])
structure CitySummary {
    @required
    cityId: CityId

    @required
    name: String
}

@readonly
operation GetCurrentTime {
    output := {
        @required
        time: Timestamp
    }
}

@readonly
operation GetForecast {
    input := for Forecast {
        // "cityId" provides the only identifier for the resource since
        // a Forecast doesn't have its own.
        @required
        $cityId
    }

    output := for Forecast {
        $chanceOfRain
    }
}

次回に続きます。

参考

https://smithy.io/2.0/index.html
https://github.com/smithy-lang/awesome-smithy
https://zenn.dev/seumo/articles/d33581c111a6d7

JINSテックブログ

Discussion