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    /// Indicates the stability of the API
34    ///
35    /// Stable APIs are enabled in the REST service by default, unstable ones
36    /// need to be explicitly configured to be enabled via config.
37    ///
38    /// Both stable and unstable APIs have a badge, indicating the api's
39    /// stability, added to the top of the description field of their
40    /// OpenAPI definition.
41    ///
42    /// By default all apis are unstable, individual apis need to explicitly
43    /// opt-in to being stable
44    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    /// Internal routine for constructing the OpenAPI definition describing this
117    /// API in its JSON form.
118    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            // Skip hidden endpoints
137            if endpoint.hidden() {
138                continue;
139            }
140
141            Self::register_endpoint(*endpoint, &mut generator, paths, &mut tags);
142        }
143
144        // Add OpenApi routes themselves
145        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        // Add the schemas for which we generated references.
154        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        // Sort the tags for stability
177        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        // Collect tags defined by this operation
218        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        // const OPENAPI_UI: &str = include_str!("../openapi/swagger.html");
235
236        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
283/// Return the OpenAPI v3.1.0 definition for this service as a JSON document
284pub 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
320/// Return the OpenAPI v3.1.0 definition for this service as a YAML document
321pub 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
357/// Provides a web UI for exploring the OpenAPI v3.1.0 definition for this
358/// service
359pub 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}