Smithy-rsの使い方

この記事では、Smithy-rsを使ってRust APIサーバーを作る方法をレポジトリの例を使って解説します。

Smithy-rsは、AWSが開発したAPIモデリング言語「Smithy」を使って、Rustのクライアントとサーバーコードを自動生成するツールです。

## Smithyとは

Smithyは、AWSが開発したプロトコルに依存しないAPIモデリング言語です。AWSでは10年以上前から内部で使用されており、数万のサービス構築に活用されてきました。2019年にオープンソース化され、現在は誰でも利用できます。

Smithyの主な特徴は以下の通りです：

- **プロトコル非依存** - 様々なプログラミング言語、環境、トランスポート層、シリアライゼーション形式と連携できます
- **リソース指向** - リソースとオペレーションを中心にAPIを定義することで、より自然なAPI設計が可能です
- **拡張性** - トレイト（trait）と呼ばれる仕組みで容易に拡張できます
- **検証機能** - モデルの検証ツールにより、高品質なAPIを設計できます
- **コード生成** - 様々な言語のクライアント、サーバー、ドキュメントを生成できます

Smithyモデルは、シェイプ（Shape）とトレイト（Trait）から構成されます。シェイプには基本的な型（string, integerなど）、構造体（structure）、サービス（service）、オペレーション（operation）などがあります。トレイトはシェイプに追加情報を与えるメタデータです。

現在、AWSではAWS CDK、AWS SDK、AWS CLIなどの多くのツールやライブラリがSmithyをベースに開発されています。

## 必要な環境

まずは開発環境を準備します：

- JDK 17（コード生成のために必要）
- Rust開発環境（rustc 1.68.0以上推奨）
- Git

## 1. リポジトリをクローンする

まずはSmitty-rsのリポジトリをクローンします：

```bash
git clone https://github.com/smithy-lang/smithy-rs.git
cd smithy-rs
```

## 2. Gradleの初期化

リポジトリのルートディレクトリで以下のコマンドを実行し、Gradleを初期化します：

```bash
./gradlew
```

これによりGradleのラッパーが初期化され、必要な依存関係がダウンロードされます。初回は少し時間がかかります。

## 3. サンプルアプリケーションを探索する

リポジトリには「ポケモンサービス」というサンプルアプリケーションが含まれています。このアプリはSmithyモデルから生成されたコードを使用して構築されています。

```bash
cd examples
```

サンプルアプリの構成は以下のようになっています：

```
examples/
├── Cargo.toml         # workspaceの設定
├── Makefile           # ビルド用のMakeコマンド
├── pokemon-service/   # HTTP版の実装
├── pokemon-service-tls/  # HTTPS版の実装
├── pokemon-service-lambda/  # Lambda版の実装
├── pokemon-service-common/  # 共通コード
```

重要なのは、これらのサービス実装が全てSmithyモデルから生成されたコードを使用している点です。そのモデルは以下にあります：

```bash
cat ../codegen-core/common-test-models/pokemon.smithy
```

## 4. Smithyモデルを確認する

`pokemon.smithy`ファイルを見てみましょう。これはAPIの全てを定義するファイルです：

```smithy
$version: "2"

namespace com.aws.example

use aws.protocols#restJson1
// 他のインポート省略

/// The Pokémon Service allows you to retrieve information about Pokémon species.
@title("Pokémon Service")
@restJson1
service PokemonService {
    version: "2024-03-18"
    resources: [
        PokemonSpecies
        Storage
    ]
    operations: [
        GetServerStatistics
        DoNothing
        CapturePokemon
        CheckHealth
        StreamPokemonRadio
    ]
}

/// Retrieve information about your Pokédex.
@readonly
@http(uri: "/pokedex/{user}", method: "GET")
operation GetStorage {
    input := @sensitive @documentation("A request to access Pokémon storage.") {
        @required
        @httpLabel
        user: String
        @required
        @httpHeader("passcode")
        passcode: String
    }
    output := @documentation("Contents of the Pokémon storage.") {
        @required
        collection: SpeciesCollection
    }
    errors: [
        ResourceNotFoundException
        StorageAccessNotAuthorized
        ValidationException
    ]
}

// 他の操作定義...
```

このファイルがAPIの設計図となっています。エンドポイント、メソッド、入出力パラメータ、エラー型などが全て定義されています。

## 5. コード生成を実行する

ではいよいよ、Smithyモデルからコードを生成しましょう！examplesディレクトリで以下のコマンドを実行します：

```bash
make codegen
```

このコマンドは内部的に以下のGradleコマンドを実行しています：

```bash
../gradlew --project-dir .. -P modules='pokemon-service-server-sdk,pokemon-service-client' :codegen-client-test:assemble :codegen-server-test:assemble
```

このGradleコマンドを詳しく解説してみましょう：

- `../gradlew` - 親ディレクトリにあるGradleラッパースクリプトを実行します。Gradleラッパーは、特定のバージョンのGradleをダウンロードして実行するスクリプトで、チーム内でGradleのバージョンを統一できます。

- `--project-dir ..` - プロジェクトディレクトリとして親ディレクトリを指定します。これにより、コマンドを実行する場所に関係なく、プロジェクトのルートを正しく参照できます。

- `-P modules='pokemon-service-server-sdk,pokemon-service-client'` - Gradleプロジェクトのプロパティを設定します。ここでは `modules` というプロパティに生成したいモジュール名を指定しています。このプロパティは、ビルドスクリプト内で条件分岐などに使われ、指定したモジュールのみをコード生成の対象とします。

- `:codegen-client-test:assemble` - `codegen-client-test` サブプロジェクトの `assemble` タスクを実行します。このタスクは、クライアントSDKのコード生成を行います。

- `:codegen-server-test:assemble` - `codegen-server-test` サブプロジェクトの `assemble` タスクを実行します。このタスクは、サーバーSDKのコード生成を行います。

つまり、このコマンド一つで、Smithyモデルからクライアントとサーバー両方のRustコードを同時に生成しています。コード生成プロセスはKotlinで書かれたプラグインによって実行され、指定されたSmithyモデル（ここでは `pokemon.smithy`）を解析して、対応するRustコードを出力します。

生成されたコードは以下のディレクトリに出力されます：
- サーバーSDK: `codegen-server-test/build/smithyprojections/codegen-server-test/pokemon-service-server-sdk/rust-server-codegen/`
- クライアントSDK: `codegen-client-test/build/smithyprojections/codegen-client-test/pokemon-service-client/rust-client-codegen/`

## 6. 生成されたコードを確認する

生成されたサーバーSDKを見てみましょう：

```bash
ls pokemon-service-server-sdk/src/
```

生成されたファイルには以下のようなものがあります：
- APIの入出力型の定義
- エラー型の定義
- モデル型の定義
- サーバーフレームワークコード

特に重要なファイルは `lib.rs` で、ここにはAPI全体の構造が定義されています。

## 7. サーバー実装を確認する

次に、生成されたSDKを使った実際のサーバー実装を見てみましょう：

```bash
cat pokemon-service/src/main.rs
```

ここで重要なのは、各APIエンドポイント（操作）に対応するハンドラー関数を登録している部分です：

```rust
let app = PokemonService::builder(config)
    // 各操作に実装を登録
    .get_pokemon_species(get_pokemon_species)
    .get_storage(get_storage_with_local_approved)
    .get_server_statistics(get_server_statistics)
    .capture_pokemon(capture_pokemon)
    .do_nothing(do_nothing_but_log_request_ids)
    .check_health(check_health)
    .stream_pokemon_radio(stream_pokemon_radio)
    .build()
    .expect("failed to build an instance of PokemonService");
```

各ハンドラー関数の実装は `pokemon-service-common/src/lib.rs` にあります：

```bash
cat pokemon-service-common/src/lib.rs
```

例えば、`get_pokemon_species` 関数の実装は以下のようになっています：

```rust
pub async fn get_pokemon_species(
    input: input::GetPokemonSpeciesInput,
    state: Extension<Arc<State>>,
) -> Result<output::GetPokemonSpeciesOutput, error::GetPokemonSpeciesError> {
    // カウンターをインクリメント
    state
        .0
        .call_count
        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
    
    // ポケモン情報を取得
    let pokemon = state.0.pokemons_translations.get(&input.name);
    match pokemon.as_ref() {
        Some(pokemon) => {
            // 成功レスポンスを構築
            let flavor_text_entries = vec![
                model::FlavorText {
                    flavor_text: pokemon.en.to_owned(),
                    language: model::Language::English,
                },
                // 他の言語...
            ];
            let output = output::GetPokemonSpeciesOutput {
                name: String::from("pikachu"),
                flavor_text_entries,
            };
            Ok(output)
        }
        None => {
            // エラーレスポンスを返す
            Err(error::GetPokemonSpeciesError::ResourceNotFoundException(
                error::ResourceNotFoundException {
                    message: String::from("Requested Pokémon not available"),
                },
            ))
        }
    }
}
```

## 8. アプリケーションをビルドして実行する

では、サンプルアプリをビルドして実行してみましょう：

```bash
make build
```

特定のサーバー実装を指定して実行してみます：

```bash
cargo run --bin pokemon-service
```

デフォルトでは、サーバーは `http://localhost:13734` で起動します。

## 9. APIをテストする

別のターミナルを開いて、起動したサーバーにリクエストを送ってみましょう：

```bash
# ポケモン情報の取得
curl http://localhost:13734/pokemon-species/pikachu

# ストレージの取得（認証が必要）
curl -H "passcode: pikachu123" http://localhost:13734/pokedex/ash
```

## まとめ

Smithy-rsを使ったAPIサーバーの作成プロセスを体験しました：

1. **Smithyモデルの定義**: APIの全ての機能を宣言的に定義
2. **コード生成**: Gradleを使ったコード生成
3. **サーバー実装**: 生成されたSDKを使ったハンドラー関数の実装
4. **実行とテスト**: 実際のサーバーの起動とAPI呼び出し

Smithy-rsの主なメリットは以下の通りです：

- **型安全**: 生成されたコードは完全に型安全
- **ボイラープレートの削減**: 手作業でのAPI実装に比べてコード量を大幅に削減
- **一貫性**: クライアントとサーバーが同じモデルから生成されるため一貫性が高い
- **高性能**: Rustのゼロコスト抽象化を活かした高性能な実装

より複雑なAPIを作成したり、AWS Lambdaへのデプロイなども可能です。

## 参考リンク

- [Smithy-rs GitHub リポジトリ](https://github.com/smithy-lang/smithy-rs)
- [Smithy-rs Design ドキュメント](https://smithy-lang.github.io/smithy-rs/design/)
- [Smithy 言語仕様](https://smithy.io/2.0/spec/index.html)
- [AWS SDK for Rust](https://github.com/awslabs/aws-sdk-rust)
- [Kotlin JVM ターゲットの互換性について](https://discuss.gradle.org/t/got-unknown-kotlin-jvm-target-20-when-running-gradle-build/46152) - JVMターゲットエラーの参考情報