use std::collections::BTreeSet;
use heck::{ToLowerCamelCase, ToShoutySnakeCase, ToUpperCamelCase};
use weaveffi_ir::ir::{Api, Module};
pub const BRAND_STEM: &str = "WeaveFFI";
pub const ERROR_BRAND: &str = "WeaveFFIError";
pub const EXCEPTION_BRAND: &str = "WeaveFFIException";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedError {
pub raw_name: String,
pub code: i32,
pub message: String,
pub doc: Option<String>,
}
impl ResolvedError {
pub fn error_class(&self) -> String {
type_name(&self.raw_name, "Error")
}
pub fn exception_class(&self) -> String {
type_name(&self.raw_name, "Exception")
}
pub fn camel(&self) -> String {
self.raw_name.to_lower_camel_case()
}
pub fn pascal(&self) -> String {
self.raw_name.to_upper_camel_case()
}
pub fn shouty(&self) -> String {
self.raw_name.to_shouty_snake_case()
}
}
pub fn pascal(raw: &str) -> String {
raw.to_upper_camel_case()
}
pub fn type_name(raw: &str, suffix: &str) -> String {
let pascal = raw.to_upper_camel_case();
if pascal.ends_with(suffix) {
pascal
} else {
format!("{pascal}{suffix}")
}
}
pub fn all(api: &Api) -> Vec<ResolvedError> {
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut out: Vec<ResolvedError> = Vec::new();
fn walk(mods: &[Module], seen: &mut BTreeSet<String>, out: &mut Vec<ResolvedError>) {
for m in mods {
if let Some(domain) = &m.errors {
for c in &domain.codes {
if seen.insert(c.name.clone()) {
out.push(ResolvedError {
raw_name: c.name.clone(),
code: c.code,
message: c.message.clone(),
doc: c.doc.clone(),
});
}
}
}
walk(&m.modules, seen, out);
}
}
walk(&api.modules, &mut seen, &mut out);
out
}
pub fn has_domains(api: &Api) -> bool {
fn any(mods: &[Module]) -> bool {
mods.iter().any(|m| m.errors.is_some() || any(&m.modules))
}
any(&api.modules)
}
#[cfg(test)]
mod tests {
use super::*;
use weaveffi_ir::ir::{ErrorCode, ErrorDomain, Module};
fn module_with_errors(name: &str, codes: Vec<(&str, i32, &str)>) -> Module {
Module {
name: name.into(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: Some(ErrorDomain {
name: format!("{name}Error"),
codes: codes
.into_iter()
.map(|(n, c, m)| ErrorCode {
name: n.into(),
code: c,
message: m.into(),
doc: None,
})
.collect(),
}),
modules: vec![],
}
}
fn api_with(mods: Vec<Module>) -> Api {
Api {
version: "0.3.0".into(),
package: None,
modules: mods,
generators: None,
}
}
#[test]
fn type_name_avoids_screaming_and_doubling() {
assert_eq!(type_name("KEY_NOT_FOUND", "Error"), "KeyNotFoundError");
assert_eq!(
type_name("KEY_NOT_FOUND", "Exception"),
"KeyNotFoundException"
);
assert_eq!(type_name("AlreadyError", "Error"), "AlreadyError");
assert_eq!(type_name("invalid_input", "Error"), "InvalidInputError");
}
#[test]
fn member_spellings() {
let e = ResolvedError {
raw_name: "KEY_NOT_FOUND".into(),
code: 1,
message: "nope".into(),
doc: None,
};
assert_eq!(e.error_class(), "KeyNotFoundError");
assert_eq!(e.exception_class(), "KeyNotFoundException");
assert_eq!(e.camel(), "keyNotFound");
assert_eq!(e.pascal(), "KeyNotFound");
assert_eq!(e.shouty(), "KEY_NOT_FOUND");
}
#[test]
fn flattens_and_dedups_across_modules() {
let api = api_with(vec![
module_with_errors("a", vec![("NOT_FOUND", 1, "x"), ("DENIED", 2, "y")]),
module_with_errors("b", vec![("NOT_FOUND", 1, "x"), ("TIMEOUT", 3, "z")]),
]);
let codes = all(&api);
let names: Vec<_> = codes.iter().map(|c| c.raw_name.as_str()).collect();
assert_eq!(names, vec!["NOT_FOUND", "DENIED", "TIMEOUT"]);
assert!(has_domains(&api));
}
#[test]
fn no_domains_is_empty() {
let api = api_with(vec![Module {
name: "m".into(),
functions: vec![],
structs: vec![],
enums: vec![],
callbacks: vec![],
listeners: vec![],
errors: None,
modules: vec![],
}]);
assert!(all(&api).is_empty());
assert!(!has_domains(&api));
}
}