elif_core/specs/
spec.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Resource specification for code generation and API definition
5#[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    /// Create a resource spec from YAML string
30    pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
31        serde_yaml::from_str(yaml)
32    }
33
34    /// Convert resource spec to YAML string
35    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
36        serde_yaml::to_string(self)
37    }
38
39    /// Create a resource spec from JSON string
40    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
41        serde_json::from_str(json)
42    }
43
44    /// Convert resource spec to JSON string
45    pub fn to_json(&self) -> Result<String, serde_json::Error> {
46        serde_json::to_string_pretty(self)
47    }
48
49    /// Get the table name for this resource
50    pub fn table_name(&self) -> &str {
51        &self.storage.table
52    }
53
54    /// Get the primary key field specification
55    pub fn primary_key(&self) -> Option<&FieldSpec> {
56        self.storage.fields.iter().find(|f| f.pk)
57    }
58
59    /// Get all required fields
60    pub fn required_fields(&self) -> Vec<&FieldSpec> {
61        self.storage.fields.iter().filter(|f| f.required).collect()
62    }
63
64    /// Get all indexed fields
65    pub fn indexed_fields(&self) -> Vec<&FieldSpec> {
66        self.storage.fields.iter().filter(|f| f.index).collect()
67    }
68
69    /// Check if resource has soft delete enabled
70    pub fn has_soft_delete(&self) -> bool {
71        self.storage.soft_delete
72    }
73
74    /// Check if resource has timestamps enabled
75    pub fn has_timestamps(&self) -> bool {
76        self.storage.timestamps
77    }
78}
79
80/// Storage specification for database schema
81#[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/// Field specification for database columns
92#[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    /// Check if field is primary key
109    pub fn is_primary_key(&self) -> bool {
110        self.pk
111    }
112
113    /// Check if field is required
114    pub fn is_required(&self) -> bool {
115        self.required
116    }
117
118    /// Check if field is indexed
119    pub fn is_indexed(&self) -> bool {
120        self.index
121    }
122
123    /// Check if field has a default value
124    pub fn has_default(&self) -> bool {
125        self.default.is_some()
126    }
127}
128
129/// Validation rule for fields
130#[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/// Index specification
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct IndexSpec {
140    pub name: String,
141    pub fields: Vec<String>,
142}
143
144/// Unique constraint specification
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct UniqueSpec {
147    pub name: String,
148    pub fields: Vec<String>,
149}
150
151/// Relation specification for foreign keys
152#[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    /// Check if relation is one-to-one
161    pub fn is_one_to_one(&self) -> bool {
162        self.relation_type == "one_to_one" || self.relation_type == "1:1"
163    }
164
165    /// Check if relation is one-to-many
166    pub fn is_one_to_many(&self) -> bool {
167        self.relation_type == "one_to_many" || self.relation_type == "1:many"
168    }
169
170    /// Check if relation is many-to-many
171    pub fn is_many_to_many(&self) -> bool {
172        self.relation_type == "many_to_many" || self.relation_type == "many:many"
173    }
174}
175
176/// API specification for endpoints
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ApiSpec {
179    pub operations: Vec<OperationSpec>,
180}
181
182/// Operation specification for API endpoints
183#[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    /// Check if operation supports paging
196    pub fn supports_paging(&self) -> bool {
197        self.paging.is_some()
198    }
199
200    /// Check if operation supports filtering
201    pub fn supports_filtering(&self) -> bool {
202        self.filter.as_ref().is_some_and(|f| !f.is_empty())
203    }
204
205    /// Check if operation supports searching
206    pub fn supports_searching(&self) -> bool {
207        self.search_by.as_ref().is_some_and(|s| !s.is_empty())
208    }
209
210    /// Check if operation supports ordering
211    pub fn supports_ordering(&self) -> bool {
212        self.order_by.as_ref().is_some_and(|o| !o.is_empty())
213    }
214}
215
216/// Policy specification for access control
217#[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/// Validation specification
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct ValidateSpec {
227    #[serde(default)]
228    pub constraints: Vec<ConstraintSpec>,
229}
230
231/// Constraint specification for validation
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ConstraintSpec {
234    pub rule: String,
235    pub code: String,
236    pub hint: String,
237}
238
239/// Event specification for event handling
240#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct EventSpec {
242    #[serde(default)]
243    pub emit: Vec<String>,
244}
245
246// Helper functions for serde defaults
247fn 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}