shaperail_codegen/
parser.rs1use shaperail_core::ResourceDefinition;
2
3#[derive(Debug, thiserror::Error)]
5pub enum ParseError {
6 #[error("{0}")]
7 Yaml(#[from] serde_yaml::Error),
8
9 #[error("{0}")]
10 Io(#[from] std::io::Error),
11
12 #[error("{0}")]
13 ConfigInterpolation(String),
14
15 #[error("[{code}] {message}")]
17 RemovedType {
18 code: &'static str,
19 message: &'static str,
20 },
21
22 #[error("{file}: {source}")]
23 Context {
24 file: String,
25 #[source]
26 source: Box<ParseError>,
27 },
28}
29
30pub fn parse_resource(yaml: &str) -> Result<ResourceDefinition, ParseError> {
36 if yaml.contains("type: bigint") {
39 return Err(ParseError::RemovedType {
40 code: "E_BIGINT_REMOVED",
41 message:
42 "type 'bigint' was removed in v0.13.0 — use 'integer' (now 64-bit by default).",
43 });
44 }
45 let mut resource: ResourceDefinition = serde_yaml::from_str(yaml)?;
46 shaperail_core::apply_endpoint_defaults(&mut resource);
47 Ok(resource)
48}
49
50pub fn parse_resource_file(path: &std::path::Path) -> Result<ResourceDefinition, ParseError> {
54 let content = std::fs::read_to_string(path)?;
55 parse_resource(&content).map_err(|e| ParseError::Context {
56 file: path.display().to_string(),
57 source: Box::new(e),
58 })
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn parse_minimal_resource() {
67 let yaml = r#"
68resource: tags
69version: 1
70schema:
71 id: { type: uuid, primary: true, generated: true }
72 name: { type: string, required: true }
73"#;
74 let rd = parse_resource(yaml).unwrap();
75 assert_eq!(rd.resource, "tags");
76 assert_eq!(rd.version, 1);
77 assert_eq!(rd.schema.len(), 2);
78 assert!(rd.endpoints.is_none());
79 assert!(rd.relations.is_none());
80 assert!(rd.indexes.is_none());
81 }
82
83 #[test]
84 fn parse_full_users_resource() {
85 let yaml = include_str!("../../resources/users.yaml");
86 let rd = parse_resource(yaml).unwrap();
87 assert_eq!(rd.resource, "users");
88 assert_eq!(rd.version, 1);
89 assert_eq!(rd.schema.len(), 10);
90 assert!(rd.endpoints.is_some());
91 assert!(rd.relations.is_some());
92 assert!(rd.indexes.is_some());
93 }
94
95 #[test]
96 fn parse_error_invalid_yaml() {
97 let yaml = "not: [valid: yaml: here";
98 let err = parse_resource(yaml).unwrap_err();
99 assert!(matches!(err, ParseError::Yaml(_)));
100 }
101
102 #[test]
103 fn parse_error_missing_resource_key() {
104 let yaml = r#"
105version: 1
106schema:
107 id: { type: uuid }
108"#;
109 let err = parse_resource(yaml).unwrap_err();
110 assert!(err.to_string().contains("missing field"));
111 }
112
113 #[test]
114 fn parse_error_unknown_top_level_key() {
115 let yaml = r#"
116resource: tags
117version: 1
118schema:
119 id: { type: uuid, primary: true, generated: true }
120unknown: true
121"#;
122 let err = parse_resource(yaml).unwrap_err();
123 assert!(err.to_string().contains("unknown field"));
124 assert!(err.to_string().contains("unknown"));
125 }
126
127 #[test]
128 fn parse_error_unknown_field_key() {
129 let yaml = r#"
130resource: tags
131version: 1
132schema:
133 id: { type: uuid, primary: true, generated: true, searchable: true }
134"#;
135 let err = parse_resource(yaml).unwrap_err();
136 assert!(err.to_string().contains("unknown field"));
137 assert!(err.to_string().contains("searchable"));
138 }
139
140 #[test]
141 fn parse_resource_with_db_key() {
142 let yaml = r#"
143resource: events
144version: 1
145db: analytics
146schema:
147 id: { type: uuid, primary: true, generated: true }
148 name: { type: string, required: true }
149"#;
150 let rd = parse_resource(yaml).unwrap();
151 assert_eq!(rd.resource, "events");
152 assert_eq!(rd.db.as_deref(), Some("analytics"));
153 }
154
155 #[test]
156 fn parse_resource_file_includes_filename_in_error() {
157 let path = std::path::Path::new("nonexistent/resource.yaml");
158 let err = parse_resource_file(path).unwrap_err();
159 assert!(matches!(err, ParseError::Io(_)));
161 }
162
163 #[test]
164 fn parse_resource_file_context_wraps_yaml_error() {
165 let dir = tempfile::tempdir().unwrap();
166 let path = dir.path().join("bad.yaml");
167 std::fs::write(&path, "resource: test\nunknown_key: true\n").unwrap();
168
169 let err = parse_resource_file(&path).unwrap_err();
170 let msg = err.to_string();
171 assert!(
173 msg.contains("bad.yaml"),
174 "Expected filename in error, got: {msg}"
175 );
176 assert!(
178 msg.contains("unknown field"),
179 "Expected parse error detail, got: {msg}"
180 );
181 }
182}