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}