use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use lino_objects_codec::LinoValue;
use crate::codec::to_json_pretty;
use crate::cors::{CorsPolicy, cors_headers};
use crate::description::{ServiceInfo, openapi_document, service_description};
use crate::headers::Headers;
use crate::media_type::{JSON_CONTENT_TYPE, LINO_CONTENT_TYPE};
use crate::middleware::{
DEFAULT_MAX_BODY_BYTES, EtagPolicy, ResponseParts, append_vary, build_problem_response,
build_response, decode_request_body, negotiate_request,
};
use crate::problem::LinoHttpError;
use crate::request::{LinoRequest, QueryDefaults};
use crate::resource::{ResourceOptions, register_resource};
use crate::response::{Body, LinoResult, raw_response};
use crate::router::{RouteMeta, RouteTable};
use crate::store::Store;
pub const HTTP_METHODS: [&str; 7] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
pub const DESCRIPTION_PATH: &str = "/.well-known/lino-api";
pub const OPENAPI_PATH: &str = "/.well-known/openapi.json";
pub type HandlerFuture = Pin<Box<dyn Future<Output = Result<LinoResult, LinoHttpError>> + Send>>;
pub type Handler = Arc<dyn Fn(LinoRequest) -> HandlerFuture + Send + Sync>;
#[derive(Debug, Clone, Default)]
pub struct RawRequest {
pub method: String,
pub path: String,
pub query_string: String,
pub headers: Headers,
pub body: Vec<u8>,
}
impl RawRequest {
pub fn new<M: Into<String>, P: Into<String>>(method: M, path: P) -> Self {
Self {
method: method.into().to_uppercase(),
path: path.into(),
..Self::default()
}
}
pub fn with_query<S: Into<String>>(mut self, query_string: S) -> Self {
self.query_string = query_string.into();
self
}
pub fn with_header<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
self.headers.insert(name, value);
self
}
pub fn with_body<B: Into<Vec<u8>>>(mut self, body: B, content_type: &str) -> Self {
self.body = body.into();
self.headers.insert("Content-Type", content_type);
self
}
}
pub struct LinoApp {
pub info: ServiceInfo,
pub cors: Option<CorsPolicy>,
pub supported: Option<Vec<String>>,
pub max_body_bytes: usize,
pub query_defaults: QueryDefaults,
routes: RouteTable,
handlers: HashMap<(String, String), Handler>,
describe_routes: bool,
}
impl Default for LinoApp {
fn default() -> Self {
Self::new()
}
}
impl LinoApp {
pub fn new() -> Self {
let mut app = Self {
info: ServiceInfo::default(),
cors: None,
supported: None,
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
query_defaults: QueryDefaults::default(),
routes: RouteTable::new(),
handlers: HashMap::new(),
describe_routes: true,
};
app.register_description();
app
}
pub fn with_info(mut self, info: ServiceInfo) -> Self {
self.info = info;
self
}
pub fn with_cors(mut self, cors: CorsPolicy) -> Self {
self.cors = Some(cors);
self
}
pub fn with_supported<I: IntoIterator<Item = String>>(mut self, supported: I) -> Self {
self.supported = Some(supported.into_iter().collect());
self
}
pub fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
self.max_body_bytes = max_body_bytes;
self
}
pub fn with_query_defaults(mut self, query_defaults: QueryDefaults) -> Self {
self.query_defaults = query_defaults;
self
}
pub fn without_description(mut self) -> Self {
self.describe_routes = false;
self.routes.remove("GET", DESCRIPTION_PATH);
self.routes.remove("GET", OPENAPI_PATH);
self
}
pub fn route<F, Fut, R>(
&mut self,
method: &str,
path: &str,
summary: &str,
handler: F,
) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
let normalized = method.to_uppercase();
assert!(
HTTP_METHODS.contains(&normalized.as_str()),
"Unsupported HTTP method: {method}"
);
self.routes
.register(&normalized, path, RouteMeta::summary(summary));
let handler: Handler = Arc::new(move |request| {
let future = handler(request);
Box::pin(async move { future.await.map(Into::into) })
});
self.handlers
.insert((normalized, path.to_string()), handler);
self
}
pub fn route_fn<F, R>(
&mut self,
method: &str,
path: &str,
summary: &str,
handler: F,
) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route(method, path, summary, move |request| {
std::future::ready(handler(request))
})
}
pub fn get<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route("GET", path, summary, handler)
}
pub fn post<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route("POST", path, summary, handler)
}
pub fn put<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route("PUT", path, summary, handler)
}
pub fn patch<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route("PATCH", path, summary, handler)
}
pub fn delete<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route("DELETE", path, summary, handler)
}
pub fn get_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route_fn("GET", path, summary, handler)
}
pub fn post_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route_fn("POST", path, summary, handler)
}
pub fn put_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route_fn("PUT", path, summary, handler)
}
pub fn patch_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route_fn("PATCH", path, summary, handler)
}
pub fn delete_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
where
F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
R: Into<LinoResult> + Send + 'static,
{
self.route_fn("DELETE", path, summary, handler)
}
pub fn resource<S: Store + 'static>(
&mut self,
path: &str,
store: Arc<S>,
options: ResourceOptions,
) -> &mut Self {
register_resource(self, path, store, options);
self
}
fn register_description(&mut self) {
self.routes.register(
"GET",
DESCRIPTION_PATH,
RouteMeta::summary("Service description"),
);
self.routes.register(
"GET",
OPENAPI_PATH,
RouteMeta::summary("OpenAPI 3.1 description"),
);
}
pub fn describe(&self) -> LinoValue {
service_description(
&self.info,
&self.routes.describe(),
self.supported.as_deref(),
)
}
pub fn openapi(&self) -> LinoValue {
openapi_document(
&self.info,
&self.routes.describe(),
self.supported.as_deref(),
)
}
pub fn routes(&self) -> &RouteTable {
&self.routes
}
pub async fn handle(&self, mut request: LinoRequest) -> Result<ResponseParts, LinoHttpError> {
let Some((entry, params)) = self.routes.match_path(&request.path) else {
return Err(LinoHttpError::new(
404,
Some(&format!("No resource at {}", request.path)),
));
};
request.params = params;
let pattern = entry.pattern.clone();
let allowed = self.routes.allowed_methods_of(entry).join(", ");
let method = request.method.clone();
let lookup = if method == "HEAD" && entry.has_method("GET") {
"GET".to_string()
} else {
method.clone()
};
if method == "OPTIONS" && !entry.has_method("OPTIONS") {
let mut headers = Headers::new();
headers.insert("Allow", allowed);
return Ok(ResponseParts::new(204, headers, ""));
}
if !entry.has_method(&lookup) {
return Err(LinoHttpError::new(
405,
Some(&format!("{method} is not allowed on {}", request.path)),
)
.with_header("Allow", allowed));
}
let result = match self.handlers.get(&(lookup.clone(), pattern.clone())) {
Some(handler) => handler(request.clone()).await?,
None => self.describe_route(&pattern)?,
};
build_response(
&result.body,
result.status,
result.media_type.as_deref().unwrap_or(&request.media_type),
&result.headers,
&request.headers,
&method,
EtagPolicy {
etag: result.etag,
preconditions: result.preconditions,
require_precondition: result.require_precondition,
},
)
}
fn describe_route(&self, pattern: &str) -> Result<LinoResult, LinoHttpError> {
match pattern {
DESCRIPTION_PATH if self.describe_routes => Ok(self.describe().into()),
OPENAPI_PATH if self.describe_routes => Ok(raw_response(
&format!("{}\n", to_json_pretty(&self.openapi())),
JSON_CONTENT_TYPE,
200,
)),
other => Err(LinoHttpError::new(
500,
Some(&format!("No handler registered for {other}")),
)),
}
}
pub async fn respond(&self, raw: RawRequest) -> ResponseParts {
let method = raw.method.to_uppercase();
let extra_headers = match &self.cors {
Some(policy) => cors_headers(policy, raw.headers.get("origin")),
None => Headers::new(),
};
let preflight =
method == "OPTIONS" && raw.headers.contains("access-control-request-method");
let mut media_type = LINO_CONTENT_TYPE.to_string();
let instance = if raw.query_string.is_empty() {
raw.path.clone()
} else {
format!("{}?{}", raw.path, raw.query_string)
};
let mut parts = if preflight && !extra_headers.is_empty() {
ResponseParts::new(204, Headers::new(), "")
} else {
match self.decode_and_handle(&raw, &method, &mut media_type).await {
Ok(parts) => parts,
Err(error) => build_problem_response(&error, &media_type, Some(&instance)),
}
};
parts.headers.extend_missing(&extra_headers);
if extra_headers
.get("Access-Control-Allow-Origin")
.is_some_and(|origin| origin != "*")
{
append_vary(&mut parts.headers, "Origin");
}
if method == "HEAD" {
parts.body = String::new();
}
parts
}
async fn decode_and_handle(
&self,
raw: &RawRequest,
method: &str,
media_type: &mut String,
) -> Result<ResponseParts, LinoHttpError> {
*media_type = negotiate_request(&raw.headers, self.supported.as_deref())?;
let decoded = decode_request_body(
&raw.body,
raw.headers.get("content-type"),
self.max_body_bytes,
)?;
let mut request = LinoRequest::new(method, &raw.path);
request.headers = raw.headers.clone();
request.query_string = raw.query_string.clone();
request.media_type = media_type.clone();
request.body = decoded.value;
request.request_media_type = decoded.media_type;
request.query_defaults = self.query_defaults;
self.handle(request).await
}
}
pub fn create_lino_app() -> LinoApp {
LinoApp::new()
}
pub const EMPTY: Body = Body::Empty;