use std::{
net::TcpListener,
path::{Path, PathBuf},
};
use async_std::task::block_on;
use futures::future::join_all;
use itertools::Itertools;
use log::info;
use any_stub::AnyStubs;
use stub_finder::StubFinder;
use crate::error::StubrResult;
#[cfg(feature = "record-standalone")]
use crate::record::{config::RecordConfig, standalone::StubrRecord};
use crate::wiremock::MockServer;
use crate::{model::JsonStub, Config};
mod any_stub;
pub mod config;
pub mod stub_finder;
pub struct Stubr {
http_server: MockServer,
}
impl Stubr {
pub async fn try_start<T>(stubs: T) -> StubrResult<Self>
where
T: Into<AnyStubs>,
{
Self::try_start_with(stubs, Config::default()).await
}
pub async fn try_start_with<T>(stubs: T, config: Config) -> StubrResult<Self>
where
T: Into<AnyStubs>,
{
let server = if let Some(p) = config.port {
Self::try_start_on(p).await
} else {
Self::try_start_on_random_port().await
}?;
server.try_register_stubs(stubs.into(), config)?;
#[cfg(not(feature = "grpc"))]
server.register_cloud_features().await;
Ok(server)
}
pub fn try_start_blocking<T>(stubs: T) -> StubrResult<Self>
where
T: Into<AnyStubs>,
{
Self::try_start_blocking_with(stubs, Config::default())
}
pub fn try_start_blocking_with<T>(stubs: T, config: Config) -> StubrResult<Self>
where
T: Into<AnyStubs>,
{
block_on(Self::try_start_with(stubs, config))
}
#[cfg(feature = "record-standalone")]
pub fn try_record() -> StubrResult<StubrRecord> {
Self::try_record_with(RecordConfig::default())
}
#[cfg(feature = "record-standalone")]
pub fn try_record_with(config: RecordConfig) -> StubrResult<StubrRecord> {
Ok(StubrRecord::record(config))
}
pub async fn try_app(name: &str) -> StubrResult<Self> {
Self::try_app_with(name, Config::default()).await
}
pub async fn try_app_with(name: &str, config: Config) -> StubrResult<Self> {
Self::try_start_with(StubFinder::find_app(name), config).await
}
pub fn try_app_blocking(name: &str) -> StubrResult<Self> {
Self::try_app_blocking_with(name, Config::default())
}
pub fn try_app_blocking_with(name: &str, config: Config) -> StubrResult<Self> {
block_on(Self::try_app_with(name, config))
}
pub async fn try_apps(names: &[&str]) -> StubrResult<Vec<Self>> {
Self::try_apps_with(names, Config::default()).await
}
pub async fn try_apps_with(names: &[&str], config: Config) -> StubrResult<Vec<Self>> {
Ok(join_all(
names
.iter()
.map(|n| async move { Self::try_app_with(n, config).await.expect("TODO") }),
)
.await)
}
pub fn try_apps_blocking(names: &[&str]) -> StubrResult<Vec<Self>> {
Self::try_apps_blocking_with(names, Config::default())
}
pub fn try_apps_blocking_with(names: &[&str], config: Config) -> StubrResult<Vec<Self>> {
block_on(Self::try_apps_with(names, config))
}
pub fn uri(&self) -> String {
self.http_server.uri()
}
pub fn path(&self, path: &str) -> String {
format!("{}{}", self.uri(), path)
}
async fn try_start_on(port: u16) -> StubrResult<Self> {
if let Ok(listener) = TcpListener::bind(format!("{}:{}", Self::HOST, port)) {
let http_server = MockServer::builder()
.disable_request_recording()
.listener(listener)
.start()
.await?;
Ok(Self { http_server })
} else {
Self::try_start_on_random_port().await
}
}
async fn try_start_on_random_port() -> StubrResult<Self> {
let http_server = MockServer::builder().disable_request_recording().start().await?;
Ok(Self { http_server })
}
fn try_register_stubs(&self, stub_folder: AnyStubs, config: Config) -> StubrResult<()> {
stub_folder
.0
.iter()
.filter_map(|folder| self.try_find_all_mocks(folder).ok().map(|mocks| (folder, mocks)))
.flat_map(|(folder, mocks)| mocks.map(move |(s, p)| (s, p, folder)))
.sorted_by(|(a, ..), (b, ..)| a.priority.cmp(&b.priority))
.filter_map(|(stub, file, folder)| stub.try_creating_from(&config, &file).ok().map(|mock| (mock, file, folder)))
.for_each(|(mock, file, folder)| {
block_on(async move {
self.http_server.register(mock).await;
});
if config.verbose {
let maybe_file_name = file.strip_prefix(folder).ok().and_then(|file| file.to_str());
if let Some(file_name) = maybe_file_name {
info!("mounted stub '{}'", file_name);
}
};
});
Ok(())
}
#[allow(clippy::needless_lifetimes)]
fn try_find_all_mocks<'a>(&self, from: &Path) -> StubrResult<impl Iterator<Item = (JsonStub, PathBuf)> + 'a> {
Ok(StubFinder::find_all_stubs(from).filter_map(move |path| JsonStub::try_from(&path).ok().map(|stub| (stub, path))))
}
#[cfg(not(feature = "grpc"))]
async fn register_cloud_features(&self) {
self.http_server.register(crate::cloud::probe::HttpProbe::health_probe()).await;
}
}
impl Stubr {
#[cfg(feature = "cloud")]
const HOST: &'static str = "0.0.0.0";
#[cfg(not(feature = "cloud"))]
const HOST: &'static str = "127.0.0.1";
pub async fn start<T>(stubs: T) -> Self
where
T: Into<AnyStubs>,
{
Self::try_start(stubs).await.expect("Could not start server")
}
pub fn start_blocking<T>(stubs: T) -> Self
where
T: Into<AnyStubs>,
{
Self::start_blocking_with(stubs, Config::default())
}
pub async fn start_with<T>(stubs: T, config: Config) -> Self
where
T: Into<AnyStubs>,
{
Self::try_start_with(stubs, config).await.expect("Could not start server")
}
pub fn start_blocking_with<T>(stubs: T, config: Config) -> Self
where
T: Into<AnyStubs>,
{
block_on(Self::start_with(stubs, config))
}
#[cfg(feature = "record-standalone")]
pub fn record() -> StubrRecord {
Self::record_with(RecordConfig::default())
}
#[cfg(feature = "record-standalone")]
pub fn record_with(config: RecordConfig) -> StubrRecord {
Self::try_record_with(config).expect("Failed recording")
}
pub async fn app(name: &str) -> Self {
Self::app_with(name, Config::default()).await
}
pub async fn app_with(name: &str, config: Config) -> Self {
Self::start_with(StubFinder::find_app(name), config).await
}
pub fn app_blocking(name: &str) -> Self {
Self::app_blocking_with(name, Config::default())
}
pub fn app_blocking_with(name: &str, config: Config) -> Self {
block_on(Self::app_with(name, config))
}
pub async fn apps(names: &[&str]) -> Vec<Self> {
Self::apps_with(names, Config::default()).await
}
pub async fn apps_with(names: &[&str], config: Config) -> Vec<Self> {
join_all(names.iter().map(|n| async move { Self::app_with(n, config).await })).await
}
pub fn apps_blocking(names: &[&str]) -> Vec<Self> {
Self::apps_blocking_with(names, Config::default())
}
pub fn apps_blocking_with(names: &[&str], config: Config) -> Vec<Self> {
block_on(Self::apps_with(names, config))
}
}
#[cfg(test)]
mod server_test {
use super::*;
#[async_std::test]
async fn should_find_all_mocks_from_dir() {
let from = PathBuf::from("tests/stubs/server");
let stubr = Stubr::try_start_on_random_port().await.unwrap();
assert!(stubr.try_find_all_mocks(&from).unwrap().count().gt(&2));
}
#[async_std::test]
async fn should_find_all_mocks_from_single_file() {
let from = PathBuf::from("tests/stubs/server/valid.json");
let stubr = Stubr::try_start_on_random_port().await.unwrap();
assert_eq!(stubr.try_find_all_mocks(&from).unwrap().count(), 1);
}
#[async_std::test]
async fn should_not_find_any_mock_when_path_does_not_exist() {
let from = PathBuf::from("tests/stubs/server/unknown");
let stubr = Stubr::try_start_on_random_port().await.unwrap();
assert_eq!(stubr.try_find_all_mocks(&from).unwrap().count(), 0);
let from = PathBuf::from("tests/stubs/server/unknown.json");
let stubr = Stubr::try_start_on_random_port().await.unwrap();
assert_eq!(stubr.try_find_all_mocks(&from).unwrap().count(), 0);
}
}