aptos_openapi_link/
helpers.rs

1// Copyright (c) Aptos
2// SPDX-License-Identifier: Apache-2.0
3
4//! In order to use a type with poem-openapi, it must implement a certain set
5//! of traits, depending on the context in which you want to use the type.
6//! For example, if you want to use a type in a struct that you return from
7//! an endpoint, it must implement Type. Normally you get this by deriving
8//! traits such as Object, Enum, Union, etc. However, in some cases, it is
9//! not feasible to use these derives.
10//!
11//!   - The type is outside of reach, e.g. in a crate in aptos-core that is
12//!     too unrelated, or even worse, in a totally different crate (the move
13//!     types are a great example of this).
14//!   - The type is not expressible via OpenAPI. For example, an enum that
15//!     has some enum variants with values and others without values.This is
16//!     not allowed in OpenAPI, types must be either unions (variants with
17//!     values) or enums (variants without values).
18//!   - We would prefer to serialize the data differently than its standard
19//!     representation. HexEncodedBytes is a good example of this. Internally,
20//!     this is a Vec<u8>, but we know it is hex and prefer to represent it as
21//!     a 0x string.
22//!
23//! For those cases, we have these macros. We can use these to implement the
24//! necessary traits for using these types with poem-openapi, without using
25//! the derives.
26//!
27//! Each macro explains itself in further detail.
28
29/// This macro allows you to use a type in a request / response type for use
30/// with poem-openapi. In order to use this macro, your type must implement
31/// Serialize and Deserialize, so we can encode it as JSON / a string.
32///
33/// With this macro, you can express what OpenAPI type you want your type to be
34/// expressed as in the spec. For example, if your type serializes just to a
35/// string, you likely want to invoke the macro like this:
36///
37///   impl_poem_type!(MyType, "string", ());
38///
39/// If your type is more complex, and you'd rather it become an "object" in the
40/// spec, you should invoke the macro like this:
41///
42///   impl_poem_type!(MyType, "object", ());
43///
44/// This macro supports applying additional information to the generated type.
45/// For example, you could invoke the macro like this:
46///
47///   impl_poem_type!(
48///       HexEncodedBytes,
49///       "string",
50///       (
51///           example = Some(serde_json::Value::String(
52///               "0x88fbd33f54e1126269769780feb24480428179f552e2313fbe571b72e62a1ca1".to_string())),
53///           description = Some("A hex encoded string"),
54///       )
55///   );
56///
57/// To see what different metadata you can apply to the generated type in the
58/// spec, take a look at MetaSchema here:
59/// https://github.com/poem-web/poem/blob/master/poem-openapi/src/registry/mod.rs
60#[macro_export]
61macro_rules! impl_poem_type {
62    ($ty:ty, $spec_type:literal, ($($key:ident = $value:expr),*)) => {
63
64        impl ::poem_openapi::types::Type for $ty {
65            const IS_REQUIRED: bool = true;
66
67            type RawValueType = Self;
68
69            type RawElementValueType = Self;
70
71            fn name() -> std::borrow::Cow<'static, str> {
72                format!("string({})", stringify!($ty)).into()
73            }
74
75            // We generate a MetaSchema for our type so we can use it as a
76            // a reference in `schema_ref`. The alternative is `schema_ref`
77            // generates its own schema there and uses it inline, which leads
78            // to lots of repetition in the spec.
79            //
80            // For example:
81            //
82            //   gas_unit_price:
83            //     $ref: "#/components/schemas/U64"
84            //
85            // Which refers to:
86            //
87            //   components:
88            //       U64:
89            //         type: string
90            //         pattern: [0-9]+
91            fn register(registry: &mut poem_openapi::registry::Registry) {
92                registry.create_schema::<Self, _>(stringify!($ty).to_string(), |_registry| {
93                    #[allow(unused_mut)]
94                    let mut meta_schema = poem_openapi::registry::MetaSchema::new($spec_type);
95                    $(
96                    meta_schema.$key = $value;
97                    )*
98                    meta_schema
99                })
100            }
101
102            // This function determines what the schema looks like when this
103            // type appears in the spec. In our case, it will look like a
104            // a reference to the type we generate in the spec.
105            fn schema_ref() -> ::poem_openapi::registry::MetaSchemaRef {
106                ::poem_openapi::registry::MetaSchemaRef::Reference(format!("{}", stringify!($ty)))
107            }
108
109            fn as_raw_value(&self) -> Option<&Self::RawValueType> {
110                Some(self)
111            }
112
113            fn raw_element_iter<'a>(
114                &'a self,
115            ) -> Box<dyn Iterator<Item = &'a Self::RawElementValueType> + 'a> {
116                Box::new(self.as_raw_value().into_iter())
117            }
118        }
119
120        impl ::poem_openapi::types::ParseFromJSON for $ty {
121            fn parse_from_json(value: Option<serde_json::Value>) -> ::poem_openapi::types::ParseResult<Self> {
122                let value = value.unwrap_or_default();
123                Ok(::serde_json::from_value(value)?)
124            }
125        }
126
127        impl ::poem_openapi::types::ToJSON for $ty {
128            fn to_json(&self) -> Option<serde_json::Value> {
129                serde_json::to_value(self).ok()
130            }
131        }
132
133        impl ::poem_openapi::types::ToHeader for $ty {
134            fn to_header(&self) -> Option<::poem::http::HeaderValue> {
135                let string = serde_json::to_value(self).ok()?.to_string();
136                ::poem::http::HeaderValue::from_str(&string).ok()
137            }
138        }
139    };
140}
141
142/// This macro implements the traits necessary for using a type as a parameter
143/// in a poem-openapi endpoint handler, specifically as an argument like Path<T>.
144/// A type must impl FromStr for this to work, hence why it is a seperate macro.
145#[macro_export]
146macro_rules! impl_poem_parameter {
147    ($($ty:ty),*) => {
148        $(
149        impl ::poem_openapi::types::ParseFromParameter for $ty {
150            fn parse_from_parameter(value: &str) -> ::poem_openapi::types::ParseResult<Self> {
151                $crate::percent_encoding::percent_decode_str(value)
152                    .decode_utf8()
153                    .map_err(::poem_openapi::types::ParseError::custom)?
154                    .parse()
155                    .map_err(::poem_openapi::types::ParseError::custom)
156            }
157        }
158
159        #[async_trait::async_trait]
160        impl ::poem_openapi::types::ParseFromMultipartField for $ty {
161            async fn parse_from_multipart(field: Option<::poem::web::Field>) -> ::poem_openapi::types::ParseResult<Self> {
162                match field {
163                    Some(field) => Ok(field.text().await?.parse()?),
164                    None => Err(::poem_openapi::types::ParseError::expected_input()),
165                }
166            }
167        }
168
169        )*
170    };
171}
172
173mod test {
174    #[allow(unused_imports)]
175    use poem_openapi::types::ParseFromParameter;
176    use serde::{Deserialize, Serialize};
177    use std::str::FromStr;
178
179    #[derive(Debug, Deserialize, Serialize)]
180    struct This {
181        value: String,
182    }
183
184    #[derive(Debug, Deserialize, Serialize)]
185    struct That(pub String);
186
187    impl FromStr for That {
188        type Err = String;
189
190        fn from_str(s: &str) -> Result<Self, Self::Err> {
191            Ok(That(s.to_string()))
192        }
193    }
194
195    #[test]
196    fn test() {
197        impl_poem_type!(This, "string", ());
198
199        impl_poem_type!(That, "string", ());
200        impl_poem_parameter!(That);
201
202        assert_eq!(
203            That::parse_from_parameter("0x1::coin::CoinStore::%3C0x1::aptos_coin::AptosCoin%3E")
204                .unwrap()
205                .0,
206            "0x1::coin::CoinStore::<0x1::aptos_coin::AptosCoin>".to_string(),
207        );
208    }
209}