fastskill_core/core/
update.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum UpdateStrategy {
13 Latest,
15 Patch,
17 Minor,
19 Major,
21 Exact(String),
23}
24
25#[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#[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
50pub struct UpdateService {
52 resolver: Arc<PackageResolver>,
53 lock: SkillsLock,
54}
55
56impl UpdateService {
57 pub fn new(resolver: Arc<PackageResolver>, lock: SkillsLock) -> Self {
59 Self { resolver, lock }
60 }
61
62 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 fn check_skill_update(
87 &self,
88 locked_skill: &LockedSkillEntry,
89 strategy: &UpdateStrategy,
90 ) -> Result<UpdateInfo, UpdateError> {
91 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 let target_version = match strategy {
100 UpdateStrategy::Latest => {
101 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 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 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 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 if !is_newer(&target_version.version, &locked_skill.version)? {
187 return Err(UpdateError::NoUpdateAvailable(locked_skill.id.clone()));
188 }
189
190 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 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 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}