1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ResourceSpec {
7 pub kind: String,
8 pub name: String,
9 pub route: String,
10 pub storage: StorageSpec,
11 #[serde(default)]
12 pub indexes: Vec<IndexSpec>,
13 #[serde(default)]
14 pub uniques: Vec<UniqueSpec>,
15 #[serde(default)]
16 pub relations: Vec<RelationSpec>,
17 pub api: ApiSpec,
18 #[serde(default)]
19 pub policy: PolicySpec,
20 #[serde(default)]
21 pub validate: ValidateSpec,
22 #[serde(default)]
23 pub examples: HashMap<String, serde_json::Value>,
24 #[serde(default)]
25 pub events: EventSpec,
26}
27
28impl ResourceSpec {
29 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
31 serde_yaml::from_str(yaml)
32 }
33
34 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
36 serde_yaml::to_string(self)
37 }
38
39 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
41 serde_json::from_str(json)
42 }
43
44 pub fn to_json(&self) -> Result<String, serde_json::Error> {
46 serde_json::to_string_pretty(self)
47 }
48
49 pub fn table_name(&self) -> &str {
51 &self.storage.table
52 }
53
54 pub fn primary_key(&self) -> Option<&FieldSpec> {
56 self.storage.fields.iter().find(|f| f.pk)
57 }
58
59 pub fn required_fields(&self) -> Vec<&FieldSpec> {
61 self.storage.fields.iter().filter(|f| f.required).collect()
62 }
63
64 pub fn indexed_fields(&self) -> Vec<&FieldSpec> {
66 self.storage.fields.iter().filter(|f| f.index).collect()
67 }
68
69 pub fn has_soft_delete(&self) -> bool {
71 self.storage.soft_delete
72 }
73
74 pub fn has_timestamps(&self) -> bool {
76 self.storage.timestamps
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct StorageSpec {
83 pub table: String,
84 #[serde(default)]
85 pub soft_delete: bool,
86 #[serde(default = "default_true")]
87 pub timestamps: bool,
88 pub fields: Vec<FieldSpec>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct FieldSpec {
94 pub name: String,
95 #[serde(rename = "type")]
96 pub field_type: String,
97 #[serde(default)]
98 pub pk: bool,
99 #[serde(default)]
100 pub required: bool,
101 #[serde(default)]
102 pub index: bool,
103 pub default: Option<String>,
104 pub validate: Option<ValidationRule>,
105}
106
107impl FieldSpec {
108 pub fn is_primary_key(&self) -> bool {
110 self.pk
111 }
112
113 pub fn is_required(&self) -> bool {
115 self.required
116 }
117
118 pub fn is_indexed(&self) -> bool {
120 self.index
121 }
122
123 pub fn has_default(&self) -> bool {
125 self.default.is_some()
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ValidationRule {
132 pub min: Option<i64>,
133 pub max: Option<i64>,
134 pub pattern: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct IndexSpec {
140 pub name: String,
141 pub fields: Vec<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct UniqueSpec {
147 pub name: String,
148 pub fields: Vec<String>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct RelationSpec {
154 pub name: String,
155 pub target: String,
156 pub relation_type: String,
157}
158
159impl RelationSpec {
160 pub fn is_one_to_one(&self) -> bool {
162 self.relation_type == "one_to_one" || self.relation_type == "1:1"
163 }
164
165 pub fn is_one_to_many(&self) -> bool {
167 self.relation_type == "one_to_many" || self.relation_type == "1:many"
168 }
169
170 pub fn is_many_to_many(&self) -> bool {
172 self.relation_type == "many_to_many" || self.relation_type == "many:many"
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ApiSpec {
179 pub operations: Vec<OperationSpec>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct OperationSpec {
185 pub op: String,
186 pub method: String,
187 pub path: String,
188 pub paging: Option<String>,
189 pub filter: Option<Vec<String>>,
190 pub search_by: Option<Vec<String>>,
191 pub order_by: Option<Vec<String>>,
192}
193
194impl OperationSpec {
195 pub fn supports_paging(&self) -> bool {
197 self.paging.is_some()
198 }
199
200 pub fn supports_filtering(&self) -> bool {
202 self.filter.as_ref().is_some_and(|f| !f.is_empty())
203 }
204
205 pub fn supports_searching(&self) -> bool {
207 self.search_by.as_ref().is_some_and(|s| !s.is_empty())
208 }
209
210 pub fn supports_ordering(&self) -> bool {
212 self.order_by.as_ref().is_some_and(|o| !o.is_empty())
213 }
214}
215
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct PolicySpec {
219 #[serde(default = "default_public")]
220 pub auth: String,
221 pub rate_limit: Option<String>,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct ValidateSpec {
227 #[serde(default)]
228 pub constraints: Vec<ConstraintSpec>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ConstraintSpec {
234 pub rule: String,
235 pub code: String,
236 pub hint: String,
237}
238
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct EventSpec {
242 #[serde(default)]
243 pub emit: Vec<String>,
244}
245
246fn default_true() -> bool {
248 true
249}
250
251fn default_public() -> String {
252 "public".to_string()
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_resource_spec_yaml() {
261 let yaml = r#"
262kind: Resource
263name: User
264route: /users
265storage:
266 table: users
267 fields:
268 - name: id
269 type: uuid
270 pk: true
271 - name: name
272 type: string
273 required: true
274api:
275 operations:
276 - op: list
277 method: GET
278 path: /
279"#;
280
281 let spec = ResourceSpec::from_yaml(yaml).unwrap();
282 assert_eq!(spec.name, "User");
283 assert_eq!(spec.storage.table, "users");
284 assert_eq!(spec.storage.fields.len(), 2);
285
286 let yaml_output = spec.to_yaml().unwrap();
287 assert!(yaml_output.contains("name: User"));
288 }
289
290 #[test]
291 fn test_field_spec_helpers() {
292 let field = FieldSpec {
293 name: "id".to_string(),
294 field_type: "uuid".to_string(),
295 pk: true,
296 required: true,
297 index: true,
298 default: Some("gen_random_uuid()".to_string()),
299 validate: None,
300 };
301
302 assert!(field.is_primary_key());
303 assert!(field.is_required());
304 assert!(field.is_indexed());
305 assert!(field.has_default());
306 }
307
308 #[test]
309 fn test_relation_spec_types() {
310 let relation = RelationSpec {
311 name: "posts".to_string(),
312 target: "Post".to_string(),
313 relation_type: "one_to_many".to_string(),
314 };
315
316 assert!(relation.is_one_to_many());
317 assert!(!relation.is_one_to_one());
318 assert!(!relation.is_many_to_many());
319 }
320}