1use serde::Serialize;
2use std::collections::BTreeMap;
3
4use crate::adapters::DatabaseAdapter;
5use crate::plugin::AuthPlugin;
6use crate::types::HttpMethod;
7
8#[derive(Debug, Serialize)]
14pub struct OpenApiSpec {
15 pub openapi: String,
16 pub info: OpenApiInfo,
17 pub paths: BTreeMap<String, BTreeMap<String, OpenApiOperation>>,
18}
19
20#[derive(Debug, Serialize)]
21pub struct OpenApiInfo {
22 pub title: String,
23 pub version: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub description: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct OpenApiOperation {
30 #[serde(rename = "operationId")]
31 pub operation_id: String,
32 pub summary: String,
33 pub tags: Vec<String>,
34 pub responses: BTreeMap<String, OpenApiResponse>,
35}
36
37#[derive(Debug, Clone, Serialize)]
38pub struct OpenApiResponse {
39 pub description: String,
40}
41
42pub struct OpenApiBuilder {
44 title: String,
45 version: String,
46 description: Option<String>,
47 paths: BTreeMap<String, BTreeMap<String, OpenApiOperation>>,
48}
49
50impl OpenApiBuilder {
51 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
52 Self {
53 title: title.into(),
54 version: version.into(),
55 description: None,
56 paths: BTreeMap::new(),
57 }
58 }
59
60 pub fn description(mut self, desc: impl Into<String>) -> Self {
61 self.description = Some(desc.into());
62 self
63 }
64
65 pub fn route(mut self, method: &HttpMethod, path: &str, operation_id: &str, tag: &str) -> Self {
67 let method_str = match method {
68 HttpMethod::Get => "get",
69 HttpMethod::Post => "post",
70 HttpMethod::Put => "put",
71 HttpMethod::Delete => "delete",
72 HttpMethod::Patch => "patch",
73 HttpMethod::Options => "options",
74 HttpMethod::Head => "head",
75 };
76
77 let operation = OpenApiOperation {
78 operation_id: operation_id.to_string(),
79 summary: operation_id.replace('_', " "),
80 tags: vec![tag.to_string()],
81 responses: {
82 let mut r = BTreeMap::new();
83 r.insert(
84 "200".to_string(),
85 OpenApiResponse {
86 description: "Successful response".to_string(),
87 },
88 );
89 r
90 },
91 };
92
93 self.paths
94 .entry(path.to_string())
95 .or_default()
96 .insert(method_str.to_string(), operation);
97 self
98 }
99
100 pub fn plugin<DB: DatabaseAdapter>(mut self, plugin: &dyn AuthPlugin<DB>) -> Self {
102 let tag = plugin.name();
103 for route in plugin.routes() {
104 self = self.route(&route.method, &route.path, &route.operation_id, tag);
105 }
106 self
107 }
108
109 pub fn core_routes(self) -> Self {
111 self.route(&HttpMethod::Get, "/ok", "ok", "core")
112 .route(&HttpMethod::Get, "/error", "error", "core")
113 .route(&HttpMethod::Post, "/update-user", "update_user", "core")
114 .route(&HttpMethod::Post, "/delete-user", "delete_user", "core")
115 .route(&HttpMethod::Post, "/change-email", "change_email", "core")
116 .route(
117 &HttpMethod::Get,
118 "/delete-user/callback",
119 "delete_user_callback",
120 "core",
121 )
122 }
123
124 pub fn build(self) -> OpenApiSpec {
126 OpenApiSpec {
127 openapi: "3.1.0".to_string(),
128 info: OpenApiInfo {
129 title: self.title,
130 version: self.version,
131 description: self.description,
132 },
133 paths: self.paths,
134 }
135 }
136}
137
138impl OpenApiSpec {
139 pub fn to_json(&self) -> serde_json::Result<String> {
141 serde_json::to_string_pretty(self)
142 }
143
144 pub fn to_value(&self) -> serde_json::Result<serde_json::Value> {
146 serde_json::to_value(self)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_builder_core_routes() {
156 let spec = OpenApiBuilder::new("Better Auth", "0.1.0")
157 .description("Authentication API")
158 .core_routes()
159 .build();
160
161 assert_eq!(spec.openapi, "3.1.0");
162 assert_eq!(spec.info.title, "Better Auth");
163 assert!(spec.paths.contains_key("/ok"));
164 assert!(spec.paths.contains_key("/error"));
165 assert!(spec.paths.contains_key("/update-user"));
166 assert!(spec.paths.contains_key("/delete-user"));
167
168 let ok_path = &spec.paths["/ok"];
170 assert!(ok_path.contains_key("get"));
171 assert_eq!(ok_path["get"].operation_id, "ok");
172 }
173
174 #[test]
175 fn test_builder_custom_route() {
176 let spec = OpenApiBuilder::new("Test", "1.0.0")
177 .route(
178 &HttpMethod::Post,
179 "/sign-in/email",
180 "sign_in_email",
181 "email-password",
182 )
183 .build();
184
185 let path = &spec.paths["/sign-in/email"];
186 assert!(path.contains_key("post"));
187 assert_eq!(path["post"].tags, vec!["email-password"]);
188 }
189
190 #[test]
191 fn test_spec_to_json() {
192 let spec = OpenApiBuilder::new("Test", "1.0.0").core_routes().build();
193
194 let json = spec.to_json().unwrap();
195 assert!(json.contains("\"openapi\": \"3.1.0\""));
196 assert!(json.contains("\"/ok\""));
197 }
198
199 #[test]
200 fn test_spec_to_value() {
201 let spec = OpenApiBuilder::new("Test", "1.0.0").core_routes().build();
202
203 let value = spec.to_value().unwrap();
204 assert_eq!(value["openapi"], "3.1.0");
205 assert!(value["paths"]["/ok"]["get"]["operationId"].is_string());
206 }
207}