1pub const AUTHORED_SPEC_VERSION: &str = "0.3.0";
10
11pub mod export;
12pub mod generator;
13pub mod loader;
14pub mod normalizer;
15pub mod passport;
16pub mod pipeline;
17mod syntax;
18pub mod types;
19pub mod validator;
20
21use thiserror::Error;
22
23#[derive(Error, Debug)]
25pub enum SpecError {
26 #[error("IO error: {0}")]
27 Io(#[from] std::io::Error),
28
29 #[error("File is not valid UTF-8: {path}")]
30 InvalidUtf8 { path: String },
31
32 #[error("YAML parse error: {message}")]
33 YamlParse { message: String, path: String },
34
35 #[error("JSON error: {0}")]
36 Json(#[from] serde_json::Error),
37
38 #[error("Schema validation failed: {message}")]
39 SchemaValidation { message: String, path: String },
40
41 #[error("Semantic validation error: {message}")]
42 SemanticValidation { message: String, path: String },
43
44 #[error("ID segment '{segment}' is a Rust reserved keyword in '{id}' at {path}")]
45 RustKeyword {
46 segment: String,
47 id: String,
48 path: String,
49 },
50
51 #[error("Duplicate ID '{id}' in {file1} and {file2}")]
52 DuplicateId {
53 id: String,
54 file1: String,
55 file2: String,
56 },
57
58 #[error("Dep fn_name collision: '{dep1}' and '{dep2}' both resolve to '{fn_name}' at {path}")]
59 DepCollision {
60 dep1: String,
61 dep2: String,
62 fn_name: String,
63 path: String,
64 },
65
66 #[error("❌ dep '{dep}' not found in this spec set")]
67 MissingDep { dep: String, path: String },
68
69 #[error("❌ cycle detected: {}", cycle_path.join(" → "))]
70 CyclicDep {
71 cycle_path: Vec<String>,
72 path: String,
73 },
74
75 #[error(
76 "body.rust must not contain use statements; declare external imports via imports (and internal unit deps via deps) at {path}"
77 )]
78 UseStatementInBody { path: String },
79
80 #[error("body.rust failed to parse as a block: {message} at {path}")]
81 BodyRustMustBeBlock { message: String, path: String },
82
83 #[error(
84 "body.rust looks like a full function declaration — spec 0.3.0 expects only the function body block. \
85 Remove the `pub fn name(params) -> ReturnType` line and keep only the `{{ ... }}` block. \
86 See migration guide. at {path}"
87 )]
88 BodyRustLooksLikeFnDeclaration { path: String },
89
90 #[error("local_tests[{id}].expect is not a valid Rust expression: {message} at {path}")]
91 LocalTestExpectNotExpr {
92 id: String,
93 message: String,
94 path: String,
95 },
96
97 #[error("duplicate local_tests id '{id}' at {path}")]
98 DuplicateLocalTestId { id: String, path: String },
99
100 #[error("contract.{field} has invalid Rust type '{type_str}': {message} at {path}")]
101 ContractTypeInvalid {
102 field: String,
103 type_str: String,
104 message: String,
105 path: String,
106 },
107
108 #[error("contract.inputs key '{name}' is not a valid Rust identifier: {message} at {path}")]
109 ContractInputNameInvalid {
110 name: String,
111 message: String,
112 path: String,
113 },
114
115 #[error("Traversal error: {message} at {path}")]
116 Traversal { message: String, path: String },
117
118 #[error("Generator error: {message}")]
119 Generator { message: String },
120
121 #[error("Output directory error: {message}")]
122 OutputDir { message: String },
123
124 #[error("Missing .spec-generated marker in {path} - refusing to clean without marker")]
125 MissingMarker { path: String },
126}
127
128impl From<walkdir::Error> for SpecError {
129 fn from(err: walkdir::Error) -> Self {
130 SpecError::Io(std::io::Error::other(err))
131 }
132}
133
134pub type Result<T> = std::result::Result<T, SpecError>;
136
137#[derive(Error, Debug)]
138pub enum SpecWarning {
139 #[error("⚠ dep '{dep}' not found in this spec set")]
140 MissingDep { dep: String, path: String },
141
142 #[error("⚠ skipped symlink cycle at '{path}'; subtree was skipped")]
143 SymlinkCycleSkipped { path: String },
144
145 #[error(
146 "⚠ spec_version not set in {path} — add `spec_version: \"{version}\"` to suppress this warning"
147 )]
148 MissingSpecVersion { path: String, version: &'static str },
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_error_display() {
157 let err = SpecError::InvalidUtf8 {
158 path: "foo.unit.spec".to_string(),
159 };
160 assert_eq!(err.to_string(), "File is not valid UTF-8: foo.unit.spec");
161
162 let err = SpecError::RustKeyword {
163 segment: "type".to_string(),
164 id: "pricing/type".to_string(),
165 path: "test.unit.spec".to_string(),
166 };
167 assert_eq!(
168 err.to_string(),
169 "ID segment 'type' is a Rust reserved keyword in 'pricing/type' at test.unit.spec"
170 );
171 }
172}