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_str(yaml: &str) -> Result<ResourceDefinition, ParseError> {
35 if yaml.contains("type: bigint") {
36 return Err(ParseError::RemovedType {
37 code: "E_BIGINT_REMOVED",
38 message:
39 "type 'bigint' was removed in v0.13.0 — use 'integer' (now 64-bit by default).",
40 });
41 }
42 Ok(serde_yaml::from_str(yaml)?)
43}
44
45pub fn parse_resource(yaml: &str) -> Result<ResourceDefinition, ParseError> {
51 if yaml.contains("type: bigint") {
54 return Err(ParseError::RemovedType {
55 code: "E_BIGINT_REMOVED",
56 message:
57 "type 'bigint' was removed in v0.13.0 — use 'integer' (now 64-bit by default).",
58 });
59 }
60 let mut resource: ResourceDefinition = serde_yaml::from_str(yaml)?;
61 shaperail_core::apply_endpoint_defaults(&mut resource);
62 Ok(resource)
63}
64
65pub fn parse_resource_file(path: &std::path::Path) -> Result<ResourceDefinition, ParseError> {
69 let content = std::fs::read_to_string(path)?;
70 parse_resource(&content).map_err(|e| ParseError::Context {
71 file: path.display().to_string(),
72 source: Box::new(e),
73 })
74}
75
76pub fn diagnose_file(
83 path: &std::path::Path,
84) -> Result<
85 (
86 shaperail_core::ResourceDefinition,
87 Vec<crate::diagnostics::Diagnostic>,
88 ),
89 ParseError,
90> {
91 let yaml = std::fs::read_to_string(path).map_err(ParseError::from)?;
92
93 #[cfg(feature = "saphyr-spans")]
94 {
95 match crate::parser_saphyr::parse_with_spans_in_file(&yaml, path.to_path_buf()) {
96 Ok((rd, spans)) => {
97 let diags = crate::diagnostics::diagnose_resource_with_spans(&rd, &spans);
98 return Ok((rd, diags));
99 }
100 Err(_saphyr_err) => {
101 }
107 }
108 }
109
110 let rd: shaperail_core::ResourceDefinition =
111 serde_yaml::from_str(&yaml).map_err(ParseError::from)?;
112 let diags = crate::diagnostics::diagnose_resource(&rd);
113 Ok((rd, diags))
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn parse_minimal_resource() {
122 let yaml = r#"
123resource: tags
124version: 1
125schema:
126 id: { type: uuid, primary: true, generated: true }
127 name: { type: string, required: true }
128"#;
129 let rd = parse_resource(yaml).unwrap();
130 assert_eq!(rd.resource, "tags");
131 assert_eq!(rd.version, 1);
132 assert_eq!(rd.schema.len(), 2);
133 assert!(rd.endpoints.is_none());
134 assert!(rd.relations.is_none());
135 assert!(rd.indexes.is_none());
136 }
137
138 #[test]
139 fn parse_full_users_resource() {
140 let yaml = include_str!("../../resources/users.yaml");
141 let rd = parse_resource(yaml).unwrap();
142 assert_eq!(rd.resource, "users");
143 assert_eq!(rd.version, 1);
144 assert_eq!(rd.schema.len(), 10);
145 assert!(rd.endpoints.is_some());
146 assert!(rd.relations.is_some());
147 assert!(rd.indexes.is_some());
148 }
149
150 #[test]
151 fn parse_error_invalid_yaml() {
152 let yaml = "not: [valid: yaml: here";
153 let err = parse_resource(yaml).unwrap_err();
154 assert!(matches!(err, ParseError::Yaml(_)));
155 }
156
157 #[test]
158 fn parse_error_missing_resource_key() {
159 let yaml = r#"
160version: 1
161schema:
162 id: { type: uuid }
163"#;
164 let err = parse_resource(yaml).unwrap_err();
165 assert!(err.to_string().contains("missing field"));
166 }
167
168 #[test]
169 fn parse_error_unknown_top_level_key() {
170 let yaml = r#"
171resource: tags
172version: 1
173schema:
174 id: { type: uuid, primary: true, generated: true }
175unknown: true
176"#;
177 let err = parse_resource(yaml).unwrap_err();
178 assert!(err.to_string().contains("unknown field"));
179 assert!(err.to_string().contains("unknown"));
180 }
181
182 #[test]
183 fn parse_error_unknown_field_key() {
184 let yaml = r#"
185resource: tags
186version: 1
187schema:
188 id: { type: uuid, primary: true, generated: true, searchable: true }
189"#;
190 let err = parse_resource(yaml).unwrap_err();
191 assert!(err.to_string().contains("unknown field"));
192 assert!(err.to_string().contains("searchable"));
193 }
194
195 #[test]
196 fn parse_resource_with_db_key() {
197 let yaml = r#"
198resource: events
199version: 1
200db: analytics
201schema:
202 id: { type: uuid, primary: true, generated: true }
203 name: { type: string, required: true }
204"#;
205 let rd = parse_resource(yaml).unwrap();
206 assert_eq!(rd.resource, "events");
207 assert_eq!(rd.db.as_deref(), Some("analytics"));
208 }
209
210 #[test]
211 fn parse_resource_file_includes_filename_in_error() {
212 let path = std::path::Path::new("nonexistent/resource.yaml");
213 let err = parse_resource_file(path).unwrap_err();
214 assert!(matches!(err, ParseError::Io(_)));
216 }
217
218 #[test]
219 fn parse_resource_file_context_wraps_yaml_error() {
220 let dir = tempfile::tempdir().unwrap();
221 let path = dir.path().join("bad.yaml");
222 std::fs::write(&path, "resource: test\nunknown_key: true\n").unwrap();
223
224 let err = parse_resource_file(&path).unwrap_err();
225 let msg = err.to_string();
226 assert!(
228 msg.contains("bad.yaml"),
229 "Expected filename in error, got: {msg}"
230 );
231 assert!(
233 msg.contains("unknown field"),
234 "Expected parse error detail, got: {msg}"
235 );
236 }
237}