Skip to main content

shaperail_core/
resource.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::{EndpointSpec, FieldSchema, IndexSpec, RelationSpec};
5
6/// Complete definition of a Shaperail resource, parsed from a resource YAML file.
7///
8/// This is the central type that all codegen and runtime modules consume.
9///
10/// ```yaml
11/// resource: users
12/// version: 1
13/// schema:
14///   id: { type: uuid, primary: true, generated: true }
15///   email: { type: string, format: email, unique: true, required: true }
16/// ```
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct ResourceDefinition {
20    /// Snake_case plural name of the resource (e.g., "users").
21    pub resource: String,
22
23    /// Schema version number (starts at 1).
24    pub version: u32,
25
26    /// Field definitions, keyed by field name. Uses IndexMap to preserve declaration order.
27    pub schema: IndexMap<String, FieldSchema>,
28
29    /// Endpoint definitions, keyed by action name (e.g., "list", "create").
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub endpoints: Option<IndexMap<String, EndpointSpec>>,
32
33    /// Relationship definitions, keyed by relation name.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub relations: Option<IndexMap<String, RelationSpec>>,
36
37    /// Additional database indexes.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub indexes: Option<Vec<IndexSpec>>,
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::{
46        AuthRule, CacheSpec, EndpointSpec, FieldType, HttpMethod, PaginationStyle, RelationType,
47    };
48
49    fn sample_resource() -> ResourceDefinition {
50        let mut schema = IndexMap::new();
51        schema.insert(
52            "id".to_string(),
53            FieldSchema {
54                field_type: FieldType::Uuid,
55                primary: true,
56                generated: true,
57                required: false,
58                unique: false,
59                nullable: false,
60                reference: None,
61                min: None,
62                max: None,
63                format: None,
64                values: None,
65                default: None,
66                sensitive: false,
67                search: false,
68                items: None,
69            },
70        );
71        schema.insert(
72            "email".to_string(),
73            FieldSchema {
74                field_type: FieldType::String,
75                primary: false,
76                generated: false,
77                required: true,
78                unique: true,
79                nullable: false,
80                reference: None,
81                min: None,
82                max: None,
83                format: Some("email".to_string()),
84                values: None,
85                default: None,
86                sensitive: false,
87                search: false,
88                items: None,
89            },
90        );
91
92        let mut endpoints = IndexMap::new();
93        endpoints.insert(
94            "list".to_string(),
95            EndpointSpec {
96                method: HttpMethod::Get,
97                path: "/users".to_string(),
98                auth: Some(AuthRule::Roles(vec![
99                    "member".to_string(),
100                    "admin".to_string(),
101                ])),
102                input: None,
103                filters: Some(vec!["role".to_string()]),
104                search: Some(vec!["email".to_string()]),
105                pagination: Some(PaginationStyle::Cursor),
106                sort: None,
107                cache: Some(CacheSpec {
108                    ttl: 60,
109                    invalidate_on: None,
110                }),
111                hooks: None,
112                events: None,
113                jobs: None,
114                upload: None,
115                soft_delete: false,
116            },
117        );
118
119        let mut relations = IndexMap::new();
120        relations.insert(
121            "orders".to_string(),
122            RelationSpec {
123                resource: "orders".to_string(),
124                relation_type: RelationType::HasMany,
125                key: None,
126                foreign_key: Some("user_id".to_string()),
127            },
128        );
129
130        ResourceDefinition {
131            resource: "users".to_string(),
132            version: 1,
133            schema,
134            endpoints: Some(endpoints),
135            relations: Some(relations),
136            indexes: Some(vec![IndexSpec {
137                fields: vec!["created_at".to_string()],
138                unique: false,
139                order: Some("desc".to_string()),
140            }]),
141        }
142    }
143
144    #[test]
145    fn resource_definition_construction() {
146        let rd = sample_resource();
147        assert_eq!(rd.resource, "users");
148        assert_eq!(rd.version, 1);
149        assert_eq!(rd.schema.len(), 2);
150        assert!(rd.schema.contains_key("id"));
151        assert!(rd.schema.contains_key("email"));
152    }
153
154    #[test]
155    fn resource_definition_serde_roundtrip() {
156        let rd = sample_resource();
157        let json = serde_json::to_string_pretty(&rd).unwrap();
158        let back: ResourceDefinition = serde_json::from_str(&json).unwrap();
159        assert_eq!(rd, back);
160    }
161
162    #[test]
163    fn resource_definition_preserves_field_order() {
164        let rd = sample_resource();
165        let keys: Vec<&String> = rd.schema.keys().collect();
166        assert_eq!(keys, vec!["id", "email"]);
167    }
168
169    #[test]
170    fn resource_definition_optional_sections() {
171        let rd = ResourceDefinition {
172            resource: "tags".to_string(),
173            version: 1,
174            schema: IndexMap::new(),
175            endpoints: None,
176            relations: None,
177            indexes: None,
178        };
179        assert!(rd.endpoints.is_none());
180        assert!(rd.relations.is_none());
181        assert!(rd.indexes.is_none());
182
183        let json = serde_json::to_string(&rd).unwrap();
184        assert!(!json.contains("endpoints"));
185        assert!(!json.contains("relations"));
186        assert!(!json.contains("indexes"));
187    }
188}