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 all(api: &Api) -> Vec<ResolvedError> {
98 let mut seen: BTreeSet<String> = BTreeSet::new();
99 let mut out: Vec<ResolvedError> = Vec::new();
100 fn walk(mods: &[Module], seen: &mut BTreeSet<String>, out: &mut Vec<ResolvedError>) {
101 for m in mods {
102 if let Some(domain) = &m.errors {
103 for c in &domain.codes {
104 if seen.insert(c.name.clone()) {
105 out.push(ResolvedError {
106 raw_name: c.name.clone(),
107 code: c.code,
108 message: c.message.clone(),
109 doc: c.doc.clone(),
110 });
111 }
112 }
113 }
114 walk(&m.modules, seen, out);
115 }
116 }
117 walk(&api.modules, &mut seen, &mut out);
118 out
119}
120
121pub fn has_domains(api: &Api) -> bool {
123 fn any(mods: &[Module]) -> bool {
124 mods.iter().any(|m| m.errors.is_some() || any(&m.modules))
125 }
126 any(&api.modules)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use weaveffi_ir::ir::{ErrorCode, ErrorDomain, Module};
133
134 fn module_with_errors(name: &str, codes: Vec<(&str, i32, &str)>) -> Module {
135 Module {
136 name: name.into(),
137 functions: vec![],
138 structs: vec![],
139 enums: vec![],
140 callbacks: vec![],
141 listeners: vec![],
142 errors: Some(ErrorDomain {
143 name: format!("{name}Error"),
144 codes: codes
145 .into_iter()
146 .map(|(n, c, m)| ErrorCode {
147 name: n.into(),
148 code: c,
149 message: m.into(),
150 doc: None,
151 })
152 .collect(),
153 }),
154 modules: vec![],
155 }
156 }
157
158 fn api_with(mods: Vec<Module>) -> Api {
159 Api {
160 version: "0.3.0".into(),
161 package: None,
162 modules: mods,
163 generators: None,
164 }
165 }
166
167 #[test]
168 fn type_name_avoids_screaming_and_doubling() {
169 assert_eq!(type_name("KEY_NOT_FOUND", "Error"), "KeyNotFoundError");
170 assert_eq!(
171 type_name("KEY_NOT_FOUND", "Exception"),
172 "KeyNotFoundException"
173 );
174 assert_eq!(type_name("AlreadyError", "Error"), "AlreadyError");
175 assert_eq!(type_name("invalid_input", "Error"), "InvalidInputError");
176 }
177
178 #[test]
179 fn member_spellings() {
180 let e = ResolvedError {
181 raw_name: "KEY_NOT_FOUND".into(),
182 code: 1,
183 message: "nope".into(),
184 doc: None,
185 };
186 assert_eq!(e.error_class(), "KeyNotFoundError");
187 assert_eq!(e.exception_class(), "KeyNotFoundException");
188 assert_eq!(e.camel(), "keyNotFound");
189 assert_eq!(e.pascal(), "KeyNotFound");
190 assert_eq!(e.shouty(), "KEY_NOT_FOUND");
191 }
192
193 #[test]
194 fn flattens_and_dedups_across_modules() {
195 let api = api_with(vec![
196 module_with_errors("a", vec![("NOT_FOUND", 1, "x"), ("DENIED", 2, "y")]),
197 module_with_errors("b", vec![("NOT_FOUND", 1, "x"), ("TIMEOUT", 3, "z")]),
198 ]);
199 let codes = all(&api);
200 let names: Vec<_> = codes.iter().map(|c| c.raw_name.as_str()).collect();
201 assert_eq!(names, vec!["NOT_FOUND", "DENIED", "TIMEOUT"]);
202 assert!(has_domains(&api));
203 }
204
205 #[test]
206 fn no_domains_is_empty() {
207 let api = api_with(vec![Module {
208 name: "m".into(),
209 functions: vec![],
210 structs: vec![],
211 enums: vec![],
212 callbacks: vec![],
213 listeners: vec![],
214 errors: None,
215 modules: vec![],
216 }]);
217 assert!(all(&api).is_empty());
218 assert!(!has_domains(&api));
219 }
220}