1use weaveffi_ir::ir::Api;
15
16pub const DEFAULT_VERSION: &str = "0.1.0";
18
19pub const DEFAULT_NAME: &str = "weaveffi";
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ResolvedPackage {
26 pub name: String,
28 pub version: String,
30 pub description: Option<String>,
31 pub license: Option<String>,
32 pub authors: Vec<String>,
33 pub homepage: Option<String>,
34 pub repository: Option<String>,
35}
36
37impl ResolvedPackage {
38 pub fn description_or_default(&self) -> String {
40 self.description
41 .clone()
42 .filter(|s| !s.is_empty())
43 .unwrap_or_else(|| format!("{} bindings generated by WeaveFFI", self.name))
44 }
45
46 pub fn ident_name(&self) -> String {
50 sanitize_ident(&self.name)
51 }
52
53 pub fn module_name(&self) -> String {
57 pascal_ident(&self.name)
58 }
59}
60
61pub fn pascal_ident(name: &str) -> String {
65 let mut out = String::with_capacity(name.len());
66 let mut start_word = true;
67 for ch in name.chars() {
68 if ch.is_ascii_alphanumeric() {
69 if start_word {
70 out.push(ch.to_ascii_uppercase());
71 } else {
72 out.push(ch);
73 }
74 start_word = false;
75 } else {
76 start_word = true;
77 }
78 }
79 if out.is_empty() {
80 pascal_ident(DEFAULT_NAME)
81 } else {
82 out
83 }
84}
85
86pub fn sanitize_ident(name: &str) -> String {
88 let mut out = String::with_capacity(name.len());
89 let mut prev_us = false;
90 for ch in name.chars() {
91 if ch.is_ascii_alphanumeric() {
92 out.push(ch.to_ascii_lowercase());
93 prev_us = false;
94 } else if !prev_us && !out.is_empty() {
95 out.push('_');
96 prev_us = true;
97 }
98 }
99 let trimmed = out.trim_end_matches('_');
100 if trimmed.is_empty() {
101 DEFAULT_NAME.to_string()
102 } else {
103 trimmed.to_string()
104 }
105}
106
107pub fn name_from_basename(basename: Option<&str>) -> String {
111 basename
112 .and_then(|b| b.rsplit(['/', '\\']).next())
113 .map(|b| b.split('.').next().unwrap_or(b))
114 .filter(|s| !s.is_empty())
115 .unwrap_or(DEFAULT_NAME)
116 .to_string()
117}
118
119pub fn resolve(
130 api: &Api,
131 name_override: Option<&str>,
132 input_basename: Option<&str>,
133) -> ResolvedPackage {
134 let pkg = api.package.as_ref();
135 let name = name_override
136 .map(str::trim)
137 .filter(|s| !s.is_empty())
138 .map(str::to_string)
139 .or_else(|| {
140 pkg.map(|p| p.name.trim().to_string())
141 .filter(|s| !s.is_empty())
142 })
143 .unwrap_or_else(|| name_from_basename(input_basename));
144 let version = pkg
145 .map(|p| p.version.trim().to_string())
146 .filter(|s| !s.is_empty())
147 .unwrap_or_else(|| DEFAULT_VERSION.to_string());
148 ResolvedPackage {
149 name,
150 version,
151 description: pkg
152 .and_then(|p| p.description.clone())
153 .filter(|s| !s.is_empty()),
154 license: pkg
155 .and_then(|p| p.license.clone())
156 .filter(|s| !s.is_empty()),
157 authors: pkg.map(|p| p.authors.clone()).unwrap_or_default(),
158 homepage: pkg
159 .and_then(|p| p.homepage.clone())
160 .filter(|s| !s.is_empty()),
161 repository: pkg
162 .and_then(|p| p.repository.clone())
163 .filter(|s| !s.is_empty()),
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use weaveffi_ir::ir::Package;
171
172 fn api_with(pkg: Option<Package>) -> Api {
173 Api {
174 version: "0.3.0".into(),
175 package: pkg,
176 modules: vec![],
177 generators: None,
178 }
179 }
180
181 fn full_pkg() -> Package {
182 Package {
183 name: "kvstore".into(),
184 version: "1.2.0".into(),
185 description: Some("KV store".into()),
186 license: Some("MIT".into()),
187 authors: vec!["Ada".into()],
188 homepage: Some("https://example.com".into()),
189 repository: Some("https://github.com/x/kvstore".into()),
190 }
191 }
192
193 #[test]
194 fn package_block_drives_identity() {
195 let api = api_with(Some(full_pkg()));
196 let r = resolve(&api, None, Some("ignored.yml"));
197 assert_eq!(r.name, "kvstore");
198 assert_eq!(r.version, "1.2.0");
199 assert_eq!(r.license.as_deref(), Some("MIT"));
200 assert_eq!(r.authors, vec!["Ada".to_string()]);
201 }
202
203 #[test]
204 fn target_override_beats_package_name() {
205 let api = api_with(Some(full_pkg()));
206 let r = resolve(&api, Some("kvstore_py"), Some("kvstore.yml"));
207 assert_eq!(r.name, "kvstore_py");
208 assert_eq!(r.version, "1.2.0");
210 }
211
212 #[test]
213 fn falls_back_to_file_stem_then_default() {
214 let api = api_with(None);
215 let r = resolve(&api, None, Some("path/to/contacts.yml"));
216 assert_eq!(r.name, "contacts");
217 assert_eq!(r.version, DEFAULT_VERSION);
218
219 let r2 = resolve(&api, None, None);
220 assert_eq!(r2.name, DEFAULT_NAME);
221 }
222
223 #[test]
224 fn description_default_is_generated() {
225 let api = api_with(None);
226 let r = resolve(&api, Some("widgets"), None);
227 assert_eq!(
228 r.description_or_default(),
229 "widgets bindings generated by WeaveFFI"
230 );
231 }
232
233 #[test]
234 fn ident_name_sanitizes() {
235 assert_eq!(sanitize_ident("my-kv.store"), "my_kv_store");
236 assert_eq!(sanitize_ident("Kvstore"), "kvstore");
237 assert_eq!(sanitize_ident("--"), DEFAULT_NAME);
238 }
239
240 #[test]
241 fn pascal_ident_upper_camels() {
242 assert_eq!(pascal_ident("my-kv.store"), "MyKvStore");
243 assert_eq!(pascal_ident("kvstore"), "Kvstore");
244 assert_eq!(pascal_ident("contacts"), "Contacts");
245 assert_eq!(pascal_ident("--"), "Weaveffi");
246 }
247}