1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4use crate::{EndpointSpec, FieldSchema, IndexSpec, RelationSpec};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct ResourceDefinition {
20 pub resource: String,
22
23 pub version: u32,
25
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub db: Option<String>,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub tenant_key: Option<String>,
35
36 pub schema: IndexMap<String, FieldSchema>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub endpoints: Option<IndexMap<String, EndpointSpec>>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub relations: Option<IndexMap<String, RelationSpec>>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub indexes: Option<Vec<IndexSpec>>,
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::{
56 AuthRule, CacheSpec, EndpointSpec, FieldType, HttpMethod, PaginationStyle, RelationType,
57 };
58
59 fn sample_resource() -> ResourceDefinition {
60 let mut schema = IndexMap::new();
61 schema.insert(
62 "id".to_string(),
63 FieldSchema {
64 field_type: FieldType::Uuid,
65 primary: true,
66 generated: true,
67 required: false,
68 unique: false,
69 nullable: false,
70 reference: None,
71 min: None,
72 max: None,
73 format: None,
74 values: None,
75 default: None,
76 sensitive: false,
77 search: false,
78 items: None,
79 transient: false,
80 },
81 );
82 schema.insert(
83 "email".to_string(),
84 FieldSchema {
85 field_type: FieldType::String,
86 primary: false,
87 generated: false,
88 required: true,
89 unique: true,
90 nullable: false,
91 reference: None,
92 min: None,
93 max: None,
94 format: Some("email".to_string()),
95 values: None,
96 default: None,
97 sensitive: false,
98 search: false,
99 items: None,
100 transient: false,
101 },
102 );
103
104 let mut endpoints = IndexMap::new();
105 endpoints.insert(
106 "list".to_string(),
107 EndpointSpec {
108 method: Some(HttpMethod::Get),
109 path: Some("/users".to_string()),
110 auth: Some(AuthRule::Roles(vec![
111 "member".to_string(),
112 "admin".to_string(),
113 ])),
114 input: None,
115 filters: Some(vec!["role".to_string()]),
116 search: Some(vec!["email".to_string()]),
117 pagination: Some(PaginationStyle::Cursor),
118 sort: None,
119 cache: Some(CacheSpec {
120 ttl: 60,
121 invalidate_on: None,
122 }),
123 controller: None,
124 events: None,
125 jobs: None,
126 subscribers: None,
127 handler: None,
128 upload: None,
129 rate_limit: None,
130 soft_delete: false,
131 },
132 );
133
134 let mut relations = IndexMap::new();
135 relations.insert(
136 "orders".to_string(),
137 RelationSpec {
138 resource: "orders".to_string(),
139 relation_type: RelationType::HasMany,
140 key: None,
141 foreign_key: Some("user_id".to_string()),
142 },
143 );
144
145 ResourceDefinition {
146 resource: "users".to_string(),
147 version: 1,
148 db: None,
149 tenant_key: None,
150 schema,
151 endpoints: Some(endpoints),
152 relations: Some(relations),
153 indexes: Some(vec![IndexSpec {
154 fields: vec!["created_at".to_string()],
155 unique: false,
156 order: Some("desc".to_string()),
157 }]),
158 }
159 }
160
161 #[test]
162 fn resource_definition_construction() {
163 let rd = sample_resource();
164 assert_eq!(rd.resource, "users");
165 assert_eq!(rd.version, 1);
166 assert_eq!(rd.schema.len(), 2);
167 assert!(rd.schema.contains_key("id"));
168 assert!(rd.schema.contains_key("email"));
169 }
170
171 #[test]
172 fn resource_definition_serde_roundtrip() {
173 let rd = sample_resource();
174 let json = serde_json::to_string_pretty(&rd).unwrap();
175 let back: ResourceDefinition = serde_json::from_str(&json).unwrap();
176 assert_eq!(rd, back);
177 }
178
179 #[test]
180 fn resource_definition_preserves_field_order() {
181 let rd = sample_resource();
182 let keys: Vec<&String> = rd.schema.keys().collect();
183 assert_eq!(keys, vec!["id", "email"]);
184 }
185
186 #[test]
187 fn resource_definition_optional_sections() {
188 let rd = ResourceDefinition {
189 resource: "tags".to_string(),
190 version: 1,
191 db: None,
192 tenant_key: None,
193 schema: IndexMap::new(),
194 endpoints: None,
195 relations: None,
196 indexes: None,
197 };
198 assert!(rd.endpoints.is_none());
199 assert!(rd.relations.is_none());
200 assert!(rd.indexes.is_none());
201
202 let json = serde_json::to_string(&rd).unwrap();
203 assert!(!json.contains("endpoints"));
204 assert!(!json.contains("relations"));
205 assert!(!json.contains("indexes"));
206 }
207}