sps_common/model/
formula.rs

1// sps-core/src/model/formula.rs
2// *** Corrected: Removed derive Deserialize from ResourceSpec, removed unused SpsError import,
3// added ResourceSpec struct and parsing ***
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use semver::Version;
9use serde::{de, Deserialize, Deserializer, Serialize};
10use serde_json::Value;
11use tracing::{debug, error};
12
13use crate::dependency::{Dependency, DependencyTag, Requirement};
14use crate::error::Result; // <-- Import only Result // Use log crate imports
15
16// --- Resource Spec Struct ---
17// *** Added struct definition, REMOVED #[derive(Deserialize)] ***
18#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
19pub struct ResourceSpec {
20    pub name: String,
21    pub url: String,
22    pub sha256: String,
23    // Add other potential fields like version if needed later
24}
25
26// --- Bottle Related Structs (Original structure) ---
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct BottleFileSpec {
29    pub url: String,
30    pub sha256: String,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
34pub struct BottleSpec {
35    pub stable: Option<BottleStableSpec>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
39pub struct BottleStableSpec {
40    pub rebuild: u32,
41    #[serde(default)]
42    pub files: HashMap<String, BottleFileSpec>,
43}
44
45// --- Formula Version Struct (Original structure) ---
46#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, Eq)]
47pub struct FormulaVersions {
48    pub stable: Option<String>,
49    pub head: Option<String>,
50    #[serde(default)]
51    pub bottle: bool,
52}
53
54// --- Main Formula Struct ---
55// *** Added 'resources' field ***
56#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
57pub struct Formula {
58    pub name: String,
59    pub stable_version_str: String,
60    #[serde(rename = "versions")]
61    pub version_semver: Version,
62    #[serde(default)]
63    pub revision: u32,
64    #[serde(default)]
65    pub desc: Option<String>,
66    #[serde(default)]
67    pub homepage: Option<String>,
68    #[serde(default)]
69    pub url: String,
70    #[serde(default)]
71    pub sha256: String,
72    #[serde(default)]
73    pub mirrors: Vec<String>,
74    #[serde(default)]
75    pub bottle: BottleSpec,
76    #[serde(skip_deserializing)]
77    pub dependencies: Vec<Dependency>,
78    #[serde(default, deserialize_with = "deserialize_requirements")]
79    pub requirements: Vec<Requirement>,
80    #[serde(skip_deserializing)] // Skip direct deserialization for this field
81    pub resources: Vec<ResourceSpec>, // Stores parsed resources
82    #[serde(skip)]
83    pub install_keg_path: Option<PathBuf>,
84}
85
86// Custom deserialization logic for Formula
87impl<'de> Deserialize<'de> for Formula {
88    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
89    where
90        D: Deserializer<'de>,
91    {
92        // Temporary struct reflecting the JSON structure more closely
93        // *** Added 'resources' field to capture raw JSON Value ***
94        #[derive(Deserialize, Debug)]
95        struct RawFormulaData {
96            name: String,
97            #[serde(default)]
98            revision: u32,
99            desc: Option<String>,
100            homepage: Option<String>,
101            versions: FormulaVersions,
102            #[serde(default)]
103            url: String,
104            #[serde(default)]
105            sha256: String,
106            #[serde(default)]
107            mirrors: Vec<String>,
108            #[serde(default)]
109            bottle: BottleSpec,
110            #[serde(default)]
111            dependencies: Vec<String>,
112            #[serde(default)]
113            build_dependencies: Vec<String>,
114            #[serde(default)]
115            test_dependencies: Vec<String>,
116            #[serde(default)]
117            recommended_dependencies: Vec<String>,
118            #[serde(default)]
119            optional_dependencies: Vec<String>,
120            #[serde(default, deserialize_with = "deserialize_requirements")]
121            requirements: Vec<Requirement>,
122            #[serde(default)]
123            resources: Vec<Value>, // Capture resources as generic Value first
124            #[serde(default)]
125            urls: Option<Value>,
126        }
127
128        let raw: RawFormulaData = RawFormulaData::deserialize(deserializer)?;
129
130        // --- Version Parsing (Original logic) ---
131        let stable_version_str = raw
132            .versions
133            .stable
134            .clone()
135            .ok_or_else(|| de::Error::missing_field("versions.stable"))?;
136        let version_semver = match crate::model::version::Version::parse(&stable_version_str) {
137            Ok(v) => v.into(),
138            Err(_) => {
139                let mut majors = 0u32;
140                let mut minors = 0u32;
141                let mut patches = 0u32;
142                let mut part_count = 0;
143                for (i, part) in stable_version_str.split('.').enumerate() {
144                    let numeric_part = part
145                        .chars()
146                        .take_while(|c| c.is_ascii_digit())
147                        .collect::<String>();
148                    if numeric_part.is_empty() && i > 0 {
149                        break;
150                    }
151                    if numeric_part.len() < part.len() && i > 0 {
152                        if let Ok(num) = numeric_part.parse::<u32>() {
153                            match i {
154                                0 => majors = num,
155                                1 => minors = num,
156                                2 => patches = num,
157                                _ => {}
158                            }
159                            part_count += 1;
160                        }
161                        break;
162                    }
163                    if let Ok(num) = numeric_part.parse::<u32>() {
164                        match i {
165                            0 => majors = num,
166                            1 => minors = num,
167                            2 => patches = num,
168                            _ => {}
169                        }
170                        part_count += 1;
171                    }
172                    if i >= 2 {
173                        break;
174                    }
175                }
176                let version_str_padded = match part_count {
177                    1 => format!("{majors}.0.0"),
178                    2 => format!("{majors}.{minors}.0"),
179                    _ => format!("{majors}.{minors}.{patches}"),
180                };
181                match Version::parse(&version_str_padded) {
182                    Ok(v) => v,
183                    Err(_) => {
184                        error!(
185                            "Warning: Could not parse version '{}' (sanitized to '{}') for formula '{}'. Using 0.0.0.",
186                            stable_version_str, version_str_padded, raw.name
187                        );
188                        Version::new(0, 0, 0)
189                    }
190                }
191            }
192        };
193
194        // --- URL/SHA256 Logic (Original logic) ---
195        let mut final_url = raw.url;
196        let mut final_sha256 = raw.sha256;
197        if final_url.is_empty() {
198            if let Some(Value::Object(urls_map)) = raw.urls {
199                if let Some(Value::Object(stable_url_info)) = urls_map.get("stable") {
200                    if let Some(Value::String(u)) = stable_url_info.get("url") {
201                        final_url = u.clone();
202                    }
203                    if let Some(Value::String(s)) = stable_url_info
204                        .get("checksum")
205                        .or_else(|| stable_url_info.get("sha256"))
206                    {
207                        final_sha256 = s.clone();
208                    }
209                }
210            }
211        }
212        if final_url.is_empty() && raw.versions.head.is_none() {
213            debug!("Warning: Formula '{}' has no stable URL defined.", raw.name);
214        }
215
216        // --- Dependency Processing (Original logic) ---
217        let mut combined_dependencies: Vec<Dependency> = Vec::new();
218        let mut seen_deps: HashMap<String, DependencyTag> = HashMap::new();
219        let mut process_list = |deps: &[String], tag: DependencyTag| {
220            for name in deps {
221                *seen_deps
222                    .entry(name.clone())
223                    .or_insert(DependencyTag::empty()) |= tag;
224            }
225        };
226        process_list(&raw.dependencies, DependencyTag::RUNTIME);
227        process_list(&raw.build_dependencies, DependencyTag::BUILD);
228        process_list(&raw.test_dependencies, DependencyTag::TEST);
229        process_list(
230            &raw.recommended_dependencies,
231            DependencyTag::RECOMMENDED | DependencyTag::RUNTIME,
232        );
233        process_list(
234            &raw.optional_dependencies,
235            DependencyTag::OPTIONAL | DependencyTag::RUNTIME,
236        );
237        for (name, tags) in seen_deps {
238            combined_dependencies.push(Dependency::new_with_tags(name, tags));
239        }
240
241        // --- Resource Processing ---
242        // *** Added parsing logic for the 'resources' field ***
243        let mut combined_resources: Vec<ResourceSpec> = Vec::new();
244        for res_val in raw.resources {
245            // Homebrew API JSON format puts resource spec inside a keyed object
246            // e.g., { "resource_name": { "url": "...", "sha256": "..." } }
247            if let Value::Object(map) = res_val {
248                // Assume only one key-value pair per object in the array
249                if let Some((res_name, res_spec_val)) = map.into_iter().next() {
250                    // Use the manual Deserialize impl for ResourceSpec
251                    match ResourceSpec::deserialize(res_spec_val.clone()) {
252                        // Use ::deserialize
253                        Ok(mut res_spec) => {
254                            // Inject the name from the key if missing
255                            if res_spec.name.is_empty() {
256                                res_spec.name = res_name;
257                            } else if res_spec.name != res_name {
258                                debug!(
259                                    "Resource name mismatch in formula '{}': key '{}' vs spec '{}'. Using key.",
260                                    raw.name, res_name, res_spec.name
261                                );
262                                res_spec.name = res_name; // Prefer key name
263                            }
264                            // Ensure required fields are present
265                            if res_spec.url.is_empty() || res_spec.sha256.is_empty() {
266                                debug!(
267                                    "Resource '{}' for formula '{}' is missing URL or SHA256. Skipping.",
268                                    res_spec.name, raw.name
269                                );
270                                continue;
271                            }
272                            debug!(
273                                "Parsed resource '{}' for formula '{}'",
274                                res_spec.name, raw.name
275                            );
276                            combined_resources.push(res_spec);
277                        }
278                        Err(e) => {
279                            // Use display for the error which comes from serde::de::Error::custom
280                            debug!(
281                                "Failed to parse resource spec value for key '{}' in formula '{}': {}. Value: {:?}",
282                                res_name, raw.name, e, res_spec_val
283                            );
284                        }
285                    }
286                } else {
287                    debug!("Empty resource object found in formula '{}'.", raw.name);
288                }
289            } else {
290                debug!(
291                    "Unexpected format for resource entry in formula '{}': expected object, got {:?}",
292                    raw.name, res_val
293                );
294            }
295        }
296
297        Ok(Self {
298            name: raw.name,
299            stable_version_str,
300            version_semver,
301            revision: raw.revision,
302            desc: raw.desc,
303            homepage: raw.homepage,
304            url: final_url,
305            sha256: final_sha256,
306            mirrors: raw.mirrors,
307            bottle: raw.bottle,
308            dependencies: combined_dependencies,
309            requirements: raw.requirements,
310            resources: combined_resources, // Assign parsed resources
311            install_keg_path: None,
312        })
313    }
314}
315
316// --- Formula impl Methods ---
317impl Formula {
318    // dependencies() and requirements() are unchanged
319    pub fn dependencies(&self) -> Result<Vec<Dependency>> {
320        Ok(self.dependencies.clone())
321    }
322    pub fn requirements(&self) -> Result<Vec<Requirement>> {
323        Ok(self.requirements.clone())
324    }
325
326    // *** Added: Returns a clone of the defined resources. ***
327    pub fn resources(&self) -> Result<Vec<ResourceSpec>> {
328        Ok(self.resources.clone())
329    }
330
331    // Other methods (set_keg_path, version_str_full, accessors) are unchanged
332    pub fn set_keg_path(&mut self, path: PathBuf) {
333        self.install_keg_path = Some(path);
334    }
335    pub fn version_str_full(&self) -> String {
336        if self.revision > 0 {
337            format!("{}_{}", self.stable_version_str, self.revision)
338        } else {
339            self.stable_version_str.clone()
340        }
341    }
342    pub fn name(&self) -> &str {
343        &self.name
344    }
345    pub fn version(&self) -> &Version {
346        &self.version_semver
347    }
348    pub fn source_url(&self) -> &str {
349        &self.url
350    }
351    pub fn source_sha256(&self) -> &str {
352        &self.sha256
353    }
354    pub fn get_bottle_spec(&self, bottle_tag: &str) -> Option<&BottleFileSpec> {
355        self.bottle.stable.as_ref()?.files.get(bottle_tag)
356    }
357}
358
359// --- BuildEnvironment Dependency Interface (Unchanged) ---
360pub trait FormulaDependencies {
361    fn name(&self) -> &str;
362    fn install_prefix(&self, cellar_path: &Path) -> Result<PathBuf>;
363    fn resolved_runtime_dependency_paths(&self) -> Result<Vec<PathBuf>>;
364    fn resolved_build_dependency_paths(&self) -> Result<Vec<PathBuf>>;
365    fn all_resolved_dependency_paths(&self) -> Result<Vec<PathBuf>>;
366}
367impl FormulaDependencies for Formula {
368    fn name(&self) -> &str {
369        &self.name
370    }
371    fn install_prefix(&self, cellar_path: &Path) -> Result<PathBuf> {
372        let version_string = self.version_str_full();
373        Ok(cellar_path.join(self.name()).join(version_string))
374    }
375    fn resolved_runtime_dependency_paths(&self) -> Result<Vec<PathBuf>> {
376        Ok(Vec::new())
377    }
378    fn resolved_build_dependency_paths(&self) -> Result<Vec<PathBuf>> {
379        Ok(Vec::new())
380    }
381    fn all_resolved_dependency_paths(&self) -> Result<Vec<PathBuf>> {
382        Ok(Vec::new())
383    }
384}
385
386// --- Deserialization Helpers ---
387// deserialize_requirements remains unchanged
388fn deserialize_requirements<'de, D>(
389    deserializer: D,
390) -> std::result::Result<Vec<Requirement>, D::Error>
391where
392    D: serde::Deserializer<'de>,
393{
394    #[derive(Deserialize, Debug)]
395    struct ReqWrapper {
396        #[serde(default)]
397        name: String,
398        #[serde(default)]
399        version: Option<String>,
400        #[serde(default)]
401        cask: Option<String>,
402        #[serde(default)]
403        download: Option<String>,
404    }
405    let raw_reqs: Vec<Value> = Deserialize::deserialize(deserializer)?;
406    let mut requirements = Vec::new();
407    for req_val in raw_reqs {
408        if let Ok(req_obj) = serde_json::from_value::<ReqWrapper>(req_val.clone()) {
409            match req_obj.name.as_str() {
410                "macos" => {
411                    requirements.push(Requirement::MacOS(
412                        req_obj.version.unwrap_or_else(|| "any".to_string()),
413                    ));
414                }
415                "xcode" => {
416                    requirements.push(Requirement::Xcode(
417                        req_obj.version.unwrap_or_else(|| "any".to_string()),
418                    ));
419                }
420                "cask" => {
421                    requirements.push(Requirement::Other(format!(
422                        "Cask Requirement: {}",
423                        req_obj.cask.unwrap_or_else(|| "?".to_string())
424                    )));
425                }
426                "download" => {
427                    requirements.push(Requirement::Other(format!(
428                        "Download Requirement: {}",
429                        req_obj.download.unwrap_or_else(|| "?".to_string())
430                    )));
431                }
432                _ => requirements.push(Requirement::Other(format!(
433                    "Unknown requirement type: {req_obj:?}"
434                ))),
435            }
436        } else if let Value::String(req_str) = req_val {
437            match req_str.as_str() {
438                "macos" => requirements.push(Requirement::MacOS("latest".to_string())),
439                "xcode" => requirements.push(Requirement::Xcode("latest".to_string())),
440                _ => {
441                    requirements.push(Requirement::Other(format!("Simple requirement: {req_str}")))
442                }
443            }
444        } else {
445            debug!("Warning: Could not parse requirement: {:?}", req_val);
446            requirements.push(Requirement::Other(format!(
447                "Unparsed requirement: {req_val:?}"
448            )));
449        }
450    }
451    Ok(requirements)
452}
453
454// Manual impl Deserialize for ResourceSpec (unchanged, this is needed)
455impl<'de> Deserialize<'de> for ResourceSpec {
456    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
457    where
458        D: Deserializer<'de>,
459    {
460        #[derive(Deserialize)]
461        struct Helper {
462            #[serde(default)]
463            name: String, // name is often the key, not in the value
464            url: String,
465            sha256: String,
466        }
467        let helper = Helper::deserialize(deserializer)?;
468        // Note: The actual resource name comes from the key in the map during Formula
469        // deserialization
470        Ok(Self {
471            name: helper.name,
472            url: helper.url,
473            sha256: helper.sha256,
474        })
475    }
476}