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 drop(object.remove("default"));
33 if let Some(crate::field_policy::FieldDefault::Value(value)) = &policy.default {
34 drop(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 drop(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 drop(object.insert("readOnly".into(), json!(true)));
86 }
87 drop(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 drop(
220 registry
221 .user_input_fields
222 .insert(field.name.clone(), field.clone()),
223 );
224 }
225 }
226 if !overrides.is_empty() {
227 registry
228 .core_overrides
229 .push(OpenApiModel::new(model.name.clone(), overrides));
230 }
231 }
232 registry.merge_model(model);
233 }
234 registry
235 }
236 pub fn register(
237 &mut self,
238 plugin: &str,
239 routes: Vec<AuthRoute>,
240 metadata: PluginOpenApiMetadata,
241 ) {
242 for route in routes {
243 let annotation = metadata
244 .endpoints
245 .iter()
246 .find(|(method, path, _)| *method == route.method && *path == route.path)
247 .map(|(_, _, value)| value.clone())
248 .unwrap_or_default();
249 self.endpoints.push(RegisteredEndpoint {
250 core: annotation.core.unwrap_or(metadata.core),
251 plugin: annotation.owner.as_deref().unwrap_or(plugin).into(),
252 route,
253 metadata: annotation,
254 });
255 }
256 for model in metadata.models {
257 if model.name == "User" {
258 for field in &model.fields {
259 drop(
260 self.user_input_fields
261 .insert(field.name.clone(), field.clone()),
262 );
263 }
264 }
265 self.merge_model(model);
266 }
267 }
268 pub fn register_fields(
271 &mut self,
272 model_name: &str,
273 fields: &crate::field_policy::FieldConfigs,
274 ) {
275 let fields = fields
276 .iter()
277 .map(|(name, policy)| OpenApiField::from_policy(name, policy))
278 .collect::<Vec<_>>();
279 if model_name == "User" {
280 for field in &fields {
281 drop(
282 self.user_input_fields
283 .insert(field.name.clone(), field.clone()),
284 );
285 }
286 }
287 self.merge_model(OpenApiModel::new(model_name, fields));
288 }
289
290 fn merge_model(&mut self, model: OpenApiModel) {
291 if let Some(existing) = self.models.get_mut(&model.name) {
292 for field in model.fields {
293 if let Some(current) = existing
294 .fields
295 .iter_mut()
296 .find(|current| current.name == field.name)
297 {
298 *current = field;
299 } else {
300 existing.fields.push(field);
301 }
302 }
303 } else {
304 drop(self.models.insert(model.name.clone(), model));
305 }
306 }
307}