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