fts_axum/
lib.rs

1#![warn(missing_docs)]
2// Note: this overwrites the link in the README to point to the rust docs of the fts-sqlite crate.
3//! [fts_core]: https://docs.rs/fts_core/latest/fts_core/index.html
4//! [fts_axum]: https://docs.rs/fts_axum/latest/fts_axum/index.html
5//! [fts_solver]: https://docs.rs/fts_solver/latest/fts_solver/index.html
6//! [fts_sqlite]: https://docs.rs/fts_sqlite/latest/fts_sqlite/index.html
7#![doc = include_str!("../README.md")]
8
9mod batch_routes;
10mod demand_routes;
11mod portfolio_routes;
12mod product_routes;
13
14use aide::{
15    axum::{ApiRouter, routing::get},
16    openapi::OpenApi,
17};
18use axum::{Extension, Json};
19use fts_core::ports::{Application, Repository, Solver};
20use headers::{Authorization, authorization::Bearer};
21use schemars::JsonSchema;
22use serde::{Serialize, de::DeserializeOwned};
23use std::{fmt::Display, sync::Arc};
24
25mod openapi;
26use openapi::{api_docs, docs_routes};
27
28pub mod config;
29use config::AxumConfig;
30
31/// Response for the health check endpoint
32#[derive(Serialize, JsonSchema)]
33#[schemars(inline)]
34struct HealthResponse {
35    status: String,
36}
37
38/// Simple health check endpoint
39async fn health_check() -> Json<HealthResponse> {
40    Json(HealthResponse {
41        status: "ok".to_string(),
42    })
43}
44
45/// Extract the OpenAPI documentation for the server
46pub fn schema<T: ApiApplication>() -> OpenApi {
47    let mut api = OpenApi::default();
48    let _ = ApiRouter::new()
49        .api_route("/health", get(health_check))
50        .nest("/product", product_routes::router::<T>())
51        .nest("/demand", demand_routes::router::<T>())
52        .nest("/portfolio", portfolio_routes::router::<T>())
53        .nest("/batch", batch_routes::router::<T>())
54        .nest_api_service("/docs", docs_routes())
55        .finish_api_with(&mut api, api_docs);
56    api
57}
58
59/// Construct a full API router with the given state and config
60pub fn router<T: ApiApplication>(state: T, config: AxumConfig) -> axum::Router {
61    let mut api = OpenApi::default();
62    ApiRouter::new()
63        .api_route("/health", get(health_check))
64        .nest("/product", product_routes::router())
65        .nest("/demand", demand_routes::router())
66        .nest("/portfolio", portfolio_routes::router())
67        .nest("/batch", batch_routes::router())
68        .nest_api_service("/docs", docs_routes())
69        .finish_api_with(&mut api, api_docs)
70        .layer(Extension(Arc::new(api))) // Arc is very important here or you will face massive memory and performance issues
71        .layer(Extension(Arc::new(config)))
72        .with_state(state)
73}
74
75/// Starts the HTTP server with the provided configuration
76pub async fn start_server<T: ApiApplication>(
77    config: AxumConfig,
78    app: T,
79) -> Result<(), std::io::Error> {
80    let listener = tokio::net::TcpListener::bind(config.bind_address)
81        .await
82        .expect("Unable to bind to address");
83
84    tracing::info!(
85        "Listening for requests on {}",
86        listener.local_addr().unwrap()
87    );
88
89    // Here, we could apply additional config like timeouts, CORS, etc.
90    let service = router(app, config);
91    axum::serve(listener, service).await
92}
93
94/// Axum imposes all sorts of constraints on what can pass for state. This
95/// trait, coupled with a blanket implementation, specifies it all upfront and
96/// in one place. If a function takes a generic `T: ApiApplication`, then
97/// everything one might reasonably want to do should work.
98pub trait ApiApplication:
99    Clone
100    + Send
101    + Sync
102    + 'static
103    + Application<
104        Context = Authorization<Bearer>,
105        DemandData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
106        PortfolioData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
107        ProductData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
108        Repository: Clone
109                        + Send
110                        + Sync
111                        + 'static
112                        + Repository<
113            DateTime: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
114            BidderId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
115            DemandId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
116            PortfolioId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
117            ProductId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
118        >,
119        Solver: Solver<
120            <Self::Repository as Repository>::DemandId,
121            <Self::Repository as Repository>::PortfolioId,
122            <Self::Repository as Repository>::ProductId,
123            PortfolioOutcome: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
124            ProductOutcome: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
125        >,
126    >
127{
128}
129
130// this is the blanket implementation
131impl<T: Clone + Send + Sync + 'static> ApiApplication for T where
132    T: Application<
133            Context = Authorization<Bearer>,
134            DemandData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
135            PortfolioData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
136            ProductData: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
137            Repository: Clone
138                            + Send
139                            + Sync
140                            + 'static
141                            + Repository<
142                DateTime: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
143                BidderId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
144                DemandId: Clone + Display + Serialize + DeserializeOwned + JsonSchema + Send + Sync,
145                PortfolioId: Clone
146                                 + Display
147                                 + Serialize
148                                 + DeserializeOwned
149                                 + JsonSchema
150                                 + Send
151                                 + Sync,
152                ProductId: Clone
153                               + Display
154                               + Serialize
155                               + DeserializeOwned
156                               + JsonSchema
157                               + Send
158                               + Sync,
159            >,
160            Solver: Solver<
161                <T::Repository as Repository>::DemandId,
162                <T::Repository as Repository>::PortfolioId,
163                <T::Repository as Repository>::ProductId,
164                PortfolioOutcome: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
165                ProductOutcome: Send + Sync + Serialize + DeserializeOwned + JsonSchema + 'static,
166            >,
167        >
168{
169}