1use crate::{AuthRoute, HttpMethod};
3use indexmap::IndexMap;
4use serde_json::{Value, json};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct OpenApiField {
9 pub name: String,
10 pub schema: Value,
11 pub required: bool,
12 pub input: bool,
13 pub returned: bool,
14 pub has_default: bool,
16}
17impl OpenApiField {
18 #[must_use]
19 pub fn new(name: impl Into<String>, schema: Value, required: bool) -> Self {
20 Self {
21 name: name.into(),
22 has_default: schema.get("default").is_some(),
23 schema,
24 required,
25 input: true,
26 returned: true,
27 }
28 }
29 fn from_policy(name: &str, policy: &crate::field_policy::FieldConfig) -> Self {
30 let mut schema = policy.schema.clone();
31 if let Some(object) = schema.as_object_mut() {
32 _ = object.remove("default");
33 if let Some(crate::field_policy::FieldDefault::Value(value)) = &policy.default {
34 _ = object.insert(
35 "default".into(),
36 value.to_json_value().unwrap_or(Value::Null),
37 );
38 }
39 }
40 Self {
41 name: name.into(),
42 schema,
43 required: policy.required,
44 input: policy.input,
45 returned: policy.returned,
46 has_default: policy.default.is_some(),
47 }
48 }
49
50 #[must_use]
51 pub const fn read_only(mut self) -> Self {
52 self.input = false;
53 self
54 }
55 #[must_use]
56 pub const fn hidden(mut self) -> Self {
57 self.returned = false;
58 self
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct OpenApiModel {
65 pub name: String,
66 pub fields: Vec<OpenApiField>,
67}
68impl OpenApiModel {
69 #[must_use]
70 pub fn new(name: impl Into<String>, fields: Vec<OpenApiField>) -> Self {
71 Self {
72 name: name.into(),
73 fields,
74 }
75 }
76 pub(crate) fn to_schema(&self) -> Value {
77 let mut properties = serde_json::Map::new();
78 _ = properties.insert("id".into(), json!({"type":"string","readOnly":true}));
79 let mut required = vec!["id".to_owned()];
80 for field in &self.fields {
81 let mut schema = field.schema.clone();
82 if !field.input
83 && let Some(object) = schema.as_object_mut()
84 {
85 _ = object.insert("readOnly".into(), json!(true));
86 }
87 _ = properties.insert(field.name.clone(), schema);
88 if field.required && field.returned && !required.contains(&field.name) {
89 required.push(field.name.clone());
90 }
91 }
92 json!({"type":"object","properties":properties,"required":required})
93 }
94}
95
96#[derive(Debug, Clone, Default)]
98pub struct OpenApiEndpoint {
99 pub owner: Option<String>,
101 pub core: Option<bool>,
103 pub document_path: Option<String>,
105 pub operation_id: Option<String>,
106 pub description: Option<String>,
107 pub tags: Option<Vec<String>>,
108 pub parameters: Vec<Value>,
109 pub request_body: Option<Value>,
110 pub responses: IndexMap<String, Value>,
111 pub server_only: bool,
112 pub native_extension: bool,
114}
115
116#[derive(Debug, Clone, Default)]
118pub struct PluginOpenApiMetadata {
119 pub core: bool,
121 pub endpoints: Vec<(HttpMethod, String, OpenApiEndpoint)>,
122 pub models: Vec<OpenApiModel>,
123}
124impl PluginOpenApiMetadata {
125 #[must_use]
126 pub fn endpoint(
127 mut self,
128 method: HttpMethod,
129 path: impl Into<String>,
130 metadata: OpenApiEndpoint,
131 ) -> Self {
132 self.endpoints.push((method, path.into(), metadata));
133 self
134 }
135 #[must_use]
136 pub fn model(mut self, model: OpenApiModel) -> Self {
137 self.models.push(model);
138 self
139 }
140}
141
142#[derive(Debug, Clone)]
143pub(super) struct RegisteredEndpoint {
144 pub route: AuthRoute,
145 pub plugin: String,
146 pub core: bool,
147 pub metadata: OpenApiEndpoint,
148}
149
150#[derive(Debug, Clone, Default)]
152pub struct OpenApiRegistry {
153 pub(super) endpoints: Vec<RegisteredEndpoint>,
154 pub(crate) models: IndexMap<String, OpenApiModel>,
155 pub(crate) user_input_fields: IndexMap<String, OpenApiField>,
156 pub(crate) core_overrides: Vec<OpenApiModel>,
157}
158impl OpenApiRegistry {
159 #[must_use]
162 pub fn is_server_only(&self, plugin: &str, route: &AuthRoute) -> bool {
163 self.endpoints.iter().any(|endpoint| {
164 endpoint.plugin == plugin
165 && endpoint.route.method == route.method
166 && endpoint.route.path == route.path
167 && endpoint.metadata.server_only
168 })
169 }
170
171 #[must_use]
173 pub fn registered_routes(&self) -> Vec<AuthRoute> {
174 self.endpoints
175 .iter()
176 .map(|endpoint| endpoint.route.clone())
177 .collect()
178 }
179
180 #[must_use]
184 pub fn configured(mut models: Vec<OpenApiModel>, config: &crate::AuthConfig) -> Self {
185 for (model_name, fields) in [
186 ("User", &config.user.additional_fields),
187 ("Session", &config.session.additional_fields),
188 ("Account", &config.account.additional_fields),
189 ] {
190 if let Some(model) = models.iter_mut().find(|model| model.name == model_name) {
191 for (name, policy) in fields {
192 let field = OpenApiField::from_policy(name, policy);
193 if let Some(current) = model.fields.iter_mut().find(|field| field.name == *name)
194 {
195 *current = field;
196 } else {
197 model.fields.push(field);
198 }
199 }
200 }
201 }
202 Self::new(models)
203 }
204
205 #[must_use]
206 pub fn new(models: Vec<OpenApiModel>) -> Self {
207 let mut registry = Self::default();
208 let defaults = super::annotations::core_models();
209 for model in models {
210 if let Some(default) = defaults.iter().find(|default| default.name == model.name) {
211 let overrides = model
212 .fields
213 .iter()
214 .filter(|field| !default.fields.contains(field))
215 .cloned()
216 .collect::<Vec<_>>();
217 if model.name == "User" {
218 for field in &overrides {
219 _ = registry
220 .user_input_fields
221 .insert(field.name.clone(), field.clone());
222 }
223 }
224 if !overrides.is_empty() {
225 registry
226 .core_overrides
227 .push(OpenApiModel::new(model.name.clone(), overrides));
228 }
229 }
230 registry.merge_model(model);
231 }
232 registry
233 }
234 pub fn register(
235 &mut self,
236 plugin: &str,
237 routes: Vec<AuthRoute>,
238 metadata: PluginOpenApiMetadata,
239 ) {
240 for route in routes {
241 let annotation = metadata
242 .endpoints
243 .iter()
244 .find(|(method, path, _)| *method == route.method && *path == route.path)
245 .map(|(_, _, value)| value.clone())
246 .unwrap_or_default();
247 self.endpoints.push(RegisteredEndpoint {
248 core: annotation.core.unwrap_or(metadata.core),
249 plugin: annotation.owner.as_deref().unwrap_or(plugin).into(),
250 route,
251 metadata: annotation,
252 });
253 }
254 for model in metadata.models {
255 if model.name == "User" {
256 for field in &model.fields {
257 _ = self
258 .user_input_fields
259 .insert(field.name.clone(), field.clone());
260 }
261 }
262 self.merge_model(model);
263 }
264 }
265 pub fn register_fields(
268 &mut self,
269 model_name: &str,
270 fields: &crate::field_policy::FieldConfigs,
271 ) {
272 let fields = fields
273 .iter()
274 .map(|(name, policy)| OpenApiField::from_policy(name, policy))
275 .collect::<Vec<_>>();
276 if model_name == "User" {
277 for field in &fields {
278 _ = self
279 .user_input_fields
280 .insert(field.name.clone(), field.clone());
281 }
282 }
283 self.merge_model(OpenApiModel::new(model_name, fields));
284 }
285
286 fn merge_model(&mut self, model: OpenApiModel) {
287 if let Some(existing) = self.models.get_mut(&model.name) {
288 for field in model.fields {
289 if let Some(current) = existing
290 .fields
291 .iter_mut()
292 .find(|current| current.name == field.name)
293 {
294 *current = field;
295 } else {
296 existing.fields.push(field);
297 }
298 }
299 } else {
300 _ = self.models.insert(model.name.clone(), model);
301 }
302 }
303}