iota_rest_api/
openapi.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    /// Internal routine for constructing the OpenAPI definition describing this
102    /// API in its JSON form.
103    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            // Skip hidden endpoints
122            if endpoint.hidden() {
123                continue;
124            }
125
126            Self::register_endpoint(*endpoint, &mut generator, paths, &mut tags);
127        }
128
129        // Add OpenApi routes themselves
130        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        // Add the schemas for which we generated references.
139        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        // Sort the tags for stability
162        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        // Collect tags defined by this operation
203        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        // const OPENAPI_UI: &str = include_str!("../openapi/swagger.html");
220
221        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}