二、如何使用 Smithy

前面說過,環境的設置是第一道難關。由于 Smithy CLI 的出現,這個問題得到大大地緩解。感恩節期間,我探索出一套簡單可行的環境配置方案。

一開始,我是想把所有應用到的資源都打包成一個 docker,然后把 smithy build 所需要的配置和 model 描述放進 docker 中進行構建,生成的代碼所在的路徑映射到宿主機。

后來我發現這有些多此一舉:Smithy 所依賴的代碼生成器都是 jar 包,所以我其實預先打包好這些 jar,使用時(無論是 CI 還是本地)直接下載這些 jar,放在合適的位置,然后使用即可。

比如寫這樣的 Makefile 來設置環境:

ASSETS = assets.tar.gz

update-smithy:
@gh release download -R tyrchen/smithy-docker -p '$(ASSETS)'
@rm -rf $HOME/.m2
@tar -xzf $(ASSETS) -C $(HOME) --strip-components=2
@rm $(ASSETS)

其中 tyrchen/smithy-docker 是我用于編譯各種 Smithy 代碼生成器的 repo,編譯好的資源打包成一個 assets.tar.gz。你只需要將其下載下來,解壓到本地的 maven 路徑下即可。注意 Smithy 相關的代碼都用 jdk 17,所以確保你本地的 java 是正確的版本。

之后,安裝 Smithy CLI,撰寫你的 model,以及 build 配置,就可以用 smithy build 生成相關的代碼了。

有了這個環境,你再也不需要和 gradle 以及復雜的環境設置打交道了。

三、構建第一個 Smithy 服務

我構建了一個最簡單的 repo: tyrchen/smithy-test 來展示如何使用 smithy,你可以使用 make build-smithy 來體驗對一個 smithy API 定義構建出 Rust 服務器和客戶端,python/typescript/swift 客戶端的過程。

服務的 model 如下:

$version: "2.0"

namespace com.example

use aws.protocols#restJson1
use smithy.framework#ValidationException

@restJson1
service EchoService {
version: "2006-03-01"
operations: [EchoMessage]
}

@http(uri: "/echo", method: "POST")
operation EchoMessage {
input := {
@required
@httpHeader("x-echo-message")
message: String
}
output := {
@required
message: String
}
errors: [ValidationException]
}

它只有一個服務,服務下沒有任何資源(resource),只有一個操作(operation)。Smithy IDL 有 trait 的概念,其中 @ 開頭的這些修飾,如 restJson1http 都是 trait。trait 具體描述了代碼生成時,這個服務使用什么協議(http),該如何序列化/反序列化資源(restJson1),以及哪些是必要字段(required),字段出現在服務操作的什么位置(httpHeader,httpLabel,httpPayload 等)。每個操作可以定義 inputoutputerrors

比如一個更復雜的服務:

@service(sdkId: "taotie")
@httpBearerAuth
@restJson1
service TaotieService {
version: "2006-03-01"
resources: [Logger]
operations: [Flush, Signin]
}

/// Logger resource provides the ability to manage different loggers.
resource Logger {
identifiers: { id: Name }
read: GetLogger
create: CreateLogger
operations: [IngestLog]
}

@readonly
@http(uri: "/logger/{id}", method: "GET")
operation GetLogger {
input := {
@required
@httpLabel
id: Name
}
output := {
@required
@httpPayload
payload: LoggerSummary
}
errors: [ValidationException, NotFoundError, ThrottlingError, ServerError]
}

...

structure LoggerSummary {
@required
total: Integer

@required
schema: LoggerSchema
}

...

@error("client")
@retryable
@httpError(429)
structure ThrottlingError {
@required
message: String
}

...

@error("server")
@httpError(500)
structure ServerError {
@required
code: ErrorCode
@required
message: String
}

服務的輸入輸出可以以內聯的方式定義,也可以詳細定義每種數據結構。數據結構可以用 structure,list 和 enum 來定義。有了上述基本的介紹,相信不難理解這段 spec。如果你想要學習更多關于 spec 的細節,可以自行閱讀 Smithy 2.0 官方文檔。

搞定了 model 的定義后,你還需要一個 smithy-build.json 來描述如何讓 smithy build 工作。根據你使用的代碼生成器的多少,這個配置文件可以很長,但基本上根據示例文件,然后連蒙帶猜可以攢出一個可用的版本(見 tyrchen/smithy-test):

如果一切正常,那么,運行 smithy build 就可以生成大量的代碼。

四、使用 Smithy 生成的服務器代碼

Smithy 官方支持 rust / typescript 的服務器代碼生成,但這里我們只介紹 Rust。感謝 Rust 生態下的 tower 生態和 hyper 生態(它們衍生出 tower-http,axum,tonic 等一系列優秀的 crate),smithy-rs 也將它們作為構建服務端 SDK 的基石。這也就意味著 smithy 生成的代碼中廣泛采用的注入 RequestResponse 這樣的概念大家都是相通的,比如你可以通過 Extension 為路由添加新的屬性,你可以用 ServiceLayer 來構建中間層。甚至,你可以把 Smithy 生成的 server SDK 作為一個 Route Service 添加到 axum 的一個子路由中,實現 website 和 REST API 共用同一個服務器的功能。在我的 smithy-test 演示代碼中,我混用了 axum 和 smithy server sdk:

pub async fn get_router(conf: AppConfig) -> Router {
// make name with static lifetime
let name = Box::leak(Box::new(conf.server_name.clone()));

let state = Arc::new(AppState::new(conf));

// smithy config
let config = EchoServiceConfig::builder()
// IdentityPlugin is a plugin that adds a middleware to the service, it just shows how to use plugins
.http_plugin(IdentityPlugin)
.layer(AddExtensionLayer::new(state.clone()))
// 我做的 smithy auth middleware
.layer(BearerTokenProviderLayer::new())
.layer(ServerRequestIdProviderLayer::new_with_response_header(
HeaderName::from_static("x-request-id"),
))
.build();

// smithy 兼容 Tower 的 service
let api = EchoService::builder(config)
.echo_message(api::echo_message)
.signin(api::signin)
.build()
.expect("failed to build an instance of Echo Service");

let doc_url = "/swagger/openapi.json";
let doc = include_str!("../../../smithy/gen/openapi/EchoService.openapi.json");

// axum 路由
Router::new()
.route("/swagger", get(|| async { Html(swagger_ui(doc_url)) }))
.route(doc_url, get(move || async move { doc }))
// 使用 nest_service 將 smithy service 加載為子路由
.nest_service("/api/", api)
// 在 smithy 之外定義的 middleware 也會影響 smithy Response
.layer(ServerTimingLayer::new(name))
.with_state(state)
}

如果你熟悉 axum,那么這段代碼很好地詮釋了 tower 生態的強大之處。說句題外話,我覺得 Rust 下的 web 框架,如果現在還沒有構建在 tower 和 hyper 生態下,那么是不值得學習和使用的。它們會慢慢凋零,無論它曾經有多大的用戶群體。Rocket 如此,actix-web 亦如此。

如上代碼所示,構建一個 smithy server sdk 的 middleware 其實就是構建一個 tower Service。比如代碼中提供的 Bearer Auth 的支持,就是實現這個接口而已:

impl<Body, S> Service<Request<Body>> for BearerTokenProvider<S>
where
S: Service<Request<Body>, Response = Response<BoxBody>>,
S::Future: std::marker::Send + 'static,

由于我對 smithy 的了解還不夠深入,所以我一直沒有找到正確的姿勢讓 smithy server sdk 生成處理 auth 的代碼。我覺得這應該是框架提供的,我只需要提供一個回調函數來驗證 auth token 即可。然而我并未在 smithy-rs 的源碼中找到這樣的代碼,所以這里我自己為其實現了一個 middleware。

在我嘗試構建服務端代碼時,我的一個最大的感悟是 smithy 讓你在定義 API 時就想好都有什么錯誤,如何組合他們,并且隨著服務的迭代,可以不斷累加錯誤的定義。錯誤處理一直是做任何系統的夢魘,但 smithy 以一種非常簡單的方式把錯誤的定義集中起來,并根據需要組合使用。比如 signin 這個操作,我目前是如下定義的:

errors: [
ValidationException,
UnauthorizedError,
ForbiddenError,
ThrottlingError
]

以后我需要更多錯誤類型時,如 ServerError,只需要相應添加,重新生成代碼,然后在代碼中應用新的錯誤類型即可。

五、使用 Smithy 生成的客戶端代碼

所有代碼生成器,減輕的最大的負擔是客戶端代碼。服務端一般會固定一種語言撰寫,但客戶端語言可以千變萬化,光是主流的客戶端語言就有 typescript / swift / kotlin / dart (flutter),如果考慮服務和服務間的集成,那還有 rust / go / python / csharp / java 等。為服務接口維護這么多客戶端是一種極大的痛苦。我們看看上面的 Echo service,如何使用 Rust 調用:

let config = Config::builder()
.endpoint_url("http://localhost:3000/api")
.bearer_token(Token::new(token, None))
.behavior_version_latest()
.build();
let client = Client::from_conf(config);

let ret = client.echo_message().message("example").send().await?;

是不是似曾相識?對,如果你用 aws Rust SDK 訪問 AWS service,那么你的代碼有幾乎一樣的結構。在這樣的客戶端代碼中,你無需關心 REST API 的細節(比如 message 放在 header 而非 body),就可以輕松調用。

除了各種語言的客戶端代碼外, Smithy 還可以生成 OpenAPI spec。這帶來一個巨大的好處就是可以在服務定義完成之后,就能借助 swagger UI 的力量,有一個可以簡單交互的 API 工具:

六、擴展 Smithy

理論上,任何人都可以通過添加 trait 來擴展 smithy 的能力。但 smithy 整套構建體系基于 kotlin 和 gradle,所以這個「任何人」至少不包括對 java 體系一無所知的我。業界比較知名的 smithy 的擴展是 Tubi 同行 Disney streaming 的 smithy4s,看名字就知道 Disney 為 smithy 提供了 scala 支持。在這個支持中,Disney 順帶對 smithy 做了不少擴展的 trait。

本來 smithy 相關的文檔和示例就很少,關于擴展 smithy,自己撰寫 trait 和代碼生成器的就更是鳳毛麟角。如果你對此有需要,相較于在互聯網上求助,不若直接聯系熟悉的 aws 工程師或者 disney 工程師來得更快。

七、總結

當我耐著極大的性子,一點點看 smithy pokemon rust 的示例代碼,smithy-rs 源碼并和網上其他各種資料相互印證,把各種艱難險阻踩在腳下,成功調用生成好的代碼時,我一下子有了桃花源記中「初極狹,才通人,復行數十步,豁然開朗」的感覺。Smithy 絕對是那種上手很痛苦(因為文檔例子不給力),但用起來很舒服(至少 Rust server SDK 如此)的工具。

這個周末,我用 smithy 寫了個 POC 項目。我把我的心得記錄于此,一來讓知識和技能得到傳播,讓這么優秀的工具不至于無人問津;二來也是把我自己在全速寫代碼,不斷踩坑又埋坑的過程梳理一下。有機會我會在 B 站做個系列視頻,系統地介紹一下用 smithy 的生態構建一個 API 服務。敬請期待!

文章轉自微信公眾號@程序人生

上一篇:

Flask-Limiter:為 API 添加訪問速率限制的 Python 擴展!

下一篇:

如何讓 Python 寫的 API 接口同時支持 Session 和 Token 認證?
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費