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