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}