Skip to main content

reinhardt_views/viewsets/
schema_metadata.rs

1//! Schema generation metadata for ViewSets
2//!
3//! Provides enhanced OpenAPI schema generation for ViewSets including
4//! request/response schemas, parameter descriptions, and action documentation.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Schema metadata for a field
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct FieldSchema {
12	/// Field type (string, integer, boolean, etc.)
13	pub field_type: String,
14	/// Field description
15	pub description: Option<String>,
16	/// Whether this field is required
17	pub required: bool,
18	/// Example value
19	pub example: Option<serde_json::Value>,
20	/// Format (e.g., "email", "date-time", "uuid")
21	pub format: Option<String>,
22	/// Minimum value (for numbers)
23	pub minimum: Option<f64>,
24	/// Maximum value (for numbers)
25	pub maximum: Option<f64>,
26	/// Minimum length (for strings/arrays)
27	pub min_length: Option<usize>,
28	/// Maximum length (for strings/arrays)
29	pub max_length: Option<usize>,
30	/// Pattern (regex for strings)
31	pub pattern: Option<String>,
32	/// Enum values
33	pub enum_values: Option<Vec<serde_json::Value>>,
34	/// Items schema (for arrays)
35	pub items: Option<Box<FieldSchema>>,
36	/// Properties (for objects)
37	pub properties: Option<HashMap<String, FieldSchema>>,
38}
39
40impl FieldSchema {
41	/// Create a string field schema
42	///
43	/// # Examples
44	///
45	/// ```
46	/// use reinhardt_views::viewsets::FieldSchema;
47	///
48	/// let schema = FieldSchema::string()
49	///     .with_description("User's email address")
50	///     .with_format("email")
51	///     .required();
52	/// assert_eq!(schema.field_type, "string");
53	/// assert!(schema.required);
54	/// ```
55	pub fn string() -> Self {
56		Self {
57			field_type: "string".to_string(),
58			description: None,
59			required: false,
60			example: None,
61			format: None,
62			minimum: None,
63			maximum: None,
64			min_length: None,
65			max_length: None,
66			pattern: None,
67			enum_values: None,
68			items: None,
69			properties: None,
70		}
71	}
72
73	/// Create an integer field schema
74	pub fn integer() -> Self {
75		Self {
76			field_type: "integer".to_string(),
77			description: None,
78			required: false,
79			example: None,
80			format: None,
81			minimum: None,
82			maximum: None,
83			min_length: None,
84			max_length: None,
85			pattern: None,
86			enum_values: None,
87			items: None,
88			properties: None,
89		}
90	}
91
92	/// Create a number field schema
93	pub fn number() -> Self {
94		Self {
95			field_type: "number".to_string(),
96			description: None,
97			required: false,
98			example: None,
99			format: None,
100			minimum: None,
101			maximum: None,
102			min_length: None,
103			max_length: None,
104			pattern: None,
105			enum_values: None,
106			items: None,
107			properties: None,
108		}
109	}
110
111	/// Create a boolean field schema
112	pub fn boolean() -> Self {
113		Self {
114			field_type: "boolean".to_string(),
115			description: None,
116			required: false,
117			example: None,
118			format: None,
119			minimum: None,
120			maximum: None,
121			min_length: None,
122			max_length: None,
123			pattern: None,
124			enum_values: None,
125			items: None,
126			properties: None,
127		}
128	}
129
130	/// Create an array field schema
131	pub fn array(items: FieldSchema) -> Self {
132		Self {
133			field_type: "array".to_string(),
134			description: None,
135			required: false,
136			example: None,
137			format: None,
138			minimum: None,
139			maximum: None,
140			min_length: None,
141			max_length: None,
142			pattern: None,
143			enum_values: None,
144			items: Some(Box::new(items)),
145			properties: None,
146		}
147	}
148
149	/// Create an object field schema
150	pub fn object() -> Self {
151		Self {
152			field_type: "object".to_string(),
153			description: None,
154			required: false,
155			example: None,
156			format: None,
157			minimum: None,
158			maximum: None,
159			min_length: None,
160			max_length: None,
161			pattern: None,
162			enum_values: None,
163			items: None,
164			properties: Some(HashMap::new()),
165		}
166	}
167
168	/// Mark field as required
169	pub fn required(mut self) -> Self {
170		self.required = true;
171		self
172	}
173
174	/// Add description
175	pub fn with_description(mut self, description: impl Into<String>) -> Self {
176		self.description = Some(description.into());
177		self
178	}
179
180	/// Add example value
181	pub fn with_example(mut self, example: serde_json::Value) -> Self {
182		self.example = Some(example);
183		self
184	}
185
186	/// Add format
187	pub fn with_format(mut self, format: impl Into<String>) -> Self {
188		self.format = Some(format.into());
189		self
190	}
191
192	/// Add minimum value
193	pub fn with_minimum(mut self, min: f64) -> Self {
194		self.minimum = Some(min);
195		self
196	}
197
198	/// Add maximum value
199	pub fn with_maximum(mut self, max: f64) -> Self {
200		self.maximum = Some(max);
201		self
202	}
203
204	/// Add minimum length
205	pub fn with_min_length(mut self, min: usize) -> Self {
206		self.min_length = Some(min);
207		self
208	}
209
210	/// Add maximum length
211	pub fn with_max_length(mut self, max: usize) -> Self {
212		self.max_length = Some(max);
213		self
214	}
215
216	/// Add pattern
217	pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
218		self.pattern = Some(pattern.into());
219		self
220	}
221
222	/// Add enum values
223	pub fn with_enum(mut self, values: Vec<serde_json::Value>) -> Self {
224		self.enum_values = Some(values);
225		self
226	}
227
228	/// Add property to object schema
229	pub fn add_property(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
230		if let Some(ref mut props) = self.properties {
231			props.insert(name.into(), schema);
232		}
233		self
234	}
235}
236
237/// Request schema metadata
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct RequestSchema {
240	/// Content type (e.g., "application/json")
241	pub content_type: String,
242	/// Schema for the request body
243	pub schema: ModelSchema,
244	/// Examples
245	pub examples: Option<HashMap<String, serde_json::Value>>,
246}
247
248/// Response schema metadata
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ResponseSchema {
251	/// HTTP status code
252	pub status_code: u16,
253	/// Response description
254	pub description: String,
255	/// Content type (e.g., "application/json")
256	pub content_type: String,
257	/// Schema for the response body
258	pub schema: ModelSchema,
259	/// Examples
260	pub examples: Option<HashMap<String, serde_json::Value>>,
261}
262
263/// Model schema metadata
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct ModelSchema {
266	/// Model name
267	pub name: String,
268	/// Model description
269	pub description: Option<String>,
270	/// Fields
271	pub fields: HashMap<String, FieldSchema>,
272}
273
274impl ModelSchema {
275	/// Create a new model schema
276	///
277	/// # Examples
278	///
279	/// ```
280	/// use reinhardt_views::viewsets::{ModelSchema, FieldSchema};
281	///
282	/// let schema = ModelSchema::new("User")
283	///     .with_description("User model")
284	///     .add_field("id", FieldSchema::integer().required())
285	///     .add_field("name", FieldSchema::string().required())
286	///     .add_field("email", FieldSchema::string().with_format("email"));
287	/// assert_eq!(schema.fields.len(), 3);
288	/// ```
289	pub fn new(name: impl Into<String>) -> Self {
290		Self {
291			name: name.into(),
292			description: None,
293			fields: HashMap::new(),
294		}
295	}
296
297	/// Add description
298	pub fn with_description(mut self, description: impl Into<String>) -> Self {
299		self.description = Some(description.into());
300		self
301	}
302
303	/// Add a field
304	pub fn add_field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
305		self.fields.insert(name.into(), schema);
306		self
307	}
308}
309
310/// ViewSet schema metadata
311#[derive(Debug, Clone)]
312pub struct ViewSetSchema {
313	/// ViewSet name
314	pub name: String,
315	/// ViewSet description
316	pub description: Option<String>,
317	/// Model schema (for model-based viewsets)
318	pub model_schema: Option<ModelSchema>,
319	/// Custom request schemas per action
320	pub request_schemas: HashMap<String, RequestSchema>,
321	/// Custom response schemas per action
322	pub response_schemas: HashMap<String, Vec<ResponseSchema>>,
323	/// Tags for OpenAPI
324	pub tags: Vec<String>,
325}
326
327impl ViewSetSchema {
328	/// Create a new ViewSet schema
329	///
330	/// # Examples
331	///
332	/// ```
333	/// use reinhardt_views::viewsets::ViewSetSchema;
334	///
335	/// let schema = ViewSetSchema::new("UserViewSet")
336	///     .with_description("User management endpoints")
337	///     .with_tags(vec!["users".to_string()]);
338	/// assert_eq!(schema.name, "UserViewSet");
339	/// assert_eq!(schema.tags.len(), 1);
340	/// ```
341	pub fn new(name: impl Into<String>) -> Self {
342		Self {
343			name: name.into(),
344			description: None,
345			model_schema: None,
346			request_schemas: HashMap::new(),
347			response_schemas: HashMap::new(),
348			tags: Vec::new(),
349		}
350	}
351
352	/// Add description
353	pub fn with_description(mut self, description: impl Into<String>) -> Self {
354		self.description = Some(description.into());
355		self
356	}
357
358	/// Set model schema
359	pub fn with_model_schema(mut self, schema: ModelSchema) -> Self {
360		self.model_schema = Some(schema);
361		self
362	}
363
364	/// Add request schema for an action
365	pub fn add_request_schema(mut self, action: impl Into<String>, schema: RequestSchema) -> Self {
366		self.request_schemas.insert(action.into(), schema);
367		self
368	}
369
370	/// Add response schema for an action
371	pub fn add_response_schema(
372		mut self,
373		action: impl Into<String>,
374		schema: ResponseSchema,
375	) -> Self {
376		self.response_schemas
377			.entry(action.into())
378			.or_default()
379			.push(schema);
380		self
381	}
382
383	/// Set tags
384	pub fn with_tags(mut self, tags: Vec<String>) -> Self {
385		self.tags = tags;
386		self
387	}
388
389	/// Add tag
390	pub fn add_tag(mut self, tag: impl Into<String>) -> Self {
391		self.tags.push(tag.into());
392		self
393	}
394}
395
396#[cfg(test)]
397mod tests {
398	use super::*;
399
400	#[test]
401	fn test_field_schema_string() {
402		let schema = FieldSchema::string()
403			.with_description("Test field")
404			.with_format("email")
405			.required();
406
407		assert_eq!(schema.field_type, "string");
408		assert_eq!(schema.description, Some("Test field".to_string()));
409		assert_eq!(schema.format, Some("email".to_string()));
410		assert!(schema.required);
411	}
412
413	#[test]
414	fn test_field_schema_integer() {
415		let schema = FieldSchema::integer()
416			.with_minimum(0.0)
417			.with_maximum(100.0)
418			.required();
419
420		assert_eq!(schema.field_type, "integer");
421		assert_eq!(schema.minimum, Some(0.0));
422		assert_eq!(schema.maximum, Some(100.0));
423		assert!(schema.required);
424	}
425
426	#[test]
427	fn test_field_schema_array() {
428		let items = FieldSchema::string();
429		let schema = FieldSchema::array(items)
430			.with_min_length(1)
431			.with_max_length(10);
432
433		assert_eq!(schema.field_type, "array");
434		assert!(schema.items.is_some());
435		assert_eq!(schema.min_length, Some(1));
436		assert_eq!(schema.max_length, Some(10));
437	}
438
439	#[test]
440	fn test_field_schema_object() {
441		let schema = FieldSchema::object()
442			.add_property("id", FieldSchema::integer().required())
443			.add_property("name", FieldSchema::string().required());
444
445		assert_eq!(schema.field_type, "object");
446		assert!(schema.properties.is_some());
447		let props = schema.properties.unwrap();
448		assert_eq!(props.len(), 2);
449		assert!(props.contains_key("id"));
450		assert!(props.contains_key("name"));
451	}
452
453	#[test]
454	fn test_model_schema() {
455		let schema = ModelSchema::new("User")
456			.with_description("User model")
457			.add_field("id", FieldSchema::integer().required())
458			.add_field("name", FieldSchema::string().required())
459			.add_field("email", FieldSchema::string().with_format("email"));
460
461		assert_eq!(schema.name, "User");
462		assert_eq!(schema.description, Some("User model".to_string()));
463		assert_eq!(schema.fields.len(), 3);
464		assert!(schema.fields.get("id").unwrap().required);
465		assert!(schema.fields.get("name").unwrap().required);
466		assert!(!schema.fields.get("email").unwrap().required);
467	}
468
469	#[test]
470	fn test_viewset_schema() {
471		let model_schema = ModelSchema::new("User")
472			.add_field("id", FieldSchema::integer().required())
473			.add_field("name", FieldSchema::string().required());
474
475		let schema = ViewSetSchema::new("UserViewSet")
476			.with_description("User management")
477			.with_model_schema(model_schema)
478			.with_tags(vec!["users".to_string()])
479			.add_tag("auth".to_string());
480
481		assert_eq!(schema.name, "UserViewSet");
482		assert_eq!(schema.description, Some("User management".to_string()));
483		assert!(schema.model_schema.is_some());
484		assert_eq!(schema.tags.len(), 2);
485	}
486
487	#[test]
488	fn test_request_schema() {
489		let model_schema = ModelSchema::new("CreateUser")
490			.add_field("name", FieldSchema::string().required())
491			.add_field(
492				"email",
493				FieldSchema::string().with_format("email").required(),
494			);
495
496		let request = RequestSchema {
497			content_type: "application/json".to_string(),
498			schema: model_schema,
499			examples: None,
500		};
501
502		assert_eq!(request.content_type, "application/json");
503		assert_eq!(request.schema.name, "CreateUser");
504		assert_eq!(request.schema.fields.len(), 2);
505	}
506
507	#[test]
508	fn test_response_schema() {
509		let model_schema = ModelSchema::new("User")
510			.add_field("id", FieldSchema::integer().required())
511			.add_field("name", FieldSchema::string().required());
512
513		let response = ResponseSchema {
514			status_code: 200,
515			description: "Success".to_string(),
516			content_type: "application/json".to_string(),
517			schema: model_schema,
518			examples: None,
519		};
520
521		assert_eq!(response.status_code, 200);
522		assert_eq!(response.description, "Success");
523		assert_eq!(response.schema.name, "User");
524	}
525
526	#[test]
527	fn test_field_schema_enum() {
528		let schema = FieldSchema::string().with_enum(vec![
529			serde_json::json!("active"),
530			serde_json::json!("inactive"),
531			serde_json::json!("pending"),
532		]);
533
534		assert!(schema.enum_values.is_some());
535		assert_eq!(schema.enum_values.unwrap().len(), 3);
536	}
537
538	#[test]
539	fn test_field_schema_pattern() {
540		let schema = FieldSchema::string()
541			.with_pattern("^[a-zA-Z0-9]+$")
542			.with_min_length(3)
543			.with_max_length(20);
544
545		assert_eq!(schema.pattern, Some("^[a-zA-Z0-9]+$".to_string()));
546		assert_eq!(schema.min_length, Some(3));
547		assert_eq!(schema.max_length, Some(20));
548	}
549}