Skip to main content

fastskill_core/core/
update.rs

1//! Update service for managing skill updates
2
3use crate::core::lock::{ProjectLockedSkillEntry as LockedSkillEntry, SkillsLock};
4use crate::core::resolver::{ConflictStrategy, PackageResolver, ResolutionResult};
5use crate::core::version::{compare_versions, is_newer, VersionError};
6use semver::Version;
7use std::sync::Arc;
8use thiserror::Error;
9
10/// Update strategy
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum UpdateStrategy {
13    /// Update to latest available version
14    Latest,
15    /// Update only patch versions (1.2.3 -> 1.2.4)
16    Patch,
17    /// Update minor and patch (1.2.3 -> 1.3.0)
18    Minor,
19    /// Update to latest major version (1.2.3 -> 2.0.0)
20    Major,
21    /// Update to specific version only
22    Exact(String),
23}
24
25/// Update information
26#[derive(Debug, Clone)]
27pub struct UpdateInfo {
28    pub skill_id: String,
29    pub current_version: String,
30    pub available_version: String,
31    pub resolution: ResolutionResult,
32}
33
34/// Update service errors
35#[derive(Debug, Error)]
36pub enum UpdateError {
37    #[error("Version error: {0}")]
38    VersionError(#[from] VersionError),
39
40    #[error("Resolver error: {0}")]
41    ResolverError(String),
42
43    #[error("No update available for skill: {0}")]
44    NoUpdateAvailable(String),
45
46    #[error("Update strategy not applicable: {0}")]
47    StrategyNotApplicable(String),
48}
49
50/// Update service for checking and applying updates
51pub struct UpdateService {
52    resolver: Arc<PackageResolver>,
53    lock: SkillsLock,
54}
55
56impl UpdateService {
57    /// Create a new update service
58    pub fn new(resolver: Arc<PackageResolver>, lock: SkillsLock) -> Self {
59        Self { resolver, lock }
60    }
61
62    /// Check for available updates
63    pub fn check_updates(
64        &self,
65        skill_id: Option<&str>,
66        strategy: UpdateStrategy,
67    ) -> Result<Vec<UpdateInfo>, UpdateError> {
68        let skills_to_check: Vec<&LockedSkillEntry> = if let Some(id) = skill_id {
69            self.lock.skills.iter().filter(|s| s.id == id).collect()
70        } else {
71            self.lock.skills.iter().collect()
72        };
73
74        let mut updates = Vec::new();
75
76        for locked_skill in skills_to_check {
77            if let Ok(update_info) = self.check_skill_update(locked_skill, &strategy) {
78                updates.push(update_info);
79            }
80        }
81
82        Ok(updates)
83    }
84
85    /// Check if a specific skill has an update available
86    fn check_skill_update(
87        &self,
88        locked_skill: &LockedSkillEntry,
89        strategy: &UpdateStrategy,
90    ) -> Result<UpdateInfo, UpdateError> {
91        // Get available versions
92        let candidates = self.resolver.get_available_versions(&locked_skill.id);
93
94        if candidates.is_empty() {
95            return Err(UpdateError::NoUpdateAvailable(locked_skill.id.clone()));
96        }
97
98        // Find the best candidate based on strategy
99        let target_version = match strategy {
100            UpdateStrategy::Latest => {
101                // Find highest version
102                candidates
103                    .iter()
104                    .max_by(|a, b| {
105                        compare_versions(&a.version, &b.version)
106                            .unwrap_or(std::cmp::Ordering::Equal)
107                    })
108                    .ok_or_else(|| UpdateError::NoUpdateAvailable(locked_skill.id.clone()))?
109            }
110            UpdateStrategy::Patch => {
111                // Find highest patch version in same minor
112                let current = Version::parse(&locked_skill.version).map_err(|e| {
113                    UpdateError::VersionError(VersionError::ParseError(e.to_string()))
114                })?;
115
116                candidates
117                    .iter()
118                    .filter(|c| {
119                        if let Ok(candidate_ver) = Version::parse(&c.version) {
120                            candidate_ver.major == current.major
121                                && candidate_ver.minor == current.minor
122                                && candidate_ver > current
123                        } else {
124                            false
125                        }
126                    })
127                    .max_by(|a, b| {
128                        compare_versions(&a.version, &b.version)
129                            .unwrap_or(std::cmp::Ordering::Equal)
130                    })
131                    .ok_or_else(|| {
132                        UpdateError::StrategyNotApplicable(format!(
133                            "No patch update available for {}",
134                            locked_skill.id
135                        ))
136                    })?
137            }
138            UpdateStrategy::Minor => {
139                // Find highest version in same major
140                let current = Version::parse(&locked_skill.version).map_err(|e| {
141                    UpdateError::VersionError(VersionError::ParseError(e.to_string()))
142                })?;
143
144                candidates
145                    .iter()
146                    .filter(|c| {
147                        if let Ok(candidate_ver) = Version::parse(&c.version) {
148                            candidate_ver.major == current.major && candidate_ver > current
149                        } else {
150                            false
151                        }
152                    })
153                    .max_by(|a, b| {
154                        compare_versions(&a.version, &b.version)
155                            .unwrap_or(std::cmp::Ordering::Equal)
156                    })
157                    .ok_or_else(|| {
158                        UpdateError::StrategyNotApplicable(format!(
159                            "No minor update available for {}",
160                            locked_skill.id
161                        ))
162                    })?
163            }
164            UpdateStrategy::Major => {
165                // Find highest version overall
166                candidates
167                    .iter()
168                    .max_by(|a, b| {
169                        compare_versions(&a.version, &b.version)
170                            .unwrap_or(std::cmp::Ordering::Equal)
171                    })
172                    .ok_or_else(|| UpdateError::NoUpdateAvailable(locked_skill.id.clone()))?
173            }
174            UpdateStrategy::Exact(version) => candidates
175                .iter()
176                .find(|c| c.version == *version)
177                .ok_or_else(|| {
178                    UpdateError::StrategyNotApplicable(format!(
179                        "Exact version {} not available for {}",
180                        version, locked_skill.id
181                    ))
182                })?,
183        };
184
185        // Check if update is actually newer
186        if !is_newer(&target_version.version, &locked_skill.version)? {
187            return Err(UpdateError::NoUpdateAvailable(locked_skill.id.clone()));
188        }
189
190        // Resolve the skill to get full resolution info
191        let resolution = self
192            .resolver
193            .resolve_skill(
194                &locked_skill.id,
195                None,
196                Some(&target_version.source_name),
197                ConflictStrategy::Priority,
198            )
199            .map_err(|e| UpdateError::ResolverError(e.to_string()))?;
200
201        Ok(UpdateInfo {
202            skill_id: locked_skill.id.clone(),
203            current_version: locked_skill.version.clone(),
204            available_version: target_version.version.clone(),
205            resolution,
206        })
207    }
208
209    /// Resolve updates for multiple skills
210    pub fn resolve_updates(
211        &self,
212        skill_ids: &[String],
213        strategy: UpdateStrategy,
214    ) -> Result<Vec<UpdateInfo>, UpdateError> {
215        let mut updates = Vec::new();
216
217        for skill_id in skill_ids {
218            if let Some(locked_skill) = self.lock.skills.iter().find(|s| s.id == *skill_id) {
219                if let Ok(update_info) = self.check_skill_update(locked_skill, &strategy) {
220                    updates.push(update_info);
221                }
222            }
223        }
224
225        Ok(updates)
226    }
227}
228
229#[cfg(test)]
230#[allow(clippy::unwrap_used)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_update_strategy_parsing() {
236        // Test that strategies can be created
237        let _latest = UpdateStrategy::Latest;
238        let _patch = UpdateStrategy::Patch;
239        let _minor = UpdateStrategy::Minor;
240        let _major = UpdateStrategy::Major;
241        let _exact = UpdateStrategy::Exact("1.2.3".to_string());
242    }
243}