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