1use lingxia_provider::{BoxFuture, ProviderError};
2use serde::{Deserialize, Serialize};
3use std::cmp::Ordering;
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
8#[serde(rename_all = "lowercase")]
9pub enum ReleaseType {
10 #[default]
11 Release,
12 Preview,
13 Developer,
14}
15
16impl ReleaseType {
17 pub fn as_str(self) -> &'static str {
18 match self {
19 Self::Release => "release",
20 Self::Preview => "preview",
21 Self::Developer => "developer",
22 }
23 }
24}
25
26impl fmt::Display for ReleaseType {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.write_str(self.as_str())
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Version {
36 pub major: u32,
37 pub minor: u32,
38 pub patch: u32,
39}
40
41impl Version {
42 pub fn parse(version_str: &str) -> Result<Self, VersionError> {
43 let parts: Vec<&str> = version_str.split('.').collect();
44 if parts.len() != 3 {
45 return Err(VersionError::InvalidFormat);
46 }
47
48 let major = parts[0]
49 .parse()
50 .map_err(|_| VersionError::InvalidComponent)?;
51 let minor = parts.get(1).map_or(Ok(0), |s| {
52 s.parse().map_err(|_| VersionError::InvalidComponent)
53 })?;
54 let patch = parts.get(2).map_or(Ok(0), |s| {
55 s.parse().map_err(|_| VersionError::InvalidComponent)
56 })?;
57
58 Ok(Self {
59 major,
60 minor,
61 patch,
62 })
63 }
64}
65
66impl FromStr for Version {
67 type Err = VersionError;
68
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 Self::parse(s)
71 }
72}
73
74impl fmt::Display for Version {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
77 }
78}
79
80impl PartialOrd for Version {
81 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
82 Some(self.cmp(other))
83 }
84}
85
86impl Ord for Version {
87 fn cmp(&self, other: &Self) -> Ordering {
88 match self.major.cmp(&other.major) {
89 Ordering::Equal => match self.minor.cmp(&other.minor) {
90 Ordering::Equal => self.patch.cmp(&other.patch),
91 ordering => ordering,
92 },
93 ordering => ordering,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
99pub enum VersionError {
100 #[error("invalid version format, expected 'major.minor.patch'")]
101 InvalidFormat,
102 #[error("invalid version component, expected unsigned integer")]
103 InvalidComponent,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub struct SemanticVersion {
108 pub major: u32,
109 pub minor: u32,
110 pub patch: u32,
111}
112
113impl SemanticVersion {
114 pub fn from_version(version: &Version) -> Self {
115 Self {
116 major: version.major,
117 minor: version.minor,
118 patch: version.patch,
119 }
120 }
121
122 pub fn to_version_string(&self) -> String {
123 format!("{}.{}.{}", self.major, self.minor, self.patch)
124 }
125}
126
127impl fmt::Display for SemanticVersion {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum LxAppUpdateQuery {
135 Latest { current_version: Option<String> },
136 TargetVersion(String),
137}
138
139impl LxAppUpdateQuery {
140 pub fn latest(current_version: Option<impl Into<String>>) -> Self {
141 Self::Latest {
142 current_version: current_version.map(Into::into),
143 }
144 }
145
146 pub fn target_version(version: impl Into<String>) -> Self {
147 Self::TargetVersion(version.into())
148 }
149}
150
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub enum UpdateTarget {
153 App {
154 current_version: Option<String>,
155 },
156 LxApp {
157 id: String,
158 release_type: ReleaseType,
159 query: LxAppUpdateQuery,
160 },
161 Plugin {
162 id: String,
163 version: String,
164 },
165}
166
167impl UpdateTarget {
168 pub fn app(current_version: Option<impl Into<String>>) -> Self {
169 Self::App {
170 current_version: current_version.map(Into::into),
171 }
172 }
173
174 pub fn lxapp(
175 id: impl Into<String>,
176 release_type: ReleaseType,
177 query: LxAppUpdateQuery,
178 ) -> Self {
179 Self::LxApp {
180 id: id.into(),
181 release_type,
182 query,
183 }
184 }
185
186 pub fn plugin(id: impl Into<String>, version: impl Into<String>) -> Self {
187 Self::Plugin {
188 id: id.into(),
189 version: version.into(),
190 }
191 }
192
193 pub fn scope_key(&self) -> String {
195 match self {
196 Self::App { .. } => "app".to_string(),
197 Self::LxApp {
198 id, release_type, ..
199 } => format!("lxapp:{id}@{}", release_type.as_str()),
200 Self::Plugin { id, version } => format!("plugin:{id}@{version}"),
201 }
202 }
203}
204
205#[derive(Clone, Debug)]
206pub struct UpdatePackageInfo {
207 pub version: String,
208 pub url: String,
209 pub checksum_sha256: String,
210 pub size: Option<u64>,
211 pub release_notes: Option<Vec<String>>,
212 pub is_force_update: bool,
213 pub required_runtime_version: Option<String>,
214}
215
216impl UpdatePackageInfo {
217 pub fn should_replace_version(
218 candidate_version: &str,
219 installed_version: Option<&str>,
220 ) -> bool {
221 installed_version != Some(candidate_version)
222 }
223
224 pub fn should_replace_installed_version(&self, installed_version: Option<&str>) -> bool {
225 Self::should_replace_version(&self.version, installed_version)
226 }
227
228 pub fn required_runtime_version_trimmed(&self) -> Option<&str> {
229 self.required_runtime_version
230 .as_deref()
231 .map(str::trim)
232 .filter(|value| !value.is_empty())
233 }
234
235 pub fn ensure_runtime_compatible(
236 &self,
237 current_runtime_version: &str,
238 target_name: &str,
239 ) -> Result<(), RuntimeCompatibilityError> {
240 let Some(required_runtime_version) = self.required_runtime_version_trimmed() else {
241 return Ok(());
242 };
243
244 let current = Version::parse(current_runtime_version).map_err(|_| {
245 RuntimeCompatibilityError::InvalidCurrentRuntimeVersion {
246 runtime_version: current_runtime_version.to_string(),
247 }
248 })?;
249 let required = Version::parse(required_runtime_version).map_err(|_| {
250 RuntimeCompatibilityError::InvalidRequiredRuntimeVersion {
251 target: target_name.to_string(),
252 update_version: self.version.clone(),
253 runtime_version: required_runtime_version.to_string(),
254 }
255 })?;
256
257 if current < required {
258 return Err(RuntimeCompatibilityError::RequiresRuntimeUpgrade {
259 target: target_name.to_string(),
260 update_version: self.version.clone(),
261 required_runtime_version: required.to_string(),
262 current_runtime_version: current.to_string(),
263 });
264 }
265
266 Ok(())
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
271pub enum RuntimeCompatibilityError {
272 #[error("invalid SDK runtime version '{runtime_version}'")]
273 InvalidCurrentRuntimeVersion { runtime_version: String },
274 #[error(
275 "invalid minRuntimeVersion '{runtime_version}' from update metadata for {target}@{update_version}"
276 )]
277 InvalidRequiredRuntimeVersion {
278 target: String,
279 update_version: String,
280 runtime_version: String,
281 },
282 #[error(
283 "{target} update {update_version} requires runtime >= {required_runtime_version}, current SDK runtime is {current_runtime_version}; update host app first"
284 )]
285 RequiresRuntimeUpgrade {
286 target: String,
287 update_version: String,
288 required_runtime_version: String,
289 current_runtime_version: String,
290 },
291}
292
293pub trait UpdateProvider: Send + Sync + 'static {
295 fn check_update<'a>(
298 &'a self,
299 target: UpdateTarget,
300 ) -> BoxFuture<'a, Result<Option<UpdatePackageInfo>, ProviderError>>;
301}
302
303#[cfg(test)]
304mod tests {
305 use super::Version;
306
307 #[test]
308 fn version_parse_accepts_full_semver_only() {
309 assert!(Version::parse("1.2.3").is_ok());
310 assert!(Version::parse("1").is_err());
311 assert!(Version::parse("1.2").is_err());
312 assert!(Version::parse("1.2.3.4").is_err());
313 }
314}