0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Smithy-rsの使い方

Posted at

この記事では、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のリポジトリをクローンします:

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

2. Gradleの初期化

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

./gradlew

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

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

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

cd examples

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

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

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

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

4. Smithyモデルを確認する

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

$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ディレクトリで以下のコマンドを実行します:

make codegen

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

../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を見てみましょう:

ls pokemon-service-server-sdk/src/

生成されたファイルには以下のようなものがあります:

  • APIの入出力型の定義
  • エラー型の定義
  • モデル型の定義
  • サーバーフレームワークコード

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

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

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

cat pokemon-service/src/main.rs

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

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 にあります:

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

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

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. アプリケーションをビルドして実行する

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

make build

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

cargo run --bin pokemon-service

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

9. APIをテストする

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

# ポケモン情報の取得
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へのデプロイなども可能です。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?