1use std::{
6 collections::HashSet,
7 sync::{Arc, OnceLock},
8};
9
10use axum::{
11 Router,
12 body::Bytes,
13 extract::State,
14 handler::Handler,
15 http::Method,
16 response::Html,
17 routing::{MethodRouter, get},
18};
19use openapiv3::v3_1::{
20 Components, Header, Info, MediaType, OpenApi, Operation, Parameter, ParameterData, PathItem,
21 Paths, ReferenceOr, RequestBody, Response, SchemaObject, Tag,
22};
23use schemars::{JsonSchema, gen::SchemaGenerator};
24use tap::Pipe;
25
26pub trait ApiEndpoint<S> {
27 fn method(&self) -> Method;
28 fn path(&self) -> &'static str;
29 fn hidden(&self) -> bool {
30 false
31 }
32
33 fn operation(&self, _generator: &mut SchemaGenerator) -> Operation {
34 Operation::default()
35 }
36
37 fn handler(&self) -> RouteHandler<S>;
38}
39
40pub struct RouteHandler<S> {
41 method: axum::http::Method,
42 handler: MethodRouter<S>,
43}
44
45impl<S: Clone> RouteHandler<S> {
46 pub fn new<H, T>(method: axum::http::Method, handler: H) -> Self
47 where
48 H: Handler<T, S>,
49 T: 'static,
50 S: Send + Sync + 'static,
51 {
52 let handler = MethodRouter::new().on(method.clone().try_into().unwrap(), handler);
53
54 Self { method, handler }
55 }
56
57 pub fn method(&self) -> &axum::http::Method {
58 &self.method
59 }
60}
61
62pub struct Api<'a, S> {
63 endpoints: Vec<&'a dyn ApiEndpoint<S>>,
64 info: Info,
65}
66
67impl<'a, S> Api<'a, S> {
68 pub fn new(info: Info) -> Self {
69 Self {
70 endpoints: Vec::new(),
71 info,
72 }
73 }
74
75 pub fn register_endpoints<I: IntoIterator<Item = &'a dyn ApiEndpoint<S>>>(
76 &mut self,
77 endpoints: I,
78 ) {
79 self.endpoints.extend(endpoints);
80 }
81
82 pub fn to_router(&self) -> axum::Router<S>
83 where
84 S: Clone + Send + Sync + 'static,
85 {
86 let mut router = OpenApiDocument::new(self.openapi()).into_router();
87 for endpoint in &self.endpoints {
88 let handler = endpoint.handler();
89 assert_eq!(handler.method(), endpoint.method());
90
91 let path = endpoint.path().replace('{', ":").replace('}', "");
94
95 router = router.route(&path, handler.handler);
96 }
97
98 router
99 }
100
101 pub fn openapi(&self) -> openapiv3::versioned::OpenApi {
102 self.gen_openapi(self.info.clone())
103 }
104
105 fn gen_openapi(&self, info: Info) -> openapiv3::versioned::OpenApi {
108 let mut openapi = OpenApi {
109 info,
110 ..Default::default()
111 };
112
113 let settings = schemars::gen::SchemaSettings::draft07().with(|s| {
114 s.definitions_path = "#/components/schemas/".into();
115 s.option_add_null_type = false;
116 });
117 let mut generator = schemars::gen::SchemaGenerator::new(settings);
118 let mut tags = HashSet::new();
119
120 let paths = openapi
121 .paths
122 .get_or_insert(openapiv3::v3_1::Paths::default());
123
124 for endpoint in &self.endpoints {
125 if endpoint.hidden() {
127 continue;
128 }
129
130 Self::register_endpoint(*endpoint, &mut generator, paths, &mut tags);
131 }
132
133 let openapi_endpoints: [&dyn ApiEndpoint<_>; 3] =
135 [&OpenApiExplorer, &OpenApiJson, &OpenApiYaml];
136 for endpoint in openapi_endpoints {
137 Self::register_endpoint(endpoint, &mut generator, paths, &mut tags);
138 }
139
140 let components = &mut openapi.components.get_or_insert_with(Components::default);
141
142 let schemas = &mut components.schemas;
144
145 generator
146 .into_root_schema_for::<()>()
147 .definitions
148 .into_iter()
149 .for_each(|(key, schema)| {
150 let schema_object = SchemaObject {
151 json_schema: schema,
152 external_docs: None,
153 example: None,
154 };
155 schemas.insert(key, schema_object);
156 });
157
158 openapi.tags = tags
159 .into_iter()
160 .map(|tag| Tag {
161 name: tag,
162 ..Default::default()
163 })
164 .collect();
165 openapi.tags.sort_by(|a, b| a.name.cmp(&b.name));
167
168 openapi.servers = vec![openapiv3::v3_1::Server {
169 url: "/api/v1".into(),
170 ..Default::default()
171 }];
172
173 openapiv3::versioned::OpenApi::Version31(openapi)
174 }
175
176 fn register_endpoint<S2>(
177 endpoint: &dyn ApiEndpoint<S2>,
178 generator: &mut schemars::gen::SchemaGenerator,
179 paths: &mut Paths,
180 tags: &mut HashSet<String>,
181 ) {
182 let path = paths
183 .paths
184 .entry(endpoint.path().to_owned())
185 .or_insert(ReferenceOr::Item(PathItem::default()));
186
187 let pathitem = match path {
188 openapiv3::v3_1::ReferenceOr::Item(ref mut item) => item,
189 _ => panic!("reference not expected"),
190 };
191
192 let method_ref = match endpoint.method() {
193 Method::DELETE => &mut pathitem.delete,
194 Method::GET => &mut pathitem.get,
195 Method::HEAD => &mut pathitem.head,
196 Method::OPTIONS => &mut pathitem.options,
197 Method::PATCH => &mut pathitem.patch,
198 Method::POST => &mut pathitem.post,
199 Method::PUT => &mut pathitem.put,
200 Method::TRACE => &mut pathitem.trace,
201 other => panic!("unexpected method `{}`", other),
202 };
203
204 let operation = endpoint.operation(generator);
205
206 tags.extend(operation.tags.clone());
208
209 method_ref.replace(operation);
210 }
211}
212
213pub struct OpenApiDocument {
214 openapi: openapiv3::versioned::OpenApi,
215 json: OnceLock<Bytes>,
216 yaml: OnceLock<Bytes>,
217 ui: &'static str,
218}
219
220impl OpenApiDocument {
221 pub fn new(openapi: openapiv3::versioned::OpenApi) -> Self {
222 const OPENAPI_UI: &str = include_str!("../openapi/elements.html");
223 Self {
226 openapi,
227 json: OnceLock::new(),
228 yaml: OnceLock::new(),
229 ui: OPENAPI_UI,
230 }
231 }
232
233 fn openapi(&self) -> &openapiv3::versioned::OpenApi {
234 &self.openapi
235 }
236
237 fn json(&self) -> Bytes {
238 self.json
239 .get_or_init(|| {
240 self.openapi()
241 .pipe(serde_json::to_string_pretty)
242 .unwrap()
243 .pipe(Bytes::from)
244 })
245 .clone()
246 }
247
248 fn yaml(&self) -> Bytes {
249 self.yaml
250 .get_or_init(|| {
251 self.openapi()
252 .pipe(serde_yaml::to_string)
253 .unwrap()
254 .pipe(Bytes::from)
255 })
256 .clone()
257 }
258
259 fn ui(&self) -> &'static str {
260 self.ui
261 }
262
263 pub fn into_router<S>(self) -> Router<S> {
264 Router::new()
265 .route("/openapi", get(openapi_ui))
266 .route("/openapi.json", get(openapi_json))
267 .route("/openapi.yaml", get(openapi_yaml))
268 .with_state(Arc::new(self))
269 }
270}
271
272pub struct OpenApiJson;
273
274impl ApiEndpoint<Arc<OpenApiDocument>> for OpenApiJson {
275 fn method(&self) -> axum::http::Method {
276 axum::http::Method::GET
277 }
278
279 fn path(&self) -> &'static str {
280 "/openapi.json"
281 }
282
283 fn operation(
284 &self,
285 _generator: &mut schemars::gen::SchemaGenerator,
286 ) -> openapiv3::v3_1::Operation {
287 OperationBuilder::new()
288 .tag("OpenApi")
289 .operation_id("openapi.json")
290 .response(
291 200,
292 ResponseBuilder::new()
293 .content(mime::APPLICATION_JSON.as_ref(), MediaType::default())
294 .build(),
295 )
296 .build()
297 }
298
299 fn handler(&self) -> RouteHandler<Arc<OpenApiDocument>> {
300 RouteHandler::new(self.method(), openapi_json)
301 }
302}
303
304pub struct OpenApiYaml;
305
306impl ApiEndpoint<Arc<OpenApiDocument>> for OpenApiYaml {
307 fn method(&self) -> axum::http::Method {
308 axum::http::Method::GET
309 }
310
311 fn path(&self) -> &'static str {
312 "/openapi.yaml"
313 }
314
315 fn operation(
316 &self,
317 _generator: &mut schemars::gen::SchemaGenerator,
318 ) -> openapiv3::v3_1::Operation {
319 OperationBuilder::new()
320 .tag("OpenApi")
321 .operation_id("openapi.yaml")
322 .response(
323 200,
324 ResponseBuilder::new()
325 .content(mime::TEXT_PLAIN_UTF_8.as_ref(), MediaType::default())
326 .build(),
327 )
328 .build()
329 }
330
331 fn handler(&self) -> RouteHandler<Arc<OpenApiDocument>> {
332 RouteHandler::new(self.method(), openapi_yaml)
333 }
334}
335
336pub struct OpenApiExplorer;
337
338impl ApiEndpoint<Arc<OpenApiDocument>> for OpenApiExplorer {
339 fn method(&self) -> axum::http::Method {
340 axum::http::Method::GET
341 }
342
343 fn path(&self) -> &'static str {
344 "/openapi"
345 }
346
347 fn operation(
348 &self,
349 _generator: &mut schemars::gen::SchemaGenerator,
350 ) -> openapiv3::v3_1::Operation {
351 OperationBuilder::new()
352 .tag("OpenApi")
353 .operation_id("OpenApi Explorer")
354 .response(
355 200,
356 ResponseBuilder::new()
357 .content(mime::TEXT_HTML_UTF_8.as_ref(), MediaType::default())
358 .build(),
359 )
360 .build()
361 }
362
363 fn handler(&self) -> RouteHandler<Arc<OpenApiDocument>> {
364 RouteHandler::new(self.method(), openapi_ui)
365 }
366}
367
368async fn openapi_json(
369 State(document): State<Arc<OpenApiDocument>>,
370) -> impl axum::response::IntoResponse {
371 (
372 [(
373 axum::http::header::CONTENT_TYPE,
374 axum::http::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
375 )],
376 document.json(),
377 )
378}
379
380async fn openapi_yaml(
381 State(document): State<Arc<OpenApiDocument>>,
382) -> impl axum::response::IntoResponse {
383 (
384 [(
385 axum::http::header::CONTENT_TYPE,
386 axum::http::HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
387 )],
388 document.yaml(),
389 )
390}
391
392async fn openapi_ui(State(document): State<Arc<OpenApiDocument>>) -> Html<&'static str> {
393 Html(document.ui())
394}
395
396fn path_parameter<T: JsonSchema>(
397 name: &str,
398 generator: &mut SchemaGenerator,
399) -> ReferenceOr<Parameter> {
400 let schema_object = SchemaObject {
401 json_schema: generator.subschema_for::<T>(),
402 external_docs: None,
403 example: None,
404 };
405
406 let parameter_data = ParameterData {
407 name: name.into(),
408 required: true,
409 format: openapiv3::v3_1::ParameterSchemaOrContent::Schema(schema_object),
410 description: None,
411 deprecated: None,
412 example: None,
413 examples: Default::default(),
414 explode: None,
415 extensions: Default::default(),
416 };
417
418 ReferenceOr::Item(Parameter::Path {
419 parameter_data,
420 style: openapiv3::v3_1::PathStyle::Simple,
421 })
422}
423
424fn query_parameters<T: JsonSchema>(generator: &mut SchemaGenerator) -> Vec<ReferenceOr<Parameter>> {
425 let mut params = Vec::new();
426
427 let schema = generator.root_schema_for::<T>().schema;
428
429 let Some(object) = &schema.object else {
430 return params;
431 };
432
433 for (name, schema) in &object.properties {
434 let s = schema.clone().into_object();
435
436 params.push(ReferenceOr::Item(Parameter::Query {
437 parameter_data: ParameterData {
438 name: name.clone(),
439 description: s.metadata.as_ref().and_then(|m| m.description.clone()),
440 required: object.required.contains(name),
441 format: openapiv3::v3_1::ParameterSchemaOrContent::Schema(SchemaObject {
442 json_schema: s.into(),
443 example: None,
444 external_docs: None,
445 }),
446 extensions: Default::default(),
447 deprecated: None,
448 example: None,
449 examples: Default::default(),
450 explode: None,
451 },
452 allow_reserved: false,
453 style: openapiv3::v3_1::QueryStyle::Form,
454 allow_empty_value: None,
455 }));
456 }
457
458 params
459}
460
461#[derive(Default)]
462pub struct OperationBuilder {
463 inner: Operation,
464}
465
466impl OperationBuilder {
467 pub fn new() -> Self {
468 Self {
469 inner: Default::default(),
470 }
471 }
472
473 pub fn build(&mut self) -> Operation {
474 self.inner.clone()
475 }
476
477 pub fn tag<T: Into<String>>(&mut self, tag: T) -> &mut Self {
478 self.inner.tags.push(tag.into());
479 self
480 }
481
482 pub fn summary<T: Into<String>>(&mut self, summary: T) -> &mut Self {
483 self.inner.summary = Some(summary.into());
484 self
485 }
486
487 pub fn description<T: Into<String>>(&mut self, description: T) -> &mut Self {
488 self.inner.description = Some(description.into());
489 self
490 }
491
492 pub fn operation_id<T: Into<String>>(&mut self, operation_id: T) -> &mut Self {
493 self.inner.operation_id = Some(operation_id.into());
494 self
495 }
496
497 pub fn path_parameter<T: JsonSchema>(
498 &mut self,
499 name: &str,
500 generator: &mut SchemaGenerator,
501 ) -> &mut Self {
502 self.inner
503 .parameters
504 .push(path_parameter::<T>(name, generator));
505 self
506 }
507
508 pub fn query_parameters<T: JsonSchema>(
509 &mut self,
510 generator: &mut SchemaGenerator,
511 ) -> &mut Self {
512 self.inner
513 .parameters
514 .extend(query_parameters::<T>(generator));
515 self
516 }
517
518 pub fn response(&mut self, status_code: u16, response: Response) -> &mut Self {
519 let responses = self.inner.responses.get_or_insert(Default::default());
520 responses.responses.insert(
521 openapiv3::v3_1::StatusCode::Code(status_code),
522 ReferenceOr::Item(response),
523 );
524
525 self
526 }
527
528 pub fn request_body(&mut self, request_body: RequestBody) -> &mut Self {
529 self.inner.request_body = Some(ReferenceOr::Item(request_body));
530 self
531 }
532}
533
534#[derive(Default)]
535pub struct ResponseBuilder {
536 inner: Response,
537}
538
539impl ResponseBuilder {
540 pub fn new() -> Self {
541 Self {
542 inner: Default::default(),
543 }
544 }
545
546 pub fn build(&mut self) -> Response {
547 self.inner.clone()
548 }
549
550 pub fn header<T: JsonSchema>(
551 &mut self,
552 name: &str,
553 generator: &mut SchemaGenerator,
554 ) -> &mut Self {
555 let schema_object = SchemaObject {
556 json_schema: generator.subschema_for::<T>(),
557 external_docs: None,
558 example: None,
559 };
560
561 let header = ReferenceOr::Item(Header {
562 description: None,
563 style: Default::default(),
564 required: false,
565 deprecated: None,
566 format: openapiv3::v3_1::ParameterSchemaOrContent::Schema(schema_object),
567 example: None,
568 examples: Default::default(),
569 extensions: Default::default(),
570 });
571
572 self.inner.headers.insert(name.into(), header);
573 self
574 }
575
576 pub fn content<T: Into<String>>(
577 &mut self,
578 content_type: T,
579 media_type: MediaType,
580 ) -> &mut Self {
581 self.inner.content.insert(content_type.into(), media_type);
582 self
583 }
584
585 pub fn json_content<T: JsonSchema>(&mut self, generator: &mut SchemaGenerator) -> &mut Self {
586 let schema_object = SchemaObject {
587 json_schema: generator.subschema_for::<T>(),
588 external_docs: None,
589 example: None,
590 };
591 let media_type = MediaType {
592 schema: Some(schema_object),
593 ..Default::default()
594 };
595
596 self.content(mime::APPLICATION_JSON.as_ref(), media_type)
597 }
598
599 pub fn bcs_content(&mut self) -> &mut Self {
600 self.content(crate::APPLICATION_BCS, MediaType::default())
601 }
602
603 pub fn text_content(&mut self) -> &mut Self {
604 self.content(mime::TEXT_PLAIN_UTF_8.as_ref(), MediaType::default())
605 }
606}
607
608#[derive(Default)]
609pub struct RequestBodyBuilder {
610 inner: RequestBody,
611}
612
613impl RequestBodyBuilder {
614 pub fn new() -> Self {
615 Self {
616 inner: Default::default(),
617 }
618 }
619
620 pub fn build(&mut self) -> RequestBody {
621 self.inner.clone()
622 }
623
624 pub fn content<T: Into<String>>(
625 &mut self,
626 content_type: T,
627 media_type: MediaType,
628 ) -> &mut Self {
629 self.inner.content.insert(content_type.into(), media_type);
630 self
631 }
632
633 pub fn json_content<T: JsonSchema>(&mut self, generator: &mut SchemaGenerator) -> &mut Self {
634 let schema_object = SchemaObject {
635 json_schema: generator.subschema_for::<T>(),
636 external_docs: None,
637 example: None,
638 };
639 let media_type = MediaType {
640 schema: Some(schema_object),
641 ..Default::default()
642 };
643
644 self.content(mime::APPLICATION_JSON.as_ref(), media_type)
645 }
646
647 pub fn bcs_content(&mut self) -> &mut Self {
648 self.content(crate::APPLICATION_BCS, MediaType::default())
649 }
650}