Skip to main content

wasm_pkg_client/
loader.rs

1use async_trait::async_trait;
2use futures_util::StreamExt;
3use wasm_pkg_common::{
4    Error,
5    package::{PackageRef, Version, VersionReq},
6};
7
8use crate::{
9    ContentStream,
10    release::{Release, VersionInfo},
11};
12
13#[derive(Debug, Default)]
14pub enum VersionSort {
15    #[default]
16    Ascending,
17    Descending,
18}
19
20#[async_trait]
21pub trait PackageLoader: Send {
22    async fn list_all_versions(&self, package: &PackageRef) -> Result<Vec<VersionInfo>, Error>;
23
24    async fn list_matching_versions(
25        &self,
26        package: &PackageRef,
27        predicate: VersionReq,
28        sort: VersionSort,
29    ) -> Result<Vec<VersionInfo>, Error> {
30        let versions = match self.list_all_versions(package).await {
31            Ok(v) => v,
32            Err(Error::PackageNotFound) => Vec::new(),
33            Err(e) => return Err(e),
34        };
35
36        let mut matching: Vec<VersionInfo> = versions
37            .into_iter()
38            .filter(|v| predicate.matches(&v.version))
39            .collect();
40
41        matching.sort();
42        if matches!(sort, VersionSort::Descending) {
43            matching.reverse();
44        }
45
46        Ok(matching)
47    }
48
49    async fn get_release(&self, package: &PackageRef, version: &Version) -> Result<Release, Error>;
50
51    async fn stream_content_unvalidated(
52        &self,
53        package: &PackageRef,
54        release: &Release,
55    ) -> Result<ContentStream, Error>;
56
57    async fn stream_content(
58        &self,
59        package: &PackageRef,
60        release: &Release,
61    ) -> Result<ContentStream, Error> {
62        let stream = self.stream_content_unvalidated(package, release).await?;
63        Ok(release.content_digest.validating_stream(stream).boxed())
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::{ContentStream, PackageLoader, Release, VersionInfo, VersionSort};
70    use async_trait::async_trait;
71    use rstest::rstest;
72    use wasm_pkg_common::{
73        Error,
74        package::{PackageRef, Version, VersionReq},
75    };
76
77    #[derive(Clone, Debug)]
78    struct VerifiablePackageLoader {
79        history: Vec<VersionInfo>,
80    }
81
82    impl VerifiablePackageLoader {
83        fn new(history: &[Version]) -> Self {
84            Self {
85                history: history
86                    .iter()
87                    .cloned()
88                    .map(|version| VersionInfo {
89                        version,
90                        yanked: false,
91                    })
92                    .collect(),
93            }
94        }
95    }
96
97    #[async_trait]
98    impl PackageLoader for VerifiablePackageLoader {
99        async fn list_all_versions(
100            &self,
101            _package: &PackageRef,
102        ) -> Result<Vec<VersionInfo>, Error> {
103            Ok(self.history.clone())
104        }
105
106        async fn get_release(
107            &self,
108            _package: &PackageRef,
109            _version: &Version,
110        ) -> Result<Release, Error> {
111            panic!("get_release is not needed in this unit test")
112        }
113
114        async fn stream_content_unvalidated(
115            &self,
116            _package: &PackageRef,
117            _release: &Release,
118        ) -> Result<ContentStream, Error> {
119            panic!("stream_content_unvalidated is not needed in this unit test")
120        }
121    }
122
123    fn v(input: &str) -> Version {
124        input.parse().expect("valid semver in test case")
125    }
126
127    fn versions(inputs: &[&str]) -> Vec<Version> {
128        inputs.iter().map(|s| v(s)).collect()
129    }
130
131    // These cases include the examples from the function docs and edge cases
132    // for lane filtering behavior.
133    #[rstest]
134    #[case::target_0_0_0(
135        "~0.0.*",
136        VersionSort::Ascending,
137        &["0.0.0", "0.0.1", "0.1.0", "1.0.0"],
138        &["0.0.0", "0.0.1"]
139    )]
140    #[case::target_0_0_3(
141        "~0.0.*",
142        VersionSort::Ascending,
143        &["0.0.0", "0.0.3", "0.0.7", "0.1.0"],
144        &["0.0.0", "0.0.3", "0.0.7"]
145    )]
146    #[case::target_1_0_0(
147        "~1.0.*",
148        VersionSort::Ascending,
149        &["1.0.0", "1.0.9", "1.1.0", "2.0.0"],
150        &["1.0.0", "1.0.9"]
151    )]
152    #[case::target_2_2_0(
153        "~2.2.*",
154        VersionSort::Ascending,
155        &["2.1.9", "2.2.0", "2.2.5", "2.3.0"],
156        &["2.2.0", "2.2.5"]
157    )]
158    #[case::empty_history("~1.2.*", VersionSort::Ascending, &[], &[])]
159    #[case::no_matching_major_minor_in_history(
160        "~3.4.*",
161        VersionSort::Ascending,
162        &["3.5.0", "3.6.1", "4.4.5"],
163        &[]
164    )]
165    #[case::all_patches_in_series(
166        "~1.2.*",
167        VersionSort::Ascending,
168        &["1.2.0", "1.2.1", "1.2.99", "1.3.0", "0.2.9"],
169        &["1.2.0", "1.2.1", "1.2.99"]
170    )]
171    // The exclusion of pre-release versions in range queries is a bit
172    // unintuitive but apparently intentional.
173    //
174    // https://github.com/dtolnay/semver/issues/98
175    #[case::pre_release_excluded_from_series(
176        "~1.2.*",
177        VersionSort::Ascending,
178        &["1.2.0", "1.2.1-beta.2", "1.2.4+build.7", "1.3.0-alpha.1"],
179        &["1.2.0", "1.2.4+build.7"]
180    )]
181    #[case::duplication_is_preserved(
182        "~1.2.*",
183        VersionSort::Ascending,
184        &["1.2.1", "1.2.1", "1.2.2", "1.3.0"],
185        &["1.2.1", "1.2.1", "1.2.2"]
186    )]
187    #[case::descending_sort_orders_matches_high_to_low(
188        "~1.2.*",
189        VersionSort::Descending,
190        &["1.2.0", "1.2.5", "1.2.1", "1.3.0"],
191        &["1.2.5", "1.2.1", "1.2.0"]
192    )]
193    #[case::descending_sort_with_empty_matches(
194        "~9.9.*",
195        VersionSort::Descending,
196        &["1.0.0", "2.0.0"],
197        &[]
198    )]
199    #[tokio::test]
200    async fn list_matching_versions_filters_by_version_req(
201        #[case] req: &str,
202        #[case] sort: VersionSort,
203        #[case] history: &[&str],
204        #[case] expected: &[&str],
205    ) {
206        let history = versions(history);
207        let expected = versions(expected);
208        let filter = VersionReq::parse(req).expect("valid series req");
209        let package: PackageRef = "example:package".parse().expect("valid package ref");
210
211        let loader = VerifiablePackageLoader::new(&history);
212
213        let got: Vec<Version> = loader
214            .list_matching_versions(&package, filter, sort)
215            .await
216            .expect("list_matching_versions should succeed")
217            .into_iter()
218            .map(|v| v.version)
219            .collect();
220
221        assert_eq!(got, expected.as_slice());
222    }
223}