mirror of https://github.com/smithy-lang/smithy-rs
Add initial implementation of a Server example (#1222)
We are adding this example service in the aws-smithy-http-server folder to showcase the SSDK and to be able soon to run benchmarks against this example. This change includes a new model for the Pokémon Service and all the necessary infrastructure to code generate the client and server SKDs and make them available to the runtime implementation. A basic README has also been added with instructions on how to build, run and test the service. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
This commit is contained in:
parent
d4c23e3fd2
commit
bbe82cd283
|
@ -37,17 +37,20 @@ val allCodegenTests = listOf(
|
||||||
CodegenTest("aws.protocoltests.restjson#RestJson", "rest_json"),
|
CodegenTest("aws.protocoltests.restjson#RestJson", "rest_json"),
|
||||||
CodegenTest("aws.protocoltests.restjson.validation#RestJsonValidation", "rest_json_validation"),
|
CodegenTest("aws.protocoltests.restjson.validation#RestJsonValidation", "rest_json_validation"),
|
||||||
CodegenTest("com.amazonaws.ebs#Ebs", "ebs"),
|
CodegenTest("com.amazonaws.ebs#Ebs", "ebs"),
|
||||||
CodegenTest("com.amazonaws.s3#AmazonS3", "s3")
|
CodegenTest("com.amazonaws.s3#AmazonS3", "s3"),
|
||||||
|
CodegenTest("com.aws.example#PokemonService", "pokemon_service_sdk")
|
||||||
)
|
)
|
||||||
|
|
||||||
task("generateSmithyBuild") {
|
task("generateSmithyBuild") {
|
||||||
description = "generate smithy-build.json"
|
description = "generate smithy-build.json"
|
||||||
doFirst {
|
doFirst {
|
||||||
projectDir.resolve("smithy-build.json")
|
projectDir.resolve("smithy-build.json")
|
||||||
.writeText(generateSmithyBuild(
|
.writeText(
|
||||||
rootProject.projectDir.absolutePath,
|
generateSmithyBuild(
|
||||||
pluginName,
|
rootProject.projectDir.absolutePath,
|
||||||
codegenTests(properties, allCodegenTests))
|
pluginName,
|
||||||
|
codegenTests(properties, allCodegenTests)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
$version: "1.0"
|
||||||
|
|
||||||
|
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: "2021-12-01",
|
||||||
|
resources: [PokemonSpecies],
|
||||||
|
operations: [GetServerStatistics],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Pokémon species forms the basis for at least one Pokémon.
|
||||||
|
@title("Pokémon Species")
|
||||||
|
resource PokemonSpecies {
|
||||||
|
identifiers: {
|
||||||
|
name: String
|
||||||
|
},
|
||||||
|
read: GetPokemonSpecies,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve information about a Pokémon species.
|
||||||
|
@readonly
|
||||||
|
@http(uri: "/pokemon-species/{name}", method: "GET")
|
||||||
|
operation GetPokemonSpecies {
|
||||||
|
input: GetPokemonSpeciesInput,
|
||||||
|
output: GetPokemonSpeciesOutput,
|
||||||
|
errors: [ResourceNotFoundException],
|
||||||
|
}
|
||||||
|
|
||||||
|
@input
|
||||||
|
structure GetPokemonSpeciesInput {
|
||||||
|
@required
|
||||||
|
@httpLabel
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@output
|
||||||
|
structure GetPokemonSpeciesOutput {
|
||||||
|
/// The name for this resource.
|
||||||
|
@required
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// A list of flavor text entries for this Pokémon species.
|
||||||
|
@required
|
||||||
|
flavorTextEntries: FlavorTextEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve HTTP server statistiscs, such as calls count.
|
||||||
|
@readonly
|
||||||
|
@http(uri: "/stats", method: "GET")
|
||||||
|
operation GetServerStatistics {
|
||||||
|
input: GetServerStatisticsInput,
|
||||||
|
output: GetServerStatisticsOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
@input
|
||||||
|
structure GetServerStatisticsInput { }
|
||||||
|
|
||||||
|
@output
|
||||||
|
structure GetServerStatisticsOutput {
|
||||||
|
/// The number of calls executed by the server.
|
||||||
|
@required
|
||||||
|
calls_count: Long,
|
||||||
|
}
|
||||||
|
|
||||||
|
list FlavorTextEntries {
|
||||||
|
member: FlavorText
|
||||||
|
}
|
||||||
|
|
||||||
|
structure FlavorText {
|
||||||
|
/// The localized flavor text for an API resource in a specific language.
|
||||||
|
@required
|
||||||
|
flavorText: String,
|
||||||
|
|
||||||
|
/// The language this name is in.
|
||||||
|
@required
|
||||||
|
language: Language,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported languages for FlavorText entries.
|
||||||
|
@enum([
|
||||||
|
{
|
||||||
|
name: "ENGLISH",
|
||||||
|
value: "en",
|
||||||
|
documentation: "American English.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SPANISH",
|
||||||
|
value: "es",
|
||||||
|
documentation: "Español.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ITALIAN",
|
||||||
|
value: "it",
|
||||||
|
documentation: "Italiano.",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
string Language
|
||||||
|
|
||||||
|
@error("client")
|
||||||
|
@httpError(404)
|
||||||
|
structure ResourceNotFoundException {
|
||||||
|
@required
|
||||||
|
message: String,
|
||||||
|
}
|
|
@ -83,17 +83,20 @@ val allCodegenTests = listOf(
|
||||||
"""
|
"""
|
||||||
, "codegen": { "renameErrors": false }
|
, "codegen": { "renameErrors": false }
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
),
|
||||||
|
CodegenTest("com.aws.example#PokemonService", "pokemon_service_client")
|
||||||
)
|
)
|
||||||
|
|
||||||
task("generateSmithyBuild") {
|
task("generateSmithyBuild") {
|
||||||
description = "generate smithy-build.json"
|
description = "generate smithy-build.json"
|
||||||
doFirst {
|
doFirst {
|
||||||
projectDir.resolve("smithy-build.json")
|
projectDir.resolve("smithy-build.json")
|
||||||
.writeText(generateSmithyBuild(
|
.writeText(
|
||||||
rootProject.projectDir.absolutePath,
|
generateSmithyBuild(
|
||||||
pluginName,
|
rootProject.projectDir.absolutePath,
|
||||||
codegenTests(properties, allCodegenTests))
|
pluginName,
|
||||||
|
codegenTests(properties, allCodegenTests)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../codegen-server-test/model/pokemon.smithy
|
|
@ -0,0 +1,2 @@
|
||||||
|
pokemon_service_client/
|
||||||
|
pokemon_service_sdk/
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Without this configuration, the workspace will be read from `rust-runtime`, causing the build to fail.
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"pokemon_service",
|
||||||
|
"pokemon_service_sdk",
|
||||||
|
"pokemon_service_client"
|
||||||
|
]
|
|
@ -0,0 +1,27 @@
|
||||||
|
SRC_DIR := $(shell git rev-parse --show-toplevel)
|
||||||
|
CUR_DIR := $(shell pwd)
|
||||||
|
GRADLE := $(SRC_DIR)/gradlew
|
||||||
|
SERVER_SDK_DST := $(CUR_DIR)/pokemon_service_sdk
|
||||||
|
CLIENT_SDK_DST := $(CUR_DIR)/pokemon_service_client
|
||||||
|
SERVER_SDK_SRC := $(SRC_DIR)/codegen-server-test/build/smithyprojections/codegen-server-test/pokemon_service_sdk/rust-server-codegen
|
||||||
|
CLIENT_SDK_SRC := $(SRC_DIR)/codegen-test/build/smithyprojections/codegen-test/pokemon_service_client/rust-codegen
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
codegen:
|
||||||
|
$(GRADLE) --project-dir $(SRC_DIR) :codegen-test:assemble
|
||||||
|
$(GRADLE) --project-dir $(SRC_DIR) :codegen-server-test:assemble
|
||||||
|
mkdir -p $(SERVER_SDK_DST) $(CLIENT_SDK_DST)
|
||||||
|
cp -av $(SERVER_SDK_SRC)/* $(SERVER_SDK_DST)/
|
||||||
|
cp -av $(CLIENT_SDK_SRC)/* $(CLIENT_SDK_DST)/
|
||||||
|
|
||||||
|
build: codegen
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cargo clean || echo "Unable to run cargo clean"
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
rm -rf $(SERVER_SDK_DST) $(CLIENT_SDK_DST) Cargo.lock
|
||||||
|
|
||||||
|
.PHONY: all
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Smithy Rust Server SDK example
|
||||||
|
|
||||||
|
This folder contains an example service called Pokémon Service used to showcase
|
||||||
|
the service framework capabilities and to run benchmarks.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Since this example requires both the server and client SDK to be code-generated
|
||||||
|
from their [model](/codegen-server-test/model/pokemon.smithy), a Makefile is
|
||||||
|
provided to build and run the service. Just run `make` to prepare the first
|
||||||
|
build.
|
||||||
|
|
||||||
|
Once the example has been built successfully the first time, idiomatic `cargo`
|
||||||
|
can be used directly.
|
||||||
|
|
||||||
|
`make dist-clean` can be used for a complete cleanup of all artefacts.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
`cargo run` can be used to start the Pokémon service on
|
||||||
|
`http://localhost:13734`.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
`cargo test` can be used to spawn the service and run some simple integration
|
||||||
|
tests against it.
|
||||||
|
|
||||||
|
More info can be found in the `tests` folder of `pokemon_service` package.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
TBD.
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "pokemon_service"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hyper = {version = "0.14", features = ["server"] }
|
||||||
|
tokio = "1"
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.2", features = ["trace"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# Local paths
|
||||||
|
aws-smithy-http-server = { path = "../../" }
|
||||||
|
pokemon_service_sdk = { path = "../pokemon_service_sdk/" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
assert_cmd = "2.0"
|
||||||
|
|
||||||
|
# Local paths
|
||||||
|
aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] }
|
||||||
|
pokemon_service_client = { path = "../pokemon_service_client/" }
|
|
@ -0,0 +1,212 @@
|
||||||
|
/*
|
||||||
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
//! Pokémon Service
|
||||||
|
//!
|
||||||
|
//! This crate implements the Pokémon Service.
|
||||||
|
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
convert::TryInto,
|
||||||
|
sync::{atomic::AtomicU64, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
|
use aws_smithy_http_server::Extension;
|
||||||
|
use pokemon_service_sdk::{error, input, model, output};
|
||||||
|
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||||
|
|
||||||
|
const PIKACHU_ENGLISH_FLAVOR_TEXT: &str =
|
||||||
|
"When several of these Pokémon gather, their electricity could build and cause lightning storms.";
|
||||||
|
const PIKACHU_SPANISH_FLAVOR_TEXT: &str =
|
||||||
|
"Cuando varios de estos Pokémon se juntan, su energía puede causar fuertes tormentas.";
|
||||||
|
const PIKACHU_ITALIAN_FLAVOR_TEXT: &str =
|
||||||
|
"Quando vari Pokémon di questo tipo si radunano, la loro energia può causare forti tempeste.";
|
||||||
|
|
||||||
|
/// Setup `tracing::subscriber` to read the log level from RUST_LOG environment variable.
|
||||||
|
pub fn setup_tracing() {
|
||||||
|
let format = tracing_subscriber::fmt::layer()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_level(true);
|
||||||
|
let filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("info"))
|
||||||
|
.unwrap();
|
||||||
|
tracing_subscriber::registry().with(format).with(filter).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure holding the translations for a Pokémon description.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct PokemonTranslations {
|
||||||
|
en: String,
|
||||||
|
es: String,
|
||||||
|
it: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PokémonService shared state.
|
||||||
|
///
|
||||||
|
/// Some applications may want to manage state between handlers. Imagine having a database connection pool
|
||||||
|
/// that can be shared between different handlers and operation implementations.
|
||||||
|
/// State management can be expressed in a struct where the attributes hold the shared entities.
|
||||||
|
///
|
||||||
|
/// **NOTE: It is up to the implementation of the state structure to handle concurrency by protecting**
|
||||||
|
/// **its attributes using synchronization mechanisms.**
|
||||||
|
///
|
||||||
|
/// The framework stores the `Arc<T>` inside an [`http::Extensions`] and conveniently passes it to
|
||||||
|
/// the operation's implementation, making it able to handle operations with two different async signatures:
|
||||||
|
/// * `FnOnce(InputType) -> Future<OutputType>`
|
||||||
|
/// * `FnOnce(InputType, Extension<Arc<T>>) -> Future<OutputType>`
|
||||||
|
///
|
||||||
|
/// Wrapping the service with a [`tower::Layer`] will allow to have operations' signatures with and without shared state:
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
/// use aws_smithy_http_server::{AddExtensionLayer, Extension, Router};
|
||||||
|
/// use tower::ServiceBuilder;
|
||||||
|
/// use tokio::sync::RwLock;
|
||||||
|
///
|
||||||
|
/// // Shared state,
|
||||||
|
/// #[derive(Debug, State)]
|
||||||
|
/// pub struct State {
|
||||||
|
/// pub count: RwLock<u64>
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Operation implementation with shared state.
|
||||||
|
/// async fn operation_with_state(input: Input, state: Extension<Arc<State>>) -> Output {
|
||||||
|
/// let mut count = state.0.write().await;
|
||||||
|
/// *count += 1;
|
||||||
|
/// Ok(Output::new())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Operation implementation without shared state.
|
||||||
|
/// async fn operation_without_state(input: Input) -> Output {
|
||||||
|
/// Ok(Output::new())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let app: Router = OperationRegistryBuilder::default()
|
||||||
|
/// .operation_with_state(operation_with_state)
|
||||||
|
/// .operation_without_state(operation_without_state)
|
||||||
|
/// .build()
|
||||||
|
/// .unwrap()
|
||||||
|
/// .into();
|
||||||
|
/// let shared_state = Arc::new(State::default());
|
||||||
|
/// let app = app.layer(ServiceBuilder::new().layer(AddExtensionLayer::new(shared_state)));
|
||||||
|
/// let server = hyper::Server::bind(&"0.0.0.0:13734".parse().unwrap()).serve(app.into_make_service());
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Without the middleware layer, the framework will require operations' signatures without
|
||||||
|
/// the shared state.
|
||||||
|
///
|
||||||
|
/// [`middleware`]: [`aws_smithy_http_server::AddExtensionLayer`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct State {
|
||||||
|
pokemons_translations: HashMap<String, PokemonTranslations>,
|
||||||
|
call_count: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut pokemons_translations = HashMap::new();
|
||||||
|
pokemons_translations.insert(
|
||||||
|
String::from("pikachu"),
|
||||||
|
PokemonTranslations {
|
||||||
|
en: String::from(PIKACHU_ENGLISH_FLAVOR_TEXT),
|
||||||
|
es: String::from(PIKACHU_SPANISH_FLAVOR_TEXT),
|
||||||
|
it: String::from(PIKACHU_ITALIAN_FLAVOR_TEXT),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
pokemons_translations,
|
||||||
|
call_count: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves information about a Pokémon 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);
|
||||||
|
// We only support retrieving information about Pikachu.
|
||||||
|
let pokemon = state.0.pokemons_translations.get(&input.name);
|
||||||
|
match pokemon.as_ref() {
|
||||||
|
Some(pokemon) => {
|
||||||
|
tracing::debug!("Requested Pokémon is {}", input.name);
|
||||||
|
let flavor_text_entries = vec![
|
||||||
|
model::FlavorText {
|
||||||
|
flavor_text: pokemon.en.to_owned(),
|
||||||
|
language: model::Language::English,
|
||||||
|
},
|
||||||
|
model::FlavorText {
|
||||||
|
flavor_text: pokemon.es.to_owned(),
|
||||||
|
language: model::Language::Spanish,
|
||||||
|
},
|
||||||
|
model::FlavorText {
|
||||||
|
flavor_text: pokemon.it.to_owned(),
|
||||||
|
language: model::Language::Italian,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let output = output::GetPokemonSpeciesOutput {
|
||||||
|
name: String::from("pikachu"),
|
||||||
|
flavor_text_entries,
|
||||||
|
};
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::error!("Requested Pokémon {} not available", input.name);
|
||||||
|
Err(error::GetPokemonSpeciesError::ResourceNotFoundException(
|
||||||
|
error::ResourceNotFoundException {
|
||||||
|
message: String::from("Requested Pokémon not available"),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates and reports metrics about this server instance.
|
||||||
|
pub async fn get_server_statistics(
|
||||||
|
_input: input::GetServerStatisticsInput,
|
||||||
|
state: Extension<Arc<State>>,
|
||||||
|
) -> output::GetServerStatisticsOutput {
|
||||||
|
// Read the current calls count.
|
||||||
|
let counter = state.0.call_count.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
let calls_count = counter
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Unable to convert u64 to i64: {}", e);
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
tracing::debug!("This instance served {} requests", counter);
|
||||||
|
output::GetServerStatisticsOutput { calls_count }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_pokemon_species_pikachu_spanish_flavor_text() {
|
||||||
|
let input = input::GetPokemonSpeciesInput {
|
||||||
|
name: String::from("pikachu"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = Arc::new(State::default());
|
||||||
|
|
||||||
|
let actual_spanish_flavor_text = get_pokemon_species(input, Extension(state.clone()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.flavor_text_entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|flavor_text| flavor_text.language == model::Language::Spanish)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(PIKACHU_SPANISH_FLAVOR_TEXT, actual_spanish_flavor_text.flavor_text());
|
||||||
|
|
||||||
|
let input = input::GetServerStatisticsInput {};
|
||||||
|
let stats = get_server_statistics(input, Extension(state.clone())).await;
|
||||||
|
assert_eq!(1, stats.calls_count);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This program is exported as a binary named `pokemon_service`.
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use aws_smithy_http_server::{AddExtensionLayer, Router};
|
||||||
|
use pokemon_service::{get_pokemon_species, get_server_statistics, setup_tracing, State};
|
||||||
|
use pokemon_service_sdk::operation_registry::OperationRegistryBuilder;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
setup_tracing();
|
||||||
|
let app: Router = OperationRegistryBuilder::default()
|
||||||
|
// Build a registry containing implementations to all the operations in the service. These
|
||||||
|
// are async functions or async closures that take as input the operation's input and
|
||||||
|
// return the operation's output.
|
||||||
|
.get_pokemon_species(get_pokemon_species)
|
||||||
|
.get_server_statistics(get_server_statistics)
|
||||||
|
.build()
|
||||||
|
.expect("Unable to build operation registry")
|
||||||
|
// Convert it into a router that will route requests to the matching operation
|
||||||
|
// implementation.
|
||||||
|
.into();
|
||||||
|
|
||||||
|
// Setup shared state and middlewares.
|
||||||
|
let shared_state = Arc::new(State::default());
|
||||||
|
let app = app.layer(
|
||||||
|
ServiceBuilder::new()
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(AddExtensionLayer::new(shared_state)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start the [`hyper::Server`].
|
||||||
|
let server = hyper::Server::bind(&"0.0.0.0:13734".parse().unwrap()).serve(app.into_make_service());
|
||||||
|
|
||||||
|
// Run forever-ish...
|
||||||
|
if let Err(err) = server.await {
|
||||||
|
eprintln!("server error: {}", err);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use assert_cmd::prelude::*;
|
||||||
|
use pokemon_service_client::{Builder, Client, Config};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
pub(crate) struct PokemonService {
|
||||||
|
child_process: std::process::Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PokemonService {
|
||||||
|
pub(crate) fn run() -> Self {
|
||||||
|
let process = Command::cargo_bin("pokemon_service").unwrap().spawn().unwrap();
|
||||||
|
|
||||||
|
Self { child_process: process }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PokemonService {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.child_process
|
||||||
|
.kill()
|
||||||
|
.expect("failed to kill Pokémon Service program")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client() -> Client<
|
||||||
|
aws_smithy_client::erase::DynConnector,
|
||||||
|
aws_smithy_client::erase::DynMiddleware<aws_smithy_client::erase::DynConnector>,
|
||||||
|
> {
|
||||||
|
let raw_client = Builder::new()
|
||||||
|
.rustls()
|
||||||
|
.middleware_fn(|mut req| {
|
||||||
|
let http_req = req.http_mut();
|
||||||
|
let uri = format!("http://localhost:13734{}", http_req.uri().path());
|
||||||
|
*http_req.uri_mut() = uri.parse().unwrap();
|
||||||
|
req
|
||||||
|
})
|
||||||
|
.build_dyn();
|
||||||
|
let config = Config::builder().build();
|
||||||
|
Client::with_config(raw_client, config)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Files here are for running integration tests.
|
||||||
|
// These tests only have access to your crate's public API.
|
||||||
|
// See: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::helpers::{client, PokemonService};
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn simple_integration_test() {
|
||||||
|
let _program = PokemonService::run();
|
||||||
|
// Give PokemonSérvice some time to start up.
|
||||||
|
time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
|
||||||
|
assert_eq!(0, service_statistics_out.calls_count.unwrap());
|
||||||
|
|
||||||
|
let pokemon_species_output = client().get_pokemon_species().name("pikachu").send().await.unwrap();
|
||||||
|
assert_eq!("pikachu", pokemon_species_output.name().unwrap());
|
||||||
|
|
||||||
|
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
|
||||||
|
assert_eq!(1, service_statistics_out.calls_count.unwrap());
|
||||||
|
|
||||||
|
let pokemon_species_error = client()
|
||||||
|
.get_pokemon_species()
|
||||||
|
.name("some_pokémon")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
r#"ResourceNotFoundError [ResourceNotFoundException]: Requested Pokémon not available"#,
|
||||||
|
pokemon_species_error.to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
|
||||||
|
assert_eq!(2, service_statistics_out.calls_count.unwrap());
|
||||||
|
}
|
Loading…
Reference in New Issue