prax_cli/commands/
validate.rs1use crate::cli::ValidateArgs;
4use crate::config::SCHEMA_FILE_PATH;
5use crate::error::{CliError, CliResult};
6use crate::output::{self, success, warn};
7
8pub async fn run(args: ValidateArgs) -> CliResult<()> {
10 output::header("Validate Schema");
11
12 let cwd = std::env::current_dir()?;
13 let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_PATH));
14
15 if !schema_path.exists() {
16 return Err(
17 CliError::Config(format!("Schema file not found: {}", schema_path.display())).into(),
18 );
19 }
20
21 output::kv("Schema", &schema_path.display().to_string());
22 output::newline();
23
24 output::step(1, 3, "Parsing schema...");
26 let schema_content = std::fs::read_to_string(&schema_path)?;
27 let schema = parse_schema(&schema_content)?;
28
29 output::step(2, 3, "Running validation checks...");
31 let validation_result = validate_schema(&schema);
32
33 output::step(3, 3, "Checking configuration...");
35 let config_warnings = check_config(&schema);
36
37 output::newline();
38
39 match validation_result {
41 Ok(()) => {
42 if config_warnings.is_empty() {
43 success("Schema is valid!");
44 } else {
45 success("Schema is valid with warnings:");
46 output::newline();
47 for warning in &config_warnings {
48 warn(warning);
49 }
50 }
51 }
52 Err(errors) => {
53 output::error("Schema validation failed!");
54 output::newline();
55 output::section("Errors");
56 for error in &errors {
57 output::list_item(&format!("❌ {}", error));
58 }
59 if !config_warnings.is_empty() {
60 output::newline();
61 output::section("Warnings");
62 for warning in &config_warnings {
63 warn(warning);
64 }
65 }
66 return Err(
67 CliError::Validation(format!("Found {} validation errors", errors.len())).into(),
68 );
69 }
70 }
71
72 output::newline();
73
74 output::section("Schema Summary");
76 output::kv("Models", &schema.models.len().to_string());
77 output::kv("Enums", &schema.enums.len().to_string());
78 output::kv("Views", &schema.views.len().to_string());
79 output::kv("Composites", &schema.types.len().to_string());
80
81 let total_fields: usize = schema.models.values().map(|m| m.fields.len()).sum();
83
84 let relations: usize = schema
86 .models
87 .values()
88 .flat_map(|m| m.fields.values())
89 .filter(|f| {
90 if let prax_schema::ast::FieldType::Model(ref name) = f.field_type {
91 schema.models.contains_key(name.as_str())
93 && !schema.enums.contains_key(name.as_str())
94 && !schema.types.contains_key(name.as_str())
95 } else {
96 false
97 }
98 })
99 .count();
100
101 output::kv("Total Fields", &total_fields.to_string());
102 output::kv("Relations", &relations.to_string());
103
104 Ok(())
105}
106
107fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
108 prax_schema::validate_schema(content)
111 .map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
112}
113
114fn validate_schema(schema: &prax_schema::ast::Schema) -> Result<(), Vec<String>> {
115 let mut errors = Vec::new();
116
117 if schema.models.is_empty() {
119 errors.push("Schema must define at least one model".to_string());
120 }
121
122 for model in schema.models.values() {
124 let has_id = model.fields.values().any(|f| f.is_id());
126 if !has_id {
127 errors.push(format!(
128 "Model '{}' must have a field with @id attribute",
129 model.name()
130 ));
131 }
132
133 let mut field_names = std::collections::HashSet::new();
135 for field in model.fields.values() {
136 if !field_names.insert(field.name()) {
137 errors.push(format!(
138 "Duplicate field '{}' in model '{}'",
139 field.name(),
140 model.name()
141 ));
142 }
143 }
144
145 for field in model.fields.values() {
147 if field.is_relation() {
148 validate_relation(field, model, schema, &mut errors);
149 }
150 }
151 }
152
153 for enum_def in schema.enums.values() {
155 if enum_def.variants.is_empty() {
156 errors.push(format!(
157 "Enum '{}' must have at least one variant",
158 enum_def.name()
159 ));
160 }
161
162 let mut variant_names = std::collections::HashSet::new();
164 for variant in &enum_def.variants {
165 if !variant_names.insert(variant.name()) {
166 errors.push(format!(
167 "Duplicate variant '{}' in enum '{}'",
168 variant.name(),
169 enum_def.name()
170 ));
171 }
172 }
173 }
174
175 let mut type_names = std::collections::HashSet::new();
177 for model in schema.models.values() {
178 if !type_names.insert(model.name()) {
179 errors.push(format!("Duplicate type name '{}'", model.name()));
180 }
181 }
182 for enum_def in schema.enums.values() {
183 if !type_names.insert(enum_def.name()) {
184 errors.push(format!("Duplicate type name '{}'", enum_def.name()));
185 }
186 }
187
188 if errors.is_empty() {
189 Ok(())
190 } else {
191 Err(errors)
192 }
193}
194
195fn validate_relation(
196 field: &prax_schema::ast::Field,
197 model: &prax_schema::ast::Model,
198 schema: &prax_schema::ast::Schema,
199 errors: &mut Vec<String>,
200) {
201 use prax_schema::ast::FieldType;
202
203 let target_type = match &field.field_type {
205 FieldType::Model(name) => name.as_str(),
206 _ => return,
207 };
208
209 if schema.enums.contains_key(target_type) {
211 return;
212 }
213
214 if schema.types.contains_key(target_type) {
216 return;
217 }
218
219 let target_model = schema.models.get(target_type);
221 if target_model.is_none() {
222 errors.push(format!(
223 "Relation '{}' in model '{}' references unknown model '{}'",
224 field.name(),
225 model.name(),
226 target_type
227 ));
228 return;
229 }
230
231 if let Some(relation_attr) = field.get_attribute("relation") {
233 if let Some(fields_arg) = relation_attr
235 .args
236 .iter()
237 .find(|a| a.name.as_ref().map(|n| n.as_str()) == Some("fields"))
238 {
239 if let Some(fields_str) = fields_arg.value.as_string() {
240 let field_names: Vec<&str> = fields_str.split(',').map(|s| s.trim()).collect();
241 for field_name in &field_names {
242 if !model.fields.contains_key(*field_name) {
243 errors.push(format!(
244 "Relation '{}' in model '{}' references unknown field '{}'",
245 field.name(),
246 model.name(),
247 field_name
248 ));
249 }
250 }
251 }
252 }
253
254 if let Some(refs_arg) = relation_attr
256 .args
257 .iter()
258 .find(|a| a.name.as_ref().map(|n| n.as_str()) == Some("references"))
259 {
260 if let Some(refs_str) = refs_arg.value.as_string() {
261 let ref_names: Vec<&str> = refs_str.split(',').map(|s| s.trim()).collect();
262 let target = target_model.unwrap();
263 for ref_name in &ref_names {
264 if !target.fields.contains_key(*ref_name) {
265 errors.push(format!(
266 "Relation '{}' in model '{}' references unknown field '{}' in model '{}'",
267 field.name(),
268 model.name(),
269 ref_name,
270 target_type
271 ));
272 }
273 }
274 }
275 }
276 }
277}
278
279fn check_config(schema: &prax_schema::ast::Schema) -> Vec<String> {
280 let mut warnings = Vec::new();
281
282 for model in schema.models.values() {
284 let has_created_at = model.fields.values().any(|f| {
286 let name_lower = f.name().to_lowercase();
287 name_lower == "createdat" || name_lower == "created_at"
288 });
289 let has_updated_at = model.fields.values().any(|f| {
290 let name_lower = f.name().to_lowercase();
291 name_lower == "updatedat" || name_lower == "updated_at"
292 });
293
294 if !has_created_at && !has_updated_at {
295 warnings.push(format!(
296 "Model '{}' has no timestamp fields (createdAt/updatedAt)",
297 model.name()
298 ));
299 }
300 }
301
302 warnings
303}