feature: init the project with basic skeleton

This commit is contained in:
Tyr Chen 2024-01-20 10:38:35 -08:00
commit 7e0bbad356
No known key found for this signature in database
GPG Key ID: FA69377479FE142A
20 changed files with 4044 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
smithy/build/

2945
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[workspace]
members = [
"smithy/build/smithy/source/rust-client-codegen",
"smithy/build/smithy/source/rust-server-codegen",
"crates/server",
"crates/service",
"crates/client",
]
resolver = "2"
[workspace.dependencies]
anyhow = "1"
axum = { version = "0.6", features = ["headers", "query", "tracing"] }
echo-client-sdk = { path = "smithy/build/smithy/source/rust-client-codegen" }
echo-server-sdk = { path = "smithy/build/smithy/source/rust-server-codegen" }
echo-service = { path = "crates/service" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
tokio = { version = "1", features = [
"rt",
"rt-multi-thread",
"macros",
"time",
] }
tracing = "0.1"
tracing-subscriber = "0.3"

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
ASSETS = assets.tar.gz
validate:
@cd smithy && smithy validate
update-smithy:
@gh release download -R tyrchen/smithy-assets -p '$(ASSETS)'
@rm -rf $HOME/.m2
@tar -xzf $(ASSETS) -C $(HOME) --strip-components=2
@rm $(ASSETS)
build-smithy:
@cd smithy && smithy build
watch:
@watchexec --restart --exts rs -- cargo run --bin echo-server
client:
@cargo run --bin echo-client

11
crates/client/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "echo-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
echo-client-sdk = { workspace = true }
tokio = { workspace = true }

42
crates/client/src/main.rs Normal file
View File

@ -0,0 +1,42 @@
use anyhow::Result;
use echo_client_sdk::{config::Token, Client, Config};
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::builder()
.endpoint_url("http://localhost:3000/api")
.behavior_version_latest()
.build();
let client = Client::from_conf(config);
println!("\n--- Calling echo_message operation without authentication");
let ret = client.echo_message().message("example").send().await;
println!("{:?}", ret);
println!("\n--- Calling signin operation to get a token");
let ret = client
.signin()
.username("test")
.password("abcd12345")
.send()
.await;
println!("{:?}", ret);
let token = ret?.token;
println!("\n-- Calling echo_message operation with authentication");
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?;
println!("{:?}", ret);
Ok(())
}

15
crates/server/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "echo-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
aws-smithy-http-server = { version = "0.60", features = ["request-id"] }
axum = { workspace = true }
echo-service = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

17
crates/server/src/main.rs Normal file
View File

@ -0,0 +1,17 @@
use anyhow::Result;
use echo_service::{get_router, AppConfig};
use std::net::SocketAddr;
use tracing::info;
#[tokio::main]
pub async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let app = get_router(AppConfig::default()).await;
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
info!("Listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}

38
crates/service/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "echo-service"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aws-smithy-http-server = { version = "0.60" }
axum = { workspace = true }
axum-swagger-ui = "0.3"
derive_more = { version = "1.0.0-beta.6", features = ["full"] }
echo-server-sdk = { workspace = true }
jwt-simple = "0.12.1"
pin-project-lite = "0.2.13"
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = "1.0.50"
tokio = { workspace = true }
tower = "0.4.13"
tower-http = { version = "0.4", features = [
"compression-full",
"cors",
"trace",
"fs",
] }
tracing = { workspace = true }
uuid7 = { version = "0.7.2", features = ["serde"] }
[dev-dependencies]
anyhow = { workspace = true }
reqwest = { version = "0.11.22", default-features = false, features = [
"rustls-tls",
"json",
"gzip",
] }

29
crates/service/src/api.rs Normal file
View File

@ -0,0 +1,29 @@
use crate::{forbidden, AppState};
use aws_smithy_http_server::Extension;
use echo_server_sdk::{error, input, output};
use std::sync::Arc;
use tracing::info;
pub async fn echo_message(
input: input::EchoMessageInput,
Extension(_state): Extension<Arc<AppState>>,
) -> Result<output::EchoMessageOutput, error::EchoMessageError> {
info!("echo: {:?}", input);
let message = input.message;
let output = output::EchoMessageOutput { message };
Ok(output)
}
pub async fn signin(
input: input::SigninInput,
Extension(state): Extension<Arc<AppState>>,
) -> Result<output::SigninOutput, error::SigninError> {
info!("signin: {:?}", input);
let signer = &state.signer;
let username = input.username;
if input.password.len() < 8 {
forbidden!("invalid password");
}
let token = signer.sign(username)?;
Ok(output::SigninOutput { token })
}

103
crates/service/src/auth.rs Normal file
View File

@ -0,0 +1,103 @@
use derive_more::Debug;
use echo_server_sdk::error::{ForbiddenError, SigninError};
use jwt_simple::prelude::*;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
pub sk: String,
pub pk: String,
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error("jwt error: {0}")]
JWTError(#[from] jwt_simple::Error),
}
type Result<T> = std::result::Result<T, AuthError>;
const TOKEN_DURATION: u64 = 14;
#[derive(Serialize, Deserialize)]
pub struct CustomClaims {
pub data: String,
}
#[derive(Debug, Clone)]
pub struct AuthSigner {
provider: String,
#[debug(skip)]
key: Ed25519KeyPair,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AuthVerifier {
provider: String,
key: Ed25519PublicKey,
}
impl AuthSigner {
pub fn try_new(provider: impl Into<String>, key: impl AsRef<str>) -> Result<Self> {
let key = Ed25519KeyPair::from_pem(key.as_ref())?;
Ok(Self {
provider: provider.into(),
key,
})
}
pub fn sign(&self, data: String) -> Result<String> {
let claims =
Claims::with_custom_claims(CustomClaims { data }, Duration::from_days(TOKEN_DURATION))
.with_issuer(&self.provider)
.with_subject("auth");
let token = self.key.sign(claims)?;
Ok(token)
}
}
impl AuthVerifier {
pub fn try_new(provider: impl Into<String>, key: impl AsRef<str>) -> Result<Self> {
let key = Ed25519PublicKey::from_pem(key.as_ref())?;
Ok(Self {
provider: provider.into(),
key,
})
}
pub fn verify(&self, token: impl AsRef<str>) -> Result<JWTClaims<CustomClaims>> {
let token = token.as_ref();
let claims = self
.key
.verify_token::<CustomClaims>(token, Some(VerificationOptions::default()))?;
Ok(claims)
}
}
impl Default for AuthConfig {
fn default() -> Self {
let sk = Ed25519KeyPair::generate();
Self {
sk: sk
.to_pem()
.split("-----BEGIN PUBLIC KEY-----")
.next()
.unwrap()
.to_string(),
pk: sk.public_key().to_pem(),
}
}
}
impl From<AuthError> for SigninError {
fn from(e: AuthError) -> Self {
Self::ForbiddenError(ForbiddenError {
message: e.to_string(),
})
}
}
#[cfg(test)]
mod tests {}

View File

@ -0,0 +1,97 @@
#[macro_export]
macro_rules! err {
($ty:ident, $msg:expr) => {
Err(echo_server_sdk::error::ServerError {
code: echo_server_sdk::model::ErrorCode::$ty,
message: $msg.to_string(),
}.into())
};
($ty:ident, $msg:expr, $($param:expr),*) => {
Err(echo_server_sdk::error::ServerError {
code: echo_server_sdk::model::ErrorCode::$ty,
message: format!($msg, $($param),*),
})
};
}
#[macro_export]
macro_rules! bail {
($ty:ident, $msg:expr) => {
return $crate::err!($ty, $msg)
};
($ty:ident, $msg:expr, $($param:expr),*) => {
return $crate::err!($ty, $msg, $($param),*)
};
}
#[macro_export]
macro_rules! try_err {
($expr:expr, $ty:ident) => {
match $expr {
Ok(v) => v,
Err(e) => return $crate::err!($ty, e.to_string()),
}
};
($expr:expr, $ty:ident, $msg:expr) => {
match $expr {
Some(v) => v,
None => return $crate::err!($ty, $msg),
}
};
}
#[macro_export]
macro_rules! not_found {
( $msg:expr) => {
return Err(echo_server_sdk::error::NotFoundError {
message: $msg.to_string(),
}.into())
};
($msg:expr, $($param:expr),*) => {
return Err(echo_server_sdk::error::NotFoundError {
message: format!($msg, $($param),*),
}.into())
};
}
#[macro_export]
macro_rules! conflict {
( $msg:expr) => {
return Err(echo_server_sdk::error::ConflictError {
message: $msg.to_string(),
}.into())
};
($msg:expr, $($param:expr),*) => {
return Err(echo_server_sdk::error::ConflictError {
message: format!($msg, $($param),*),
}.into())
};
}
#[macro_export]
macro_rules! forbidden {
( $msg:expr) => {
return Err(echo_server_sdk::error::ForbiddenError {
message: $msg.to_string(),
}.into())
};
($msg:expr, $($param:expr),*) => {
return Err(echo_server_sdk::error::ForbiddenError {
message: format!($msg, $($param),*),
}.into())
};
}
#[macro_export]
macro_rules! unauthorized {
( $msg:expr) => {
return Err(echo_server_sdk::error::UnauthorizedError {
message: $msg.to_string(),
}.into())
};
($msg:expr, $($param:expr),*) => {
return Err(echo_server_sdk::error::UnauthorizedError {
message: format!($msg, $($param),*),
}.into())
};
}

109
crates/service/src/lib.rs Normal file
View File

@ -0,0 +1,109 @@
mod api;
mod auth;
mod error;
mod middleware;
use auth::{AuthConfig, AuthSigner, AuthVerifier};
use aws_smithy_http_server::{
plugin::IdentityPlugin, request::request_id::ServerRequestIdProviderLayer, AddExtensionLayer,
};
use axum::{
http::{HeaderName, Method},
response::Html,
routing::get,
Router,
};
use axum_swagger_ui::swagger_ui;
use derive_more::Debug;
use echo_server_sdk::{EchoService, EchoServiceConfig};
use middleware::{BearerTokenProviderLayer, ServerTimingLayer};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
#[derive(Debug)]
pub struct AppState {
#[allow(dead_code)]
config: AppConfig,
pub(crate) verifier: AuthVerifier,
#[allow(dead_code)]
pub(crate) signer: AuthSigner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub server_name: String,
pub port: u16,
pub auth: AuthConfig,
}
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));
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()))
.layer(BearerTokenProviderLayer::new())
.layer(ServerRequestIdProviderLayer::new_with_response_header(
HeaderName::from_static("x-request-id"),
))
.build();
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/build/smithy/source/openapi/EchoService.openapi.json");
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
Method::HEAD,
Method::OPTIONS,
])
.allow_headers(Any)
// allow requests from any origin
.allow_origin(Any)
.allow_private_network(true);
Router::new()
.route("/swagger", get(|| async { Html(swagger_ui(doc_url)) }))
.route(doc_url, get(move || async move { doc }))
.nest_service("/api/", api)
.layer(ServerTimingLayer::new(name))
.layer(cors)
.with_state(state)
}
impl Default for AppConfig {
fn default() -> Self {
Self {
server_name: "echo-service".to_string(),
port: 3000,
auth: AuthConfig::default(),
}
}
}
impl AppState {
pub fn new(config: AppConfig) -> Self {
let signer = AuthSigner::try_new(&config.server_name, &config.auth.sk).unwrap();
let verifier = AuthVerifier::try_new(&config.server_name, &config.auth.pk).unwrap();
Self {
config,
verifier,
signer,
}
}
}

View File

@ -0,0 +1,120 @@
use crate::AppState;
use aws_smithy_http_server::body::BoxBody;
use axum::http::{Request, Response, StatusCode};
use echo_server_sdk::server::response::IntoResponse;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use thiserror::Error;
use tower::{Layer, Service};
/// The server request ID has not been added to the [`Request`](http::Request) or has been previously removed.
#[non_exhaustive]
#[derive(Debug, Error)]
#[error("the `Authorization` header is not present in the `http::Request`")]
pub enum BearTokenError {
#[error("the `Authorization` header is not present in the `http::Request`")]
Missing,
#[error("the `Authorization` header is not valid")]
Invalid,
}
#[derive(Clone)]
pub struct BearerTokenProvider<S> {
inner: S,
}
/// A layer that provides services with bearer token
#[derive(Debug)]
#[non_exhaustive]
pub struct BearerTokenProviderLayer {}
impl BearerTokenProviderLayer {
pub fn new() -> Self {
Self {}
}
}
impl<S> Layer<S> for BearerTokenProviderLayer {
type Service = BearerTokenProvider<S>;
fn layer(&self, inner: S) -> Self::Service {
BearerTokenProvider { inner }
}
}
impl<Body, S> Service<Request<Body>> for BearerTokenProvider<S>
where
S: Service<Request<Body>, Response = Response<BoxBody>>,
S::Future: std::marker::Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Send + Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
match self.process(req) {
Ok(req) => {
let fut = self.inner.call(req);
Box::pin(async move {
let res = fut.await?;
Ok(res)
})
}
Err(e) => {
let res = <BearTokenError as IntoResponse<()>>::into_response(e);
Box::pin(async move { Ok(res) })
}
}
}
}
impl<S> BearerTokenProvider<S> {
fn process<Body>(&self, mut req: Request<Body>) -> Result<Request<Body>, BearTokenError> {
// TODO: how to read the smithy auth trait to see if the auth is required?
let path = req.uri().path();
if path.starts_with("/signin") || path.starts_with("/echo") {
return Ok(req);
}
let v = req
.headers_mut()
.remove("Authorization")
.ok_or(BearTokenError::Missing)?;
let v = v.to_str().map_err(|_| BearTokenError::Invalid)?;
let token = v.trim_start_matches("Bearer ").to_string();
let verifier = &req.extensions().get::<Arc<AppState>>().unwrap().verifier;
match verifier.verify(token) {
Ok(claim) => {
req.extensions_mut().insert(claim);
Ok(req)
}
Err(_) => Err(BearTokenError::Invalid),
}
}
}
impl<Protocol> IntoResponse<Protocol> for BearTokenError {
fn into_response(self) -> Response<BoxBody> {
match self {
BearTokenError::Missing => Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(BoxBody::default())
.unwrap(),
BearTokenError::Invalid => Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(BoxBody::default())
.unwrap(),
}
}
}
#[cfg(test)]
mod tests {}

View File

@ -0,0 +1,5 @@
mod bearer_auth;
mod server_timing;
pub use bearer_auth::BearerTokenProviderLayer;
pub use server_timing::ServerTimingLayer;

View File

@ -0,0 +1,243 @@
// code from: https://github.com/JensWalter/axum-server-timing/blob/main/src/lib.rs
use std::{
future::Future,
pin::Pin,
task::{ready, Context, Poll},
time::Instant,
};
use axum::http::{HeaderValue, Request, Response};
use pin_project_lite::pin_project;
use tower::{Layer, Service};
#[derive(Debug, Clone)]
pub struct ServerTimingLayer<'a> {
app: &'a str,
description: Option<&'a str>,
}
impl<'a> ServerTimingLayer<'a> {
pub fn new(app: &'a str) -> Self {
ServerTimingLayer {
app,
description: None,
}
}
#[allow(dead_code)]
pub fn with_description(&mut self, description: &'a str) -> Self {
let mut new_self = self.clone();
new_self.description = Some(description);
new_self
}
}
impl<'a, S> Layer<S> for ServerTimingLayer<'a> {
type Service = ServerTimingService<'a, S>;
fn layer(&self, service: S) -> Self::Service {
ServerTimingService {
service,
app: self.app,
description: self.description,
}
}
}
#[derive(Clone)]
pub struct ServerTimingService<'a, S> {
service: S,
app: &'a str,
description: Option<&'a str>,
}
impl<'a, S, ReqBody, ResBody> Service<Request<ReqBody>> for ServerTimingService<'a, S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Default,
{
type Response = S::Response;
type Error = S::Error;
type Future = ResponseFuture<'a, S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let (parts, body) = req.into_parts();
let req = Request::from_parts(parts, body);
ResponseFuture {
inner: self.service.call(req),
request_time: Instant::now(),
app: self.app,
description: self.description,
}
}
}
pin_project! {
pub struct ResponseFuture<'a, F> {
#[pin]
inner: F,
request_time: Instant,
app: &'a str,
description: Option<&'a str>,
}
}
impl<F, B, E> Future for ResponseFuture<'_, F>
where
F: Future<Output = Result<Response<B>, E>>,
B: Default,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let time = self.request_time;
let app = self.app;
let description = self.description;
let mut response: Response<B> = ready!(self.project().inner.poll(cx))?;
let hdr = response.headers_mut();
let x = time.elapsed().as_millis();
let header_value = match description {
Some(val) => format!("{app};desc=\"{val}\";dur={x}"),
None => format!("{app};dur={x}"),
};
match hdr.try_entry("Server-Timing") {
Ok(entry) => {
match entry {
axum::http::header::Entry::Occupied(mut val) => {
//has val
let old_val = val.get();
let new_val = format!("{header_value}, {}", old_val.to_str().unwrap());
val.insert(HeaderValue::from_str(&new_val).unwrap());
}
axum::http::header::Entry::Vacant(val) => {
val.insert(HeaderValue::from_str(&header_value).unwrap());
}
}
}
Err(_) => {
hdr.append(
"Server-Timing",
HeaderValue::from_str(&header_value).unwrap(),
);
}
}
Poll::Ready(Ok(response))
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{
http::{HeaderMap, HeaderValue},
routing::get,
Router,
};
use std::{net::SocketAddr, time::Duration};
#[test]
fn service_name() {
let name = "svc1";
let obj = ServerTimingLayer::new(name);
assert_eq!(obj.app, name);
}
#[test]
fn service_desc() {
let name = "svc1";
let desc = "desc1";
let obj = ServerTimingLayer::new(name).with_description(desc);
assert_eq!(obj.app, name);
assert_eq!(obj.description, Some(desc));
}
#[tokio::test]
async fn header_exists_on_response() {
let name = "svc1";
let app = Router::new()
.route("/", get(|| async move { "" }))
.layer(ServerTimingLayer::new(name));
tokio::spawn(async {
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap()
});
//test request
let resp = reqwest::get("http://localhost:3001/").await.unwrap();
let hdr = resp.headers().get("server-timing");
assert!(hdr.is_some());
}
#[tokio::test]
async fn header_value() {
let name = "svc1";
let app = Router::new()
.route(
"/",
get(|| async move {
tokio::time::sleep(Duration::from_millis(100)).await;
""
}),
)
.layer(ServerTimingLayer::new(name));
tokio::spawn(async {
let addr = SocketAddr::from(([127, 0, 0, 1], 3002));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap()
});
//test request
let resp = reqwest::get("http://localhost:3002/").await.unwrap();
if let Some(hdr) = resp.headers().get("server-timing") {
let val = &hdr.to_str().unwrap()[9..];
let val_num: f32 = val.parse().unwrap();
assert!(val_num >= 100_f32);
} else {
panic!("no header found");
}
}
#[tokio::test]
async fn support_existing_header() {
let name = "svc1";
let app = Router::new()
.route(
"/",
get(|| async move {
tokio::time::sleep(Duration::from_millis(100)).await;
let mut hdr = HeaderMap::new();
hdr.insert("server-timing", HeaderValue::from_static("inner;dur=23"));
(hdr, "")
}),
)
.layer(ServerTimingLayer::new(name));
tokio::spawn(async {
let addr = SocketAddr::from(([127, 0, 0, 1], 3003));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap()
});
//test request
let resp = reqwest::get("http://localhost:3003/").await.unwrap();
let hdr = resp.headers().get("server-timing").unwrap();
let hdr_str = hdr.to_str().unwrap();
assert!(hdr_str.contains("svc1"));
assert!(hdr_str.contains("inner"));
println!("{hdr:?}");
}
}

61
smithy/model/error.smithy Normal file
View File

@ -0,0 +1,61 @@
$version: "2.0"
namespace com.example
/// Throttling error.
@error("client")
@retryable
@httpError(429)
structure ThrottlingError {
@required
message: String
}
/// Not found error.
@error("client")
@httpError(404)
structure NotFoundError {
@required
message: String
}
/// Conflict error.
@error("client")
@httpError(409)
structure ConflictError {
@required
message: String
}
/// Unauthorized error.
@error("client")
@httpError(401)
structure UnauthorizedError {
@required
message: String
}
/// Forbidden error.
@error("client")
@httpError(403)
structure ForbiddenError {
@required
message: String
}
/// Server error.
@error("server")
@httpError(500)
structure ServerError {
@required
code: ErrorCode
@required
message: String
}
enum ErrorCode {
INFER = "infer",
NETWORK = "network",
DATABASE = "database",
UNKNOWN = "unknown",
}

50
smithy/model/main.smithy Normal file
View File

@ -0,0 +1,50 @@
$version: "2.0"
namespace com.example
use aws.protocols#restJson1
use smithy.framework#ValidationException
use aws.api#service
/// Echoes input
@service(sdkId: "echo")
@restJson1
@httpBearerAuth
service EchoService {
version: "2023-12-03"
operations: [EchoMessage, Signin]
}
@http(uri: "/echo", method: "POST")
@auth([])
operation EchoMessage {
input := {
@required
@httpHeader("x-echo-message")
message: String
}
output := {
@required
message: String
}
errors: [ValidationException]
}
/// Signin to get a token.
@http(uri: "/signin", method: "POST")
@auth([])
operation Signin {
input := {
@required
username: String
@required
password: String
}
output := {
@required
token: String
}
errors: [ValidationException, UnauthorizedError, ForbiddenError, ThrottlingError]
}

88
smithy/smithy-build.json Normal file
View File

@ -0,0 +1,88 @@
{
"version": "1.0",
"sources": [
"model"
],
"maven": {
"dependencies": [
"software.amazon.smithy:smithy-model:[1.41.0,2.0)",
"software.amazon.smithy:smithy-validation-model:[1.41.0,2.0)",
"software.amazon.smithy:smithy-aws-traits:[1.41.0,2.0)",
"software.amazon.smithy:smithy-openapi:1.41.1",
"software.amazon.smithy.python:smithy-python-codegen:0.1.0",
"software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0",
"software.amazon.smithy.typescript:smithy-aws-typescript-codegen:0.19.0",
"software.amazon.smithy:smithy-swift-codegen:0.1.0",
"software.amazon.smithy.rust.codegen:codegen-client:0.1.0",
"software.amazon.smithy.rust.codegen.server.smithy:codegen-server:0.1.0"
]
},
"plugins": {
"openapi": {
"service": "com.example#EchoService",
"protocol": "aws.protocols#restJson1",
"version": "3.1.0",
"tags": true,
"useIntegerType": true,
"jsonAdd": {
"/servers": [
{
"url": "http://localhost:3000/api",
"description": "Local server"
}
]
}
},
"rust-server-codegen": {
"service": "com.example#EchoService",
"module": "echo-server-sdk",
"moduleDescription": "Rust server SDK for echo server",
"moduleVersion": "0.1.0",
"moduleAuthors": [
"Tyr Chen <tchen@abc.xyz>"
],
"runtimeConfig": {
"versions": {
"DEFAULT": "1.0.1",
"aws-smithy-http": "0.60.0",
"aws-smithy-json": "0.60.0",
"aws-smithy-http-server": "0.60.0"
}
}
},
"python-client-codegen": {
"service": "com.example#EchoService",
"module": "echo",
"moduleVersion": "0.0.1"
},
"typescript-client-codegen": {
"package": "echo",
"packageVersion": "0.0.1"
},
"swift-codegen": {
"service": "com.example#EchoService",
"module": "echo",
"moduleVersion": "0.0.1",
"homepage": "https://github.com/tyrchen",
"gitRepo": "https://github.com/tyrchen",
"author": "Tyr Chen <tchen@abc.xyz>",
"swiftVersion": "5.0"
},
"rust-client-codegen": {
"service": "com.example#EchoService",
"module": "echo-client-sdk",
"moduleDescription": "Rust client SDK for echo service",
"moduleVersion": "0.1.0",
"moduleAuthors": [
"Tyr Chen <tchen@abc.xyz>"
],
"runtimeConfig": {
"versions": {
"DEFAULT": "1.0.1",
"aws-smithy-http": "0.60.0",
"aws-smithy-json": "0.60.0"
}
}
}
}
}

24
test.http Normal file
View File

@ -0,0 +1,24 @@
# Echo REST API
### signin
# User should use existing anonymous token to signin. Here just for demo purpose.
# @name login
POST http://localhost:3000/api/signin
Content-Type: application/json
{
"username": "admin",
"password": "abcd1234"
}
### echo
# retrieve token as echo service need it.
@token = {{ login.response.body.token }}
POST http://localhost:3000/api/echo
Authorization: Bearer {{ token }}
X-Echo-Message: hello world!