1use std::collections::BTreeSet;
15
16use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase};
17use weaveffi_ir::ir::{Api, Module};
18
19pub const BRAND_STEM: &str = "WeaveFFI";
22
23pub const ERROR_BRAND: &str = "WeaveFFIError";
26
27pub const EXCEPTION_BRAND: &str = "WeaveFFIException";
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ResolvedError {
34 pub raw_name: String,
36 pub code: i32,
38 pub message: String,
40 pub doc: Option<String>,
42}
43
44impl ResolvedError {
45 pub fn error_class(&self) -> String {
48 type_name(&self.raw_name, "Error")
49 }
50
51 pub fn exception_class(&self) -> String {
54 type_name(&self.raw_name, "Exception")
55 }
56
57 pub fn camel(&self) -> String {
60 self.raw_name.to_lower_camel_case()
61 }
62
63 pub fn pascal(&self) -> String {
65 self.raw_name.to_upper_camel_case()
66 }
67
68 pub fn shouty(&self) -> String {
70 self.raw_name.to_shouty_snake_case()
71 }
72}
73
74pub fn pascal(raw: &str) -> String {
79 raw.to_upper_camel_case()
80}
81
82pub fn type_name(raw: &str, suffix: &str) -> String {
86 let pascal = raw.to_upper_camel_case();
87 if pascal.ends_with(suffix) {
88 pascal
89 } else {
90 format!("{pascal}{suffix}")
91 }
92}
93
94pub fn exception_type_name(raw: &str) -> String {
99 let pascal = raw.to_upper_camel_case();
100 let stem = pascal.strip_suffix("Error").unwrap_or(&pascal);
101 if stem.is_empty() {
102 "WeaveFFIException".to_string()
103 } else {
104 type_name(stem, "Exception")
105 }
106}
107
108pub fn all(api: &Api) -> Vec<ResolvedError> {
112 let mut seen: BTreeSet<String> = BTreeSet::new();
113 let mut out: Vec<ResolvedError> = Vec::new();
114 fn walk(mods: &[Module], seen: &mut BTreeSet<String>, out: &mut Vec<ResolvedError>) {
115 for m in mods {
116 if let Some(domain) = &m.errors {
117 for c in &domain.codes {
118 if seen.insert(c.name.clone()) {
119 out.push(ResolvedError {
120 raw_name: c.name.clone(),
121 code: c.code,
122 message: c.message.clone(),
123 doc: c.doc.clone(),
124 });
125 }
126 }
127 }
128 walk(&m.modules, seen, out);
129 }
130 }
131 walk(&api.modules, &mut seen, &mut out);
132 out
133}
134
135pub fn has_domains(api: &Api) -> bool {
137 fn any(mods: &[Module]) -> bool {
138 mods.iter().any(|m| m.errors.is_some() || any(&m.modules))
139 }
140 any(&api.modules)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use weaveffi_ir::ir::{ErrorCode, ErrorDomain, Module};
147
148 fn module_with_errors(name: &str, codes: Vec<(&str, i32, &str)>) -> Module {
149 Module {
150 name: name.into(),
151 functions: vec![],
152 interfaces: vec![],
153 structs: vec![],
154 enums: vec![],
155 callbacks: vec![],
156 listeners: vec![],
157 errors: Some(ErrorDomain {
158 name: format!("{name}Error"),
159 codes: codes
160 .into_iter()
161 .map(|(n, c, m)| ErrorCode {
162 name: n.into(),
163 code: c,
164 message: m.into(),
165 doc: None,
166 })
167 .collect(),
168 }),
169 modules: vec![],
170 }
171 }
172
173 fn api_with(mods: Vec<Module>) -> Api {
174 Api {
175 version: "0.5.0".into(),
176 package: None,
177 modules: mods,
178 generators: None,
179 }
180 }
181
182 #[test]
183 fn type_name_avoids_screaming_and_doubling() {
184 assert_eq!(type_name("KEY_NOT_FOUND", "Error"), "KeyNotFoundError");
185 assert_eq!(
186 type_name("KEY_NOT_FOUND", "Exception"),
187 "KeyNotFoundException"
188 );
189 assert_eq!(type_name("AlreadyError", "Error"), "AlreadyError");
190 assert_eq!(type_name("invalid_input", "Error"), "InvalidInputError");
191 }
192
193 #[test]
194 fn exception_type_name_replaces_error_stem() {
195 assert_eq!(exception_type_name("KvError"), "KvException");
196 assert_eq!(exception_type_name("ContactsError"), "ContactsException");
197 assert_eq!(exception_type_name("Failure"), "FailureException");
198 assert_eq!(exception_type_name("KvException"), "KvException");
199 assert_eq!(exception_type_name("Error"), "WeaveFFIException");
200 }
201
202 #[test]
203 fn member_spellings() {
204 let e = ResolvedError {
205 raw_name: "KEY_NOT_FOUND".into(),
206 code: 1,
207 message: "nope".into(),
208 doc: None,
209 };
210 assert_eq!(e.error_class(), "KeyNotFoundError");
211 assert_eq!(e.exception_class(), "KeyNotFoundException");
212 assert_eq!(e.camel(), "keyNotFound");
213 assert_eq!(e.pascal(), "KeyNotFound");
214 assert_eq!(e.shouty(), "KEY_NOT_FOUND");
215 }
216
217 #[test]
218 fn flattens_and_dedups_across_modules() {
219 let api = api_with(vec![
220 module_with_errors("a", vec![("NOT_FOUND", 1, "x"), ("DENIED", 2, "y")]),
221 module_with_errors("b", vec![("NOT_FOUND", 1, "x"), ("TIMEOUT", 3, "z")]),
222 ]);
223 let codes = all(&api);
224 let names: Vec<_> = codes.iter().map(|c| c.raw_name.as_str()).collect();
225 assert_eq!(names, vec!["NOT_FOUND", "DENIED", "TIMEOUT"]);
226 assert!(has_domains(&api));
227 }
228
229 #[test]
230 fn no_domains_is_empty() {
231 let api = api_with(vec![Module {
232 name: "m".into(),
233 functions: vec![],
234 interfaces: vec![],
235 structs: vec![],
236 enums: vec![],
237 callbacks: vec![],
238 listeners: vec![],
239 errors: None,
240 modules: vec![],
241 }]);
242 assert!(all(&api).is_empty());
243 assert!(!has_domains(&api));
244 }
245}