Skip to main content

shaperail_codegen/
json_schema.rs

1use serde_json::{json, Value};
2
3/// Generate a JSON Schema (draft 2020-12) that validates Shaperail resource YAML files.
4///
5/// This schema is the canonical machine-readable definition of the resource format.
6/// LLMs and IDEs can use it for autocomplete and validation. The schema is generated
7/// from the same types that `parser.rs` uses, ensuring they never drift apart.
8pub fn generate_resource_json_schema() -> Value {
9    json!({
10        "$schema": "https://json-schema.org/draft/2020-12/schema",
11        "$id": "https://shaperail.dev/schema/resource.v1.json",
12        "title": "Shaperail Resource Definition",
13        "description": "Schema for Shaperail resource YAML files. Defines a single API resource with its fields, endpoints, relations, and indexes.",
14        "type": "object",
15        "required": ["resource", "version", "schema"],
16        "additionalProperties": false,
17        "properties": {
18            "resource": {
19                "type": "string",
20                "description": "Snake_case plural name of the resource (e.g., 'users', 'blog_posts').",
21                "pattern": "^[a-z][a-z0-9_]*$"
22            },
23            "version": {
24                "type": "integer",
25                "description": "Schema version number (starts at 1). Drives route prefix: /v{version}/...",
26                "minimum": 1
27            },
28            "db": {
29                "type": "string",
30                "description": "Named database connection for this resource (M14 multi-DB). Default: 'default'."
31            },
32            "tenant_key": {
33                "type": "string",
34                "description": "Tenant isolation key (M18). References a uuid schema field that identifies the tenant. When set, all queries are automatically scoped to the authenticated user's tenant_id claim."
35            },
36            "schema": {
37                "type": "object",
38                "description": "Field definitions keyed by field name. At least one field with primary: true is required.",
39                "minProperties": 1,
40                "additionalProperties": { "$ref": "#/$defs/FieldSchema" }
41            },
42            "endpoints": {
43                "type": "object",
44                "description": "Endpoint definitions keyed by action name (e.g., 'list', 'get', 'create', 'update', 'delete').",
45                "additionalProperties": { "$ref": "#/$defs/EndpointSpec" }
46            },
47            "relations": {
48                "type": "object",
49                "description": "Relationship definitions keyed by relation name.",
50                "additionalProperties": { "$ref": "#/$defs/RelationSpec" }
51            },
52            "indexes": {
53                "type": "array",
54                "description": "Additional database indexes.",
55                "items": { "$ref": "#/$defs/IndexSpec" }
56            }
57        },
58        "$defs": {
59            "FieldType": {
60                "type": "string",
61                "enum": ["uuid", "string", "integer", "number", "boolean", "timestamp", "date", "enum", "json", "array", "file"],
62                "description": "The data type of a field."
63            },
64            "FieldSchema": {
65                "type": "object",
66                "description": "Definition of a single field in a resource schema.",
67                "required": ["type"],
68                "additionalProperties": false,
69                "properties": {
70                    "type": { "$ref": "#/$defs/FieldType" },
71                    "primary": {
72                        "type": "boolean",
73                        "default": false,
74                        "description": "Whether this field is the primary key. Exactly one field must be primary."
75                    },
76                    "generated": {
77                        "type": "boolean",
78                        "default": false,
79                        "description": "Whether this field is auto-generated (e.g., uuid v4, timestamps)."
80                    },
81                    "required": {
82                        "type": "boolean",
83                        "default": false,
84                        "description": "Whether this field is required (NOT NULL + validated on input)."
85                    },
86                    "unique": {
87                        "type": "boolean",
88                        "default": false,
89                        "description": "Whether this field has a unique constraint."
90                    },
91                    "nullable": {
92                        "type": "boolean",
93                        "default": false,
94                        "description": "Whether this field is explicitly nullable."
95                    },
96                    "ref": {
97                        "type": "string",
98                        "description": "Foreign key reference in 'resource.field' format (e.g., 'organizations.id'). Field type must be uuid.",
99                        "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$"
100                    },
101                    "min": {
102                        "description": "Minimum value (number) or minimum length (string)."
103                    },
104                    "max": {
105                        "description": "Maximum value (number) or maximum length (string)."
106                    },
107                    "format": {
108                        "type": "string",
109                        "description": "String format validation. Only valid when type is 'string'.",
110                        "enum": ["email", "url", "uuid"]
111                    },
112                    "values": {
113                        "type": "array",
114                        "items": { "type": "string" },
115                        "minItems": 1,
116                        "description": "Allowed values for enum-type fields. Required when type is 'enum'."
117                    },
118                    "default": {
119                        "description": "Default value for this field."
120                    },
121                    "sensitive": {
122                        "type": "boolean",
123                        "default": false,
124                        "description": "Whether this field contains sensitive data (redacted in logs)."
125                    },
126                    "search": {
127                        "type": "boolean",
128                        "default": false,
129                        "description": "Whether this field is included in full-text search."
130                    },
131                    "items": {
132                        "type": "string",
133                        "description": "Element type for array fields. Required when type is 'array'.",
134                        "enum": ["uuid", "string", "integer", "number", "boolean", "timestamp", "date"]
135                    }
136                }
137            },
138            "HttpMethod": {
139                "type": "string",
140                "enum": ["GET", "POST", "PATCH", "PUT", "DELETE"]
141            },
142            "AuthRule": {
143                "description": "Authentication rule. Use 'public' for no auth, 'owner' for ownership check, or an array of role strings.",
144                "oneOf": [
145                    { "const": "public" },
146                    { "const": "owner" },
147                    {
148                        "type": "array",
149                        "items": { "type": "string" },
150                        "minItems": 1,
151                        "description": "Requires JWT with one of these roles. Use 'owner' in the array to combine role + ownership check."
152                    }
153                ]
154            },
155            "PaginationStyle": {
156                "type": "string",
157                "enum": ["cursor", "offset"],
158                "description": "Pagination strategy for list endpoints."
159            },
160            "CacheSpec": {
161                "type": "object",
162                "description": "Cache configuration for an endpoint.",
163                "required": ["ttl"],
164                "additionalProperties": false,
165                "properties": {
166                    "ttl": {
167                        "type": "integer",
168                        "minimum": 1,
169                        "description": "Time-to-live in seconds."
170                    },
171                    "invalidate_on": {
172                        "type": "array",
173                        "items": { "type": "string", "enum": ["create", "update", "delete"] },
174                        "description": "Events that invalidate this cache."
175                    }
176                }
177            },
178            "UploadSpec": {
179                "type": "object",
180                "description": "File upload configuration for an endpoint.",
181                "required": ["field", "storage", "max_size"],
182                "additionalProperties": false,
183                "properties": {
184                    "field": {
185                        "type": "string",
186                        "description": "Schema field that stores the file URL. Must be type 'file'."
187                    },
188                    "storage": {
189                        "type": "string",
190                        "enum": ["local", "s3", "gcs", "azure"],
191                        "description": "Storage backend."
192                    },
193                    "max_size": {
194                        "type": "string",
195                        "description": "Maximum file size (e.g., '5mb', '10mb').",
196                        "pattern": "^[0-9]+[kmg]b$"
197                    },
198                    "types": {
199                        "type": "array",
200                        "items": { "type": "string" },
201                        "description": "Allowed file extensions (e.g., ['jpg', 'png', 'pdf'])."
202                    }
203                }
204            },
205            "ControllerSpec": {
206                "type": "object",
207                "description": "Controller specification for synchronous in-request business logic. Functions live in resources/<resource>.controller.rs.",
208                "additionalProperties": false,
209                "properties": {
210                    "before": {
211                        "description": "Function name (or list of function names) called before the DB operation. Hooks run in declaration order; first error short-circuits. Prefix any name with 'wasm:' for WASM plugins (e.g., 'wasm:./plugins/validator.wasm').",
212                        "oneOf": [
213                            { "type": "string" },
214                            { "type": "array", "items": { "type": "string" }, "minItems": 1 }
215                        ]
216                    },
217                    "after": {
218                        "description": "Function name (or list of function names) called after the DB operation. Hooks run in declaration order; first error short-circuits. Prefix any name with 'wasm:' for WASM plugins.",
219                        "oneOf": [
220                            { "type": "string" },
221                            { "type": "array", "items": { "type": "string" }, "minItems": 1 }
222                        ]
223                    }
224                }
225            },
226            "EndpointSpec": {
227                "type": "object",
228                "description": "Specification for a single endpoint in a resource.",
229                "additionalProperties": false,
230                "properties": {
231                    "method": { "$ref": "#/$defs/HttpMethod" },
232                    "path": {
233                        "type": "string",
234                        "description": "URL path pattern (e.g., '/users', '/users/:id'). Auto-prefixed with /v{version}.",
235                        "pattern": "^/"
236                    },
237                    "auth": { "$ref": "#/$defs/AuthRule" },
238                    "input": {
239                        "type": "array",
240                        "items": { "type": "string" },
241                        "description": "Schema fields accepted as input for create/update. Each must exist in schema."
242                    },
243                    "filters": {
244                        "type": "array",
245                        "items": { "type": "string" },
246                        "description": "Schema fields available as query filters. Each must exist in schema."
247                    },
248                    "search": {
249                        "type": "array",
250                        "items": { "type": "string" },
251                        "description": "Schema fields included in full-text search. Each must exist in schema."
252                    },
253                    "pagination": { "$ref": "#/$defs/PaginationStyle" },
254                    "sort": {
255                        "type": "array",
256                        "items": { "type": "string" },
257                        "description": "Schema fields available for sorting. Each must exist in schema."
258                    },
259                    "cache": { "$ref": "#/$defs/CacheSpec" },
260                    "controller": { "$ref": "#/$defs/ControllerSpec" },
261                    "events": {
262                        "type": "array",
263                        "items": { "type": "string" },
264                        "description": "Events to emit after successful execution (e.g., ['user.created'])."
265                    },
266                    "jobs": {
267                        "type": "array",
268                        "items": { "type": "string" },
269                        "description": "Background jobs to enqueue after successful execution (e.g., ['send_welcome_email'])."
270                    },
271                    "upload": { "$ref": "#/$defs/UploadSpec" },
272                    "soft_delete": {
273                        "type": "boolean",
274                        "default": false,
275                        "description": "Whether this endpoint performs a soft delete. Requires a 'deleted_at' nullable timestamp field in schema."
276                    }
277                }
278            },
279            "RelationType": {
280                "type": "string",
281                "enum": ["belongs_to", "has_many", "has_one"]
282            },
283            "RelationSpec": {
284                "type": "object",
285                "description": "Specification for a relationship to another resource.",
286                "required": ["resource", "type"],
287                "additionalProperties": false,
288                "properties": {
289                    "resource": {
290                        "type": "string",
291                        "description": "Name of the related resource."
292                    },
293                    "type": { "$ref": "#/$defs/RelationType" },
294                    "key": {
295                        "type": "string",
296                        "description": "Local foreign key field. Required for belongs_to."
297                    },
298                    "foreign_key": {
299                        "type": "string",
300                        "description": "Foreign key on the related resource. Required for has_many and has_one."
301                    }
302                }
303            },
304            "IndexSpec": {
305                "type": "object",
306                "description": "Specification for a database index.",
307                "required": ["fields"],
308                "additionalProperties": false,
309                "properties": {
310                    "fields": {
311                        "type": "array",
312                        "items": { "type": "string" },
313                        "minItems": 1,
314                        "description": "Fields included in this index. Each must exist in schema."
315                    },
316                    "unique": {
317                        "type": "boolean",
318                        "default": false,
319                        "description": "Whether this is a unique index."
320                    },
321                    "order": {
322                        "type": "string",
323                        "enum": ["asc", "desc"],
324                        "description": "Sort order for the index."
325                    }
326                }
327            }
328        }
329    })
330}
331
332/// Render the JSON Schema as a pretty-printed JSON string.
333pub fn render_json_schema() -> String {
334    serde_json::to_string_pretty(&generate_resource_json_schema())
335        .expect("JSON schema serialization cannot fail")
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn schema_is_valid_json() {
344        let schema = generate_resource_json_schema();
345        assert_eq!(schema["type"], "object");
346        assert!(
347            schema["$defs"]["FieldType"]["enum"]
348                .as_array()
349                .unwrap()
350                .len()
351                == 11
352        );
353    }
354
355    #[test]
356    fn schema_requires_resource_version_schema() {
357        let schema = generate_resource_json_schema();
358        let required = schema["required"].as_array().unwrap();
359        assert!(required.contains(&json!("resource")));
360        assert!(required.contains(&json!("version")));
361        assert!(required.contains(&json!("schema")));
362    }
363
364    #[test]
365    fn schema_disallows_additional_properties() {
366        let schema = generate_resource_json_schema();
367        assert_eq!(schema["additionalProperties"], false);
368    }
369
370    #[test]
371    fn render_produces_nonempty_string() {
372        let rendered = render_json_schema();
373        assert!(rendered.len() > 1000);
374        assert!(rendered.contains("$schema"));
375    }
376}