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("{file}: {source}")]
16 Context {
17 file: String,
18 #[source]
19 source: Box<ParseError>,
20 },
21}
22
23pub fn parse_resource(yaml: &str) -> Result<ResourceDefinition, ParseError> {
29 let mut resource: ResourceDefinition = serde_yaml::from_str(yaml)?;
30 shaperail_core::apply_endpoint_defaults(&mut resource);
31 Ok(resource)
32}
33
34pub fn parse_resource_file(path: &std::path::Path) -> Result<ResourceDefinition, ParseError> {
38 let content = std::fs::read_to_string(path)?;
39 parse_resource(&content).map_err(|e| ParseError::Context {
40 file: path.display().to_string(),
41 source: Box::new(e),
42 })
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn parse_minimal_resource() {
51 let yaml = r#"
52resource: tags
53version: 1
54schema:
55 id: { type: uuid, primary: true, generated: true }
56 name: { type: string, required: true }
57"#;
58 let rd = parse_resource(yaml).unwrap();
59 assert_eq!(rd.resource, "tags");
60 assert_eq!(rd.version, 1);
61 assert_eq!(rd.schema.len(), 2);
62 assert!(rd.endpoints.is_none());
63 assert!(rd.relations.is_none());
64 assert!(rd.indexes.is_none());
65 }
66
67 #[test]
68 fn parse_full_users_resource() {
69 let yaml = include_str!("../../resources/users.yaml");
70 let rd = parse_resource(yaml).unwrap();
71 assert_eq!(rd.resource, "users");
72 assert_eq!(rd.version, 1);
73 assert_eq!(rd.schema.len(), 9);
74 assert!(rd.endpoints.is_some());
75 assert!(rd.relations.is_some());
76 assert!(rd.indexes.is_some());
77 }
78
79 #[test]
80 fn parse_error_invalid_yaml() {
81 let yaml = "not: [valid: yaml: here";
82 let err = parse_resource(yaml).unwrap_err();
83 assert!(matches!(err, ParseError::Yaml(_)));
84 }
85
86 #[test]
87 fn parse_error_missing_resource_key() {
88 let yaml = r#"
89version: 1
90schema:
91 id: { type: uuid }
92"#;
93 let err = parse_resource(yaml).unwrap_err();
94 assert!(err.to_string().contains("missing field"));
95 }
96
97 #[test]
98 fn parse_error_unknown_top_level_key() {
99 let yaml = r#"
100resource: tags
101version: 1
102schema:
103 id: { type: uuid, primary: true, generated: true }
104unknown: true
105"#;
106 let err = parse_resource(yaml).unwrap_err();
107 assert!(err.to_string().contains("unknown field"));
108 assert!(err.to_string().contains("unknown"));
109 }
110
111 #[test]
112 fn parse_error_unknown_field_key() {
113 let yaml = r#"
114resource: tags
115version: 1
116schema:
117 id: { type: uuid, primary: true, generated: true, searchable: true }
118"#;
119 let err = parse_resource(yaml).unwrap_err();
120 assert!(err.to_string().contains("unknown field"));
121 assert!(err.to_string().contains("searchable"));
122 }
123
124 #[test]
125 fn parse_resource_with_db_key() {
126 let yaml = r#"
127resource: events
128version: 1
129db: analytics
130schema:
131 id: { type: uuid, primary: true, generated: true }
132 name: { type: string, required: true }
133"#;
134 let rd = parse_resource(yaml).unwrap();
135 assert_eq!(rd.resource, "events");
136 assert_eq!(rd.db.as_deref(), Some("analytics"));
137 }
138
139 #[test]
140 fn parse_resource_file_includes_filename_in_error() {
141 let path = std::path::Path::new("nonexistent/resource.yaml");
142 let err = parse_resource_file(path).unwrap_err();
143 assert!(matches!(err, ParseError::Io(_)));
145 }
146
147 #[test]
148 fn parse_resource_file_context_wraps_yaml_error() {
149 let dir = tempfile::tempdir().unwrap();
150 let path = dir.path().join("bad.yaml");
151 std::fs::write(&path, "resource: test\nunknown_key: true\n").unwrap();
152
153 let err = parse_resource_file(&path).unwrap_err();
154 let msg = err.to_string();
155 assert!(
157 msg.contains("bad.yaml"),
158 "Expected filename in error, got: {msg}"
159 );
160 assert!(
162 msg.contains("unknown field"),
163 "Expected parse error detail, got: {msg}"
164 );
165 }
166}