Smithyを調べてみた①
JINSでITアーキテクト/テックリードしている@Takuma3atoです。今回は、前から少し気になっていたSmithyについて書きます。
Smithyについて
Smithyは、AWSが開発を進めているインターフェース定義言語(IDL)です。現在も開発されていて頻繁にアップデートがなされています。IDLとしてのバージョンは2系が最新のようです。(Changelogのバージョンの値は関係なさそう)
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.天候サービスの作成
「都市の天候情報を提供する」というサービスを作ってみます。このサービスには、 City
やForecast
などのリソースがあり、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
に対して、namespaceやserviceを用いて、サービスとしての最初を記述します。
$version: "2"
namespace example.weather
// ダブルスラッシュは、smithyファイル内でコメントアウトとして使用します
/// トリプルスラッシュは、shapes(次に記載)に対するドキュメンテーションコメントとして使用します
service Weather {
version: "2006-03-01"
}
1-2.リソースを定義する
SmithyはShapeと呼ばれるモデルを定義する宣言によって構成されていきます。まず初めに定義するのはサービスのShapeです。
Shapeは以下のフォーマットで記述します。
{型} {名前} {
{プロパティ名}: {値}
}
以下に記載しているresourceは、Smithyで定義される基本的なモジュールで、データや機能を表します。
@pattern
は、traitの一つで、shapeに対する追加情報を付与できます。
$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
プロパティに追加します。
人が認知しやすい情報構成でリソースを定義していくので分かり易いですね。
resource City {
identifiers: { cityId: CityId }
read: GetCity
list: ListCities
resources: [
Forecast
]
}
resource Forecast {
identifiers: { cityId: CityId }
read: GetForecast
}
続けます。
リソースの状態(リソースが持っているデータや設定等)は、そのプロパティ(リソースの要素)によって表されます。City
リソースは「coordinates(座標)プロパティ」を持ち、Forecast
リソースは「chanceOfRain(降水確率)プロパティ」を持ちます。
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
のプロパティの内、put
、create
、read
、update
、delete
、list
については、resources
のライフサイクル操作の定義に使用します。このライフサイクル操作とは、明確に定義されたセマンティクスを使用し、リソースの状態を読み取ったりする標準的なメソッドを指します。リソースに対する操作を行う為の入力値と出力値は、リソースのプロパティまたは識別子に対応します。
以下は、City
に対してread
を定義したコード例です。
@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
を定義したコード例です。
@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のモデルを扱う、コード生成ツールや検証ツールは、サービス内のリソースの関係を深く理解できるようになります。
/// 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
サービスから現在時刻を取得しているコード例です。
/// 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
}
}
次回に続きます。
参考
Discussion