1use crate::{Catalogue, catalogue::plugins_repo_url, error::*, manifest::PluginManifest};
2use semver::Version;
3use std::fs::File;
4
5pub struct PluginRef {
7 pub name: String,
8 pub version: Option<Version>,
9}
10
11impl PluginRef {
12 pub fn new(name: &str, version: Option<Version>) -> Self {
13 Self {
14 name: name.to_lowercase(),
15 version,
16 }
17 }
18
19 pub(crate) async fn resolve_manifest(
24 &self,
25 catalogue: &Catalogue,
26 skip_compatibility_check: bool,
27 spin_version: &str,
28 ) -> PluginLookupResult<PluginManifest> {
29 let exact = self.resolve_manifest_exact(catalogue).await?;
30 if skip_compatibility_check
31 || self.version.is_some()
32 || exact.is_compatible_spin_version(spin_version)
33 {
34 return Ok(exact);
35 }
36
37 let manifests = catalogue.manifests()?;
39 let relevant_manifests = manifests.into_iter().filter(|m| m.name() == self.name);
40 let compatible_manifests = relevant_manifests
41 .filter(|m| m.has_compatible_package() && m.is_compatible_spin_version(spin_version));
42 let highest_compatible_manifest =
43 compatible_manifests.max_by_key(|m| m.try_version().unwrap_or_else(|_| null_version()));
44
45 Ok(highest_compatible_manifest.unwrap_or(exact))
46 }
47
48 pub(crate) async fn resolve_manifest_exact(
52 &self,
53 catalogue: &Catalogue,
54 ) -> PluginLookupResult<PluginManifest> {
55 let url = plugins_repo_url()?;
56 tracing::info!("Pulling manifest for plugin {} from {url}", self.name);
57 catalogue.ensure_inited(&url).await.map_err(|e| {
58 Error::ConnectionFailed(ConnectionFailedError::new(url.to_string(), e.to_string()))
59 })?;
60
61 self.resolve_manifest_exact_from_good_repo(catalogue)
62 }
63
64 #[allow(clippy::let_and_return)]
67 fn resolve_manifest_exact_from_good_repo(
68 &self,
69 catalogue: &Catalogue,
70 ) -> PluginLookupResult<PluginManifest> {
71 let expected_path = catalogue.manifest_path(&self.name, &self.version);
72
73 let not_found = |e: std::io::Error| {
74 Err(Error::NotFound(NotFoundError::new(
75 Some(self.name.clone()),
76 expected_path.display().to_string(),
77 e.to_string(),
78 )))
79 };
80
81 let manifest = match File::open(&expected_path) {
82 Ok(file) => serde_json::from_reader(file).map_err(|e| {
83 Error::InvalidManifest(InvalidManifestError::new(
84 Some(self.name.clone()),
85 expected_path.display().to_string(),
86 e.to_string(),
87 ))
88 }),
89 Err(e) if e.kind() == std::io::ErrorKind::NotFound && self.version.is_some() => {
90 let latest = Self::new(&self.name, None);
93 match latest.resolve_manifest_exact_from_good_repo(catalogue) {
94 Ok(manifest) if manifest.try_version().ok() == self.version => Ok(manifest),
95 _ => not_found(e),
96 }
97 }
98 Err(e) => not_found(e),
99 };
100
101 manifest
102 }
103}
104
105fn null_version() -> semver::Version {
106 semver::Version::new(0, 0, 0)
107}
108
109#[cfg(test)]
110mod tests {
111 use std::path::PathBuf;
112
113 use super::*;
114 use crate::store::PluginStore;
115
116 const TEST_NAME: &str = "some-spin-ver-some-not";
117 const TESTS_STORE_DIR: &str = "tests";
118
119 fn tests_store_dir() -> PathBuf {
120 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TESTS_STORE_DIR)
121 }
122
123 fn tests_store() -> Catalogue {
124 PluginStore::new(tests_store_dir()).catalogue()
125 }
126
127 #[tokio::test]
128 async fn if_no_version_given_and_latest_is_compatible_then_latest() -> PluginLookupResult<()> {
129 let lookup = PluginRef::new(TEST_NAME, None);
130 let resolved = lookup
131 .resolve_manifest(&tests_store(), false, "99.0.0")
132 .await?;
133 assert_eq!("99.0.1", resolved.version);
134 Ok(())
135 }
136
137 #[tokio::test]
138 async fn if_no_version_given_and_latest_is_not_compatible_then_highest_compatible()
139 -> PluginLookupResult<()> {
140 let lookup = PluginRef::new(TEST_NAME, None);
144 let resolved = lookup
145 .resolve_manifest(&tests_store(), false, "98.0.0")
146 .await?;
147 assert_eq!("98.0.0", resolved.version);
148 Ok(())
149 }
150
151 #[tokio::test]
152 async fn if_version_given_it_gets_used_regardless() -> PluginLookupResult<()> {
153 let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("99.0.0").unwrap()));
154 let resolved = lookup
155 .resolve_manifest(&tests_store(), false, "98.0.0")
156 .await?;
157 assert_eq!("99.0.0", resolved.version);
158 Ok(())
159 }
160
161 #[tokio::test]
162 async fn if_latest_version_given_it_gets_used_regardless() -> PluginLookupResult<()> {
163 let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("99.0.1").unwrap()));
164 let resolved = lookup
165 .resolve_manifest(&tests_store(), false, "98.0.0")
166 .await?;
167 assert_eq!("99.0.1", resolved.version);
168 Ok(())
169 }
170
171 #[tokio::test]
172 async fn if_no_version_given_but_skip_compat_then_highest() -> PluginLookupResult<()> {
173 let lookup = PluginRef::new(TEST_NAME, None);
174 let resolved = lookup
175 .resolve_manifest(&tests_store(), true, "98.0.0")
176 .await?;
177 assert_eq!("99.0.1", resolved.version);
178 Ok(())
179 }
180
181 #[tokio::test]
182 async fn if_non_existent_version_given_then_error() -> PluginLookupResult<()> {
183 let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("177.7.7").unwrap()));
184 lookup
185 .resolve_manifest(&tests_store(), true, "99.0.0")
186 .await
187 .expect_err("Should have errored because plugin v177.7.7 does not exist");
188 Ok(())
189 }
190}