Skip to main content

loadsmith_core/
lib.rs

1//! Core types, traits, and errors for the loadsmith mod-manager library.
2//!
3//! This is an internal crate of the [`loadsmith`] workspace. Most consumers
4//! should depend on the `loadsmith` facade crate instead of using this
5//! crate directly.
6
7use std::{fmt::Display, str::FromStr};
8
9use camino::Utf8PathBuf;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13mod checksum;
14mod error;
15mod source;
16
17pub use checksum::{Checksum, ChecksumAlgorithm};
18pub use error::{Error, Result};
19pub use semver::{Version, VersionReq};
20pub use source::FileUrl;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
23pub struct PackageId(String);
24
25impl PackageId {
26    pub fn new(id: impl Into<String>) -> Self {
27        Self(id.into())
28    }
29
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33
34    pub fn into_string(self) -> String {
35        self.0
36    }
37}
38
39impl AsRef<str> for PackageId {
40    fn as_ref(&self) -> &str {
41        self.as_str()
42    }
43}
44
45impl From<String> for PackageId {
46    fn from(id: String) -> Self {
47        Self(id)
48    }
49}
50
51impl From<&str> for PackageId {
52    fn from(id: &str) -> Self {
53        id.to_string().into()
54    }
55}
56
57impl Display for PackageId {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}", self.0)
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
64#[serde(into = "String", try_from = "&str")]
65pub struct PackageRef {
66    id: PackageId,
67    version: Version,
68}
69
70impl PackageRef {
71    pub fn new(id: impl Into<PackageId>, version: impl Into<Version>) -> Self {
72        Self {
73            id: id.into(),
74            version: version.into(),
75        }
76    }
77
78    pub fn id(&self) -> &PackageId {
79        &self.id
80    }
81
82    pub fn version(&self) -> &Version {
83        &self.version
84    }
85
86    pub fn into_id(self) -> PackageId {
87        self.id
88    }
89
90    pub fn into_version(self) -> Version {
91        self.version
92    }
93
94    pub fn into_split(self) -> (PackageId, Version) {
95        (self.id, self.version)
96    }
97}
98
99impl Display for PackageRef {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}@{}", self.id, self.version)
102    }
103}
104
105impl FromStr for PackageRef {
106    type Err = Error;
107
108    fn from_str(s: &str) -> Result<Self> {
109        let mut split = s.split('@');
110
111        let id = split.next().ok_or(Error::InvalidPackageRefFormat)?;
112        let version = split.next().ok_or(Error::InvalidPackageRefFormat)?;
113
114        if split.next().is_some() {
115            return Err(Error::InvalidPackageRefFormat);
116        }
117
118        let id = PackageId::new(id);
119        let version = Version::from_str(version)?;
120
121        Ok(Self::new(id, version))
122    }
123}
124
125impl From<PackageRef> for String {
126    fn from(package_ref: PackageRef) -> Self {
127        package_ref.to_string()
128    }
129}
130
131impl TryFrom<&str> for PackageRef {
132    type Error = Error;
133
134    fn try_from(s: &str) -> Result<Self> {
135        s.parse()
136    }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct InstalledPackage {
141    #[serde(rename = "package")]
142    ref_: PackageRef,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    checksum: Option<Checksum>,
145    date: DateTime<Utc>,
146    files: Vec<InstalledFile>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct InstalledFile {
151    relative_path: Utf8PathBuf,
152    linked: bool,
153}
154
155impl InstalledPackage {
156    pub fn now(ref_: PackageRef, files: Vec<InstalledFile>, checksum: Option<Checksum>) -> Self {
157        Self {
158            ref_,
159            files,
160            date: Utc::now(),
161            checksum,
162        }
163    }
164
165    pub fn ref_(&self) -> &PackageRef {
166        &self.ref_
167    }
168
169    pub fn files(&self) -> &[InstalledFile] {
170        &self.files
171    }
172
173    pub fn files_mut(&mut self) -> &mut Vec<InstalledFile> {
174        &mut self.files
175    }
176
177    pub fn date(&self) -> &DateTime<Utc> {
178        &self.date
179    }
180
181    pub fn checksum(&self) -> Option<&Checksum> {
182        self.checksum.as_ref()
183    }
184}
185
186impl InstalledFile {
187    pub fn new(relative_path: impl Into<Utf8PathBuf>, linked: bool) -> Self {
188        Self {
189            relative_path: relative_path.into(),
190            linked,
191        }
192    }
193
194    pub fn relative_path(&self) -> &Utf8PathBuf {
195        &self.relative_path
196    }
197
198    pub fn linked(&self) -> bool {
199        self.linked
200    }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct Dependency {
205    pub id: PackageId,
206    pub version_req: VersionReq,
207    pub source: String,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub registry_metadata: Option<serde_json::Value>,
210}
211
212impl Dependency {
213    pub fn new(
214        id: impl Into<PackageId>,
215        version_req: impl Into<VersionReq>,
216        source: impl Into<String>,
217    ) -> Self {
218        Self {
219            id: id.into(),
220            version_req: version_req.into(),
221            source: source.into(),
222            registry_metadata: None,
223        }
224    }
225
226    pub fn with_registry_metadata(mut self, metadata: impl Into<serde_json::Value>) -> Self {
227        self.registry_metadata = Some(metadata.into());
228        self
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    use std::assert_matches;
237
238    #[test]
239    fn package_ref_from_str() {
240        let package_ref: PackageRef = "author-name@1.2.3"
241            .parse()
242            .expect("failed to parse package ref");
243        assert_eq!(package_ref.id.as_str(), "author-name");
244        assert_eq!(package_ref.version, Version::new(1, 2, 3));
245    }
246
247    #[test]
248    fn package_ref_from_str_two_ats() {
249        let res: Result<PackageRef> = "author-name@1.2.3@4.5.6".parse();
250        assert_matches!(res, Err(Error::InvalidPackageRefFormat));
251    }
252
253    #[test]
254    fn package_ref_from_str_invalid() {
255        let res: Result<PackageRef> = "author-name-1.2.3".parse();
256        assert_matches!(res, Err(Error::InvalidPackageRefFormat));
257    }
258
259    #[test]
260    fn package_id_from_str() {
261        let package_id: PackageId = PackageId::new("author-name");
262        assert_eq!(package_id.as_str(), "author-name");
263    }
264}