Skip to main content

openauth_core/api/
openapi.rs

1use std::collections::BTreeMap;
2
3use http::Method;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7use super::endpoint::AsyncAuthEndpoint;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct OpenApiOperation {
11    pub operation_id: Option<String>,
12    pub description: Option<String>,
13    pub tags: Vec<String>,
14    pub parameters: Vec<Value>,
15    pub request_body: Option<Value>,
16    pub responses: BTreeMap<String, Value>,
17}
18
19impl OpenApiOperation {
20    pub fn new(operation_id: impl Into<String>) -> Self {
21        Self {
22            operation_id: Some(operation_id.into()),
23            description: None,
24            tags: Vec::new(),
25            parameters: Vec::new(),
26            request_body: None,
27            responses: BTreeMap::new(),
28        }
29    }
30
31    #[must_use]
32    pub fn description(mut self, description: impl Into<String>) -> Self {
33        self.description = Some(description.into());
34        self
35    }
36
37    #[must_use]
38    pub fn tag(mut self, tag: impl Into<String>) -> Self {
39        self.tags.push(tag.into());
40        self
41    }
42
43    #[must_use]
44    pub fn request_body(mut self, request_body: Value) -> Self {
45        self.request_body = Some(request_body);
46        self
47    }
48
49    #[must_use]
50    pub fn parameter(mut self, parameter: Value) -> Self {
51        self.parameters.push(parameter);
52        self
53    }
54
55    #[must_use]
56    pub fn response(mut self, status: impl Into<String>, response: Value) -> Self {
57        self.responses.insert(status.into(), response);
58        self
59    }
60}
61
62pub(super) fn openapi_operation_for_endpoint(endpoint: &AsyncAuthEndpoint) -> Value {
63    let operation = endpoint
64        .options
65        .openapi
66        .clone()
67        .unwrap_or_else(|| OpenApiOperation {
68            operation_id: endpoint.options.operation_id.clone(),
69            description: None,
70            tags: Vec::new(),
71            parameters: Vec::new(),
72            request_body: None,
73            responses: BTreeMap::new(),
74        });
75    let request_body = operation.request_body.or_else(|| {
76        endpoint
77            .options
78            .body_schema
79            .as_ref()
80            .map(|schema| {
81                json!({
82                    "required": true,
83                    "content": {
84                        "application/json": {
85                            "schema": schema.openapi_schema(),
86                        },
87                    },
88                })
89            })
90            .or_else(|| {
91                method_uses_request_body(&endpoint.method).then(|| {
92                    json!({
93                        "content": {
94                            "application/json": {
95                                "schema": {
96                                    "type": "object",
97                                    "properties": {},
98                                },
99                            },
100                        },
101                    })
102                })
103            })
104    });
105    let mut responses = default_openapi_responses();
106    for (status, response) in operation.responses {
107        responses.insert(status, response);
108    }
109    let mut tags = vec!["Default".to_owned()];
110    for tag in operation.tags {
111        if !tags.iter().any(|existing| existing == &tag) {
112            tags.push(tag);
113        }
114    }
115
116    let mut value = serde_json::Map::new();
117    value.insert(
118        "tags".to_owned(),
119        Value::Array(tags.into_iter().map(Value::String).collect()),
120    );
121    if let Some(description) = operation.description {
122        value.insert("description".to_owned(), Value::String(description));
123    }
124    if let Some(operation_id) = operation
125        .operation_id
126        .or_else(|| endpoint.options.operation_id.clone())
127    {
128        value.insert("operationId".to_owned(), Value::String(operation_id));
129    }
130    value.insert(
131        "security".to_owned(),
132        json!([
133            {
134                "bearerAuth": [],
135            },
136        ]),
137    );
138    value.insert("parameters".to_owned(), Value::Array(operation.parameters));
139    if let Some(request_body) = request_body {
140        value.insert("requestBody".to_owned(), request_body);
141    }
142    value.insert("responses".to_owned(), Value::Object(responses));
143    Value::Object(value)
144}
145
146fn method_uses_request_body(method: &Method) -> bool {
147    matches!(*method, Method::POST | Method::PATCH | Method::PUT)
148}
149
150pub(super) fn to_openapi_path(path: &str) -> String {
151    path.split('/')
152        .map(|part| {
153            part.strip_prefix(':')
154                .map(|name| format!("{{{name}}}"))
155                .unwrap_or_else(|| part.to_owned())
156        })
157        .collect::<Vec<_>>()
158        .join("/")
159}
160
161fn default_openapi_responses() -> serde_json::Map<String, Value> {
162    let mut responses = serde_json::Map::new();
163    responses.insert(
164        "400".to_owned(),
165        openapi_error_response(
166            "Bad Request. Usually due to missing parameters, or invalid parameters.",
167            true,
168        ),
169    );
170    responses.insert(
171        "401".to_owned(),
172        openapi_error_response(
173            "Unauthorized. Due to missing or invalid authentication.",
174            true,
175        ),
176    );
177    responses.insert(
178        "403".to_owned(),
179        openapi_error_response(
180            "Forbidden. You do not have permission to access this resource or to perform this action.",
181            false,
182        ),
183    );
184    responses.insert(
185        "404".to_owned(),
186        openapi_error_response("Not Found. The requested resource was not found.", false),
187    );
188    responses.insert(
189        "429".to_owned(),
190        openapi_error_response(
191            "Too Many Requests. You have exceeded the rate limit. Try again later.",
192            false,
193        ),
194    );
195    responses.insert(
196        "500".to_owned(),
197        openapi_error_response(
198            "Internal Server Error. This is a problem with the server that you cannot fix.",
199            false,
200        ),
201    );
202    responses
203}
204
205fn openapi_error_response(description: &str, require_message: bool) -> Value {
206    let required = require_message.then(|| json!(["message"]));
207    let mut schema = serde_json::Map::new();
208    schema.insert("type".to_owned(), Value::String("object".to_owned()));
209    schema.insert(
210        "properties".to_owned(),
211        json!({
212            "message": {
213                "type": "string",
214            },
215        }),
216    );
217    if let Some(required) = required {
218        schema.insert("required".to_owned(), required);
219    }
220    json!({
221        "content": {
222            "application/json": {
223                "schema": Value::Object(schema),
224            },
225        },
226        "description": description,
227    })
228}
229
230pub(super) fn openapi_model_schemas() -> Value {
231    json!({
232        "User": {
233            "type": "object",
234            "properties": {
235                "id": { "type": "string" },
236                "email": { "type": "string", "format": "email" },
237                "name": { "type": "string" },
238                "image": { "type": "string", "format": "uri", "nullable": true },
239                "emailVerified": { "type": "boolean" },
240                "createdAt": { "type": "string", "format": "date-time" },
241                "updatedAt": { "type": "string", "format": "date-time" },
242            },
243            "required": ["id", "email", "name", "emailVerified", "createdAt", "updatedAt"],
244        },
245        "Session": {
246            "type": "object",
247            "properties": {
248                "id": { "type": "string" },
249                "userId": { "type": "string" },
250                "expiresAt": { "type": "string", "format": "date-time" },
251                "token": { "type": "string" },
252                "ipAddress": { "type": "string", "nullable": true },
253                "userAgent": { "type": "string", "nullable": true },
254                "createdAt": { "type": "string", "format": "date-time" },
255                "updatedAt": { "type": "string", "format": "date-time" },
256            },
257            "required": ["id", "userId", "expiresAt", "token", "createdAt", "updatedAt"],
258        },
259        "Account": {
260            "type": "object",
261            "properties": {
262                "id": { "type": "string" },
263                "providerId": { "type": "string" },
264                "accountId": { "type": "string" },
265                "userId": { "type": "string" },
266                "accessToken": { "type": "string", "nullable": true },
267                "refreshToken": { "type": "string", "nullable": true },
268                "idToken": { "type": "string", "nullable": true },
269                "scope": { "type": "string", "nullable": true },
270                "password": { "type": "string", "nullable": true },
271                "createdAt": { "type": "string", "format": "date-time" },
272                "updatedAt": { "type": "string", "format": "date-time" },
273            },
274            "required": ["id", "providerId", "accountId", "userId", "createdAt", "updatedAt"],
275        },
276        "Verification": {
277            "type": "object",
278            "properties": {
279                "id": { "type": "string" },
280                "identifier": { "type": "string" },
281                "value": { "type": "string" },
282                "expiresAt": { "type": "string", "format": "date-time" },
283                "createdAt": { "type": "string", "format": "date-time" },
284                "updatedAt": { "type": "string", "format": "date-time" },
285            },
286            "required": ["id", "identifier", "value", "expiresAt", "createdAt", "updatedAt"],
287        },
288    })
289}