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