wwsvc_mock/
lib.rs

1#![forbid(missing_docs)]
2#![forbid(unsafe_code)]
3#![warn(missing_debug_implementations)]
4#![doc = include_str!("../README.md")]
5
6use std::sync::Arc;
7
8use axum::{
9    body::{Body, Bytes},
10    extract::Request,
11    http::StatusCode,
12    middleware::Next,
13    response::{IntoResponse, Response},
14    routing::{get, put},
15    Router,
16};
17use http_body_util::BodyExt;
18
19mod app_config;
20mod routes;
21
22pub use app_config::{AppConfig, FileOrString, MockResource, MockResourceMethod, ServerConfig, WebwareConfig, WebservicesConfig, CredentialsConfig};
23use routes::{
24    exec_json::exec_json,
25    service_pass::{handle_deregister, handle_register},
26};
27
28#[derive(axum::extract::FromRef, Clone)]
29struct AppState {
30    pub config: Arc<AppConfig>,
31}
32
33#[cfg(not(tarpaulin_include))]
34async fn logging_middleware(
35    request: Request,
36    next: Next,
37) -> Result<impl IntoResponse, (StatusCode, String)> {
38    let (parts, body) = request.into_parts();
39    let bytes =
40        buffer_and_print(&format!("--> {} {}", parts.method, parts.uri.path()), body).await?;
41    let req = Request::from_parts(parts, Body::from(bytes));
42    let res = next.run(req).await;
43
44    let (parts, body) = res.into_parts();
45    let bytes = buffer_and_print(&format!("<-- {}", parts.status), body).await?;
46    let res = Response::from_parts(parts, Body::from(bytes));
47
48    Ok(res)
49}
50
51#[cfg(not(tarpaulin_include))]
52async fn buffer_and_print<B>(direction: &str, body: B) -> Result<Bytes, (StatusCode, String)>
53where
54    B: axum::body::HttpBody<Data = Bytes>,
55    B::Error: std::fmt::Display,
56{
57    let bytes = match body.collect().await {
58        Ok(collected) => collected.to_bytes(),
59        Err(err) => {
60            return Err((
61                StatusCode::BAD_REQUEST,
62                format!("failed to read {direction} body: {err}"),
63            ));
64        }
65    };
66
67    if let Ok(body) = std::str::from_utf8(&bytes) {
68        tracing::debug!("{} {}", direction, body);
69    }
70
71    Ok(bytes)
72}
73
74/// A wrapper for `serde_json::Value` that serializes as an empty object if `None`.
75#[derive(serde::Serialize, Debug)]
76pub struct OptionalJson(
77    #[serde(skip_serializing_if = "Option::is_none")] Option<serde_json::Value>,
78);
79
80/// Generates the router for the mock server using the provided configuration.
81/// 
82/// It currently supports the following routes:
83/// 
84/// - `PUT/POST/DELETE /WWSVC/EXECJSON/`
85/// - `PUT/POST/DELETE /WWSVC/EXECJSON`
86/// - `GET /WWSVC/WWSERVICE/REGISTER/:vendor_hash/:app_hash/:secret/:revision/`
87/// - `GET /WWSVC/WWSERVICE/DEREGISTER/:service_pass/`
88pub async fn app(config: &AppConfig) -> anyhow::Result<Router> {
89    let registering_routes = Router::new()
90        .route(
91            "/REGISTER/:vendor_hash/:app_hash/:secret/:revision/",
92            get(handle_register),
93        )
94        .route("/DEREGISTER/:service_pass/", get(handle_deregister));
95
96    let wwsvc_router = Router::new()
97        .route(
98            "/EXECJSON/",
99            put(exec_json).post(exec_json).delete(exec_json),
100        )
101        .route(
102            "/EXECJSON",
103            put(exec_json).post(exec_json).delete(exec_json),
104        )
105        .nest("/WWSERVICE", registering_routes);
106
107    let mut router = Router::new()
108        .nest("/WWSVC", wwsvc_router)
109        .with_state(AppState {
110            config: Arc::new(config.clone()),
111        });
112
113    if config.debug {
114        router = router.layer(axum::middleware::from_fn(logging_middleware));
115    }
116
117    Ok(router)
118}