1use std::collections::HashMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17
18use lino_objects_codec::LinoValue;
19
20use crate::codec::to_json_pretty;
21use crate::cors::{CorsPolicy, cors_headers};
22use crate::description::{ServiceInfo, openapi_document, service_description};
23use crate::headers::Headers;
24use crate::media_type::{JSON_CONTENT_TYPE, LINO_CONTENT_TYPE};
25use crate::middleware::{
26 DEFAULT_MAX_BODY_BYTES, EtagPolicy, ResponseParts, append_vary, build_problem_response,
27 build_response, decode_request_body, negotiate_request,
28};
29use crate::problem::LinoHttpError;
30use crate::request::{LinoRequest, QueryDefaults};
31use crate::resource::{ResourceOptions, register_resource};
32use crate::response::{Body, LinoResult, raw_response};
33use crate::router::{RouteMeta, RouteTable};
34use crate::store::Store;
35
36pub const HTTP_METHODS: [&str; 7] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
38
39pub const DESCRIPTION_PATH: &str = "/.well-known/lino-api";
41
42pub const OPENAPI_PATH: &str = "/.well-known/openapi.json";
44
45pub type HandlerFuture = Pin<Box<dyn Future<Output = Result<LinoResult, LinoHttpError>> + Send>>;
47
48pub type Handler = Arc<dyn Fn(LinoRequest) -> HandlerFuture + Send + Sync>;
50
51#[derive(Debug, Clone, Default)]
53pub struct RawRequest {
54 pub method: String,
56 pub path: String,
58 pub query_string: String,
60 pub headers: Headers,
62 pub body: Vec<u8>,
64}
65
66impl RawRequest {
67 pub fn new<M: Into<String>, P: Into<String>>(method: M, path: P) -> Self {
69 Self {
70 method: method.into().to_uppercase(),
71 path: path.into(),
72 ..Self::default()
73 }
74 }
75
76 pub fn with_query<S: Into<String>>(mut self, query_string: S) -> Self {
78 self.query_string = query_string.into();
79 self
80 }
81
82 pub fn with_header<N: Into<String>, V: Into<String>>(mut self, name: N, value: V) -> Self {
84 self.headers.insert(name, value);
85 self
86 }
87
88 pub fn with_body<B: Into<Vec<u8>>>(mut self, body: B, content_type: &str) -> Self {
90 self.body = body.into();
91 self.headers.insert("Content-Type", content_type);
92 self
93 }
94}
95
96pub struct LinoApp {
98 pub info: ServiceInfo,
100 pub cors: Option<CorsPolicy>,
102 pub supported: Option<Vec<String>>,
104 pub max_body_bytes: usize,
106 pub query_defaults: QueryDefaults,
108 routes: RouteTable,
109 handlers: HashMap<(String, String), Handler>,
110 describe_routes: bool,
111}
112
113impl Default for LinoApp {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl LinoApp {
120 pub fn new() -> Self {
140 let mut app = Self {
141 info: ServiceInfo::default(),
142 cors: None,
143 supported: None,
144 max_body_bytes: DEFAULT_MAX_BODY_BYTES,
145 query_defaults: QueryDefaults::default(),
146 routes: RouteTable::new(),
147 handlers: HashMap::new(),
148 describe_routes: true,
149 };
150 app.register_description();
151 app
152 }
153
154 pub fn with_info(mut self, info: ServiceInfo) -> Self {
156 self.info = info;
157 self
158 }
159
160 pub fn with_cors(mut self, cors: CorsPolicy) -> Self {
162 self.cors = Some(cors);
163 self
164 }
165
166 pub fn with_supported<I: IntoIterator<Item = String>>(mut self, supported: I) -> Self {
168 self.supported = Some(supported.into_iter().collect());
169 self
170 }
171
172 pub fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self {
174 self.max_body_bytes = max_body_bytes;
175 self
176 }
177
178 pub fn with_query_defaults(mut self, query_defaults: QueryDefaults) -> Self {
180 self.query_defaults = query_defaults;
181 self
182 }
183
184 pub fn without_description(mut self) -> Self {
186 self.describe_routes = false;
187 self.routes.remove("GET", DESCRIPTION_PATH);
188 self.routes.remove("GET", OPENAPI_PATH);
189 self
190 }
191
192 pub fn route<F, Fut, R>(
201 &mut self,
202 method: &str,
203 path: &str,
204 summary: &str,
205 handler: F,
206 ) -> &mut Self
207 where
208 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
209 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
210 R: Into<LinoResult> + Send + 'static,
211 {
212 let normalized = method.to_uppercase();
213 assert!(
214 HTTP_METHODS.contains(&normalized.as_str()),
215 "Unsupported HTTP method: {method}"
216 );
217
218 self.routes
219 .register(&normalized, path, RouteMeta::summary(summary));
220 let handler: Handler = Arc::new(move |request| {
221 let future = handler(request);
222 Box::pin(async move { future.await.map(Into::into) })
223 });
224 self.handlers
225 .insert((normalized, path.to_string()), handler);
226 self
227 }
228
229 pub fn route_fn<F, R>(
231 &mut self,
232 method: &str,
233 path: &str,
234 summary: &str,
235 handler: F,
236 ) -> &mut Self
237 where
238 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
239 R: Into<LinoResult> + Send + 'static,
240 {
241 self.route(method, path, summary, move |request| {
242 std::future::ready(handler(request))
243 })
244 }
245
246 pub fn get<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
248 where
249 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
250 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
251 R: Into<LinoResult> + Send + 'static,
252 {
253 self.route("GET", path, summary, handler)
254 }
255
256 pub fn post<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
258 where
259 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
260 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
261 R: Into<LinoResult> + Send + 'static,
262 {
263 self.route("POST", path, summary, handler)
264 }
265
266 pub fn put<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
268 where
269 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
270 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
271 R: Into<LinoResult> + Send + 'static,
272 {
273 self.route("PUT", path, summary, handler)
274 }
275
276 pub fn patch<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
278 where
279 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
280 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
281 R: Into<LinoResult> + Send + 'static,
282 {
283 self.route("PATCH", path, summary, handler)
284 }
285
286 pub fn delete<F, Fut, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
288 where
289 F: Fn(LinoRequest) -> Fut + Send + Sync + 'static,
290 Fut: Future<Output = Result<R, LinoHttpError>> + Send + 'static,
291 R: Into<LinoResult> + Send + 'static,
292 {
293 self.route("DELETE", path, summary, handler)
294 }
295
296 pub fn get_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
298 where
299 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
300 R: Into<LinoResult> + Send + 'static,
301 {
302 self.route_fn("GET", path, summary, handler)
303 }
304
305 pub fn post_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
307 where
308 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
309 R: Into<LinoResult> + Send + 'static,
310 {
311 self.route_fn("POST", path, summary, handler)
312 }
313
314 pub fn put_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
316 where
317 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
318 R: Into<LinoResult> + Send + 'static,
319 {
320 self.route_fn("PUT", path, summary, handler)
321 }
322
323 pub fn patch_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
325 where
326 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
327 R: Into<LinoResult> + Send + 'static,
328 {
329 self.route_fn("PATCH", path, summary, handler)
330 }
331
332 pub fn delete_fn<F, R>(&mut self, path: &str, summary: &str, handler: F) -> &mut Self
334 where
335 F: Fn(LinoRequest) -> Result<R, LinoHttpError> + Send + Sync + 'static,
336 R: Into<LinoResult> + Send + 'static,
337 {
338 self.route_fn("DELETE", path, summary, handler)
339 }
340
341 pub fn resource<S: Store + 'static>(
345 &mut self,
346 path: &str,
347 store: Arc<S>,
348 options: ResourceOptions,
349 ) -> &mut Self {
350 register_resource(self, path, store, options);
351 self
352 }
353
354 fn register_description(&mut self) {
360 self.routes.register(
361 "GET",
362 DESCRIPTION_PATH,
363 RouteMeta::summary("Service description"),
364 );
365 self.routes.register(
366 "GET",
367 OPENAPI_PATH,
368 RouteMeta::summary("OpenAPI 3.1 description"),
369 );
370 }
371
372 pub fn describe(&self) -> LinoValue {
376 service_description(
377 &self.info,
378 &self.routes.describe(),
379 self.supported.as_deref(),
380 )
381 }
382
383 pub fn openapi(&self) -> LinoValue {
385 openapi_document(
386 &self.info,
387 &self.routes.describe(),
388 self.supported.as_deref(),
389 )
390 }
391
392 pub fn routes(&self) -> &RouteTable {
394 &self.routes
395 }
396
397 pub async fn handle(&self, mut request: LinoRequest) -> Result<ResponseParts, LinoHttpError> {
406 let Some((entry, params)) = self.routes.match_path(&request.path) else {
407 return Err(LinoHttpError::new(
408 404,
409 Some(&format!("No resource at {}", request.path)),
410 ));
411 };
412 request.params = params;
413 let pattern = entry.pattern.clone();
414 let allowed = self.routes.allowed_methods_of(entry).join(", ");
415
416 let method = request.method.clone();
417 let lookup = if method == "HEAD" && entry.has_method("GET") {
420 "GET".to_string()
421 } else {
422 method.clone()
423 };
424
425 if method == "OPTIONS" && !entry.has_method("OPTIONS") {
426 let mut headers = Headers::new();
427 headers.insert("Allow", allowed);
428 return Ok(ResponseParts::new(204, headers, ""));
429 }
430
431 if !entry.has_method(&lookup) {
432 return Err(LinoHttpError::new(
433 405,
434 Some(&format!("{method} is not allowed on {}", request.path)),
435 )
436 .with_header("Allow", allowed));
437 }
438
439 let result = match self.handlers.get(&(lookup.clone(), pattern.clone())) {
440 Some(handler) => handler(request.clone()).await?,
441 None => self.describe_route(&pattern)?,
442 };
443
444 build_response(
445 &result.body,
446 result.status,
447 result.media_type.as_deref().unwrap_or(&request.media_type),
448 &result.headers,
449 &request.headers,
450 &method,
451 EtagPolicy {
452 etag: result.etag,
453 preconditions: result.preconditions,
454 require_precondition: result.require_precondition,
455 },
456 )
457 }
458
459 fn describe_route(&self, pattern: &str) -> Result<LinoResult, LinoHttpError> {
466 match pattern {
467 DESCRIPTION_PATH if self.describe_routes => Ok(self.describe().into()),
468 OPENAPI_PATH if self.describe_routes => Ok(raw_response(
469 &format!("{}\n", to_json_pretty(&self.openapi())),
470 JSON_CONTENT_TYPE,
471 200,
472 )),
473 other => Err(LinoHttpError::new(
474 500,
475 Some(&format!("No handler registered for {other}")),
476 )),
477 }
478 }
479
480 pub async fn respond(&self, raw: RawRequest) -> ResponseParts {
486 let method = raw.method.to_uppercase();
487 let extra_headers = match &self.cors {
488 Some(policy) => cors_headers(policy, raw.headers.get("origin")),
489 None => Headers::new(),
490 };
491 let preflight =
492 method == "OPTIONS" && raw.headers.contains("access-control-request-method");
493
494 let mut media_type = LINO_CONTENT_TYPE.to_string();
495 let instance = if raw.query_string.is_empty() {
496 raw.path.clone()
497 } else {
498 format!("{}?{}", raw.path, raw.query_string)
499 };
500
501 let mut parts = if preflight && !extra_headers.is_empty() {
502 ResponseParts::new(204, Headers::new(), "")
503 } else {
504 match self.decode_and_handle(&raw, &method, &mut media_type).await {
505 Ok(parts) => parts,
506 Err(error) => build_problem_response(&error, &media_type, Some(&instance)),
507 }
508 };
509
510 parts.headers.extend_missing(&extra_headers);
511 if extra_headers
512 .get("Access-Control-Allow-Origin")
513 .is_some_and(|origin| origin != "*")
514 {
515 append_vary(&mut parts.headers, "Origin");
516 }
517 if method == "HEAD" {
518 parts.body = String::new();
519 }
520 parts
521 }
522
523 async fn decode_and_handle(
530 &self,
531 raw: &RawRequest,
532 method: &str,
533 media_type: &mut String,
534 ) -> Result<ResponseParts, LinoHttpError> {
535 *media_type = negotiate_request(&raw.headers, self.supported.as_deref())?;
536 let decoded = decode_request_body(
537 &raw.body,
538 raw.headers.get("content-type"),
539 self.max_body_bytes,
540 )?;
541
542 let mut request = LinoRequest::new(method, &raw.path);
543 request.headers = raw.headers.clone();
544 request.query_string = raw.query_string.clone();
545 request.media_type = media_type.clone();
546 request.body = decoded.value;
547 request.request_media_type = decoded.media_type;
548 request.query_defaults = self.query_defaults;
549 self.handle(request).await
550 }
551}
552
553pub fn create_lino_app() -> LinoApp {
555 LinoApp::new()
556}
557
558pub const EMPTY: Body = Body::Empty;