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.validation#RestJsonValidation", "rest_json_validation"),
|
||||
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") {
|
||||
description = "generate smithy-build.json"
|
||||
doFirst {
|
||||
projectDir.resolve("smithy-build.json")
|
||||
.writeText(generateSmithyBuild(
|
||||
rootProject.projectDir.absolutePath,
|
||||
pluginName,
|
||||
codegenTests(properties, allCodegenTests))
|
||||
.writeText(
|
||||
generateSmithyBuild(
|
||||
rootProject.projectDir.absolutePath,
|
||||
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 }
|
||||
""".trimIndent()
|
||||
)
|
||||
),
|
||||
CodegenTest("com.aws.example#PokemonService", "pokemon_service_client")
|
||||
)
|
||||
|
||||
task("generateSmithyBuild") {
|
||||
description = "generate smithy-build.json"
|
||||
doFirst {
|
||||
projectDir.resolve("smithy-build.json")
|
||||
.writeText(generateSmithyBuild(
|
||||
rootProject.projectDir.absolutePath,
|
||||
pluginName,
|
||||
codegenTests(properties, allCodegenTests))
|
||||
.writeText(
|
||||
generateSmithyBuild(
|
||||
rootProject.projectDir.absolutePath,
|
||||
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