winbrew_models\catalog/
conversion.rs

1//! Conversion logic between raw catalog records and validated catalog types.
2//!
3//! This module owns the bridge from upstream schema-shaped payloads to the typed
4//! catalog model used by the rest of the workspace. Keep parsing and
5//! normalization rules here so the raw and typed layers stay clearly separated.
6
7use core::convert::TryFrom;
8
9use crate::catalog::package::{CatalogInstaller, CatalogPackage};
10use crate::catalog::raw::{RawCatalogInstaller, RawCatalogPackage};
11use crate::package::Package;
12use crate::package::PackageId;
13use crate::shared::ModelError;
14
15impl From<&Package> for CatalogPackage {
16    fn from(package: &Package) -> Self {
17        let package_id = PackageId::parse(package.id.as_ref()).expect("package id should parse");
18
19        Self {
20            id: package.id.clone().into(),
21            name: package.name.clone(),
22            version: package.version.clone(),
23            source: package_id.source(),
24            namespace: package_id.namespace().map(str::to_string),
25            source_id: package_id.source_id().to_string(),
26            created_at: None,
27            updated_at: None,
28            description: package.description.clone(),
29            homepage: package.homepage.clone(),
30            license: package.license.clone(),
31            publisher: package.publisher.clone(),
32            locale: None,
33            moniker: None,
34            platform: None,
35            commands: None,
36            protocols: None,
37            file_extensions: None,
38            capabilities: None,
39            tags: None,
40            bin: None,
41            env_add_path: None,
42        }
43    }
44}
45
46impl TryFrom<RawCatalogPackage> for CatalogPackage {
47    type Error = ModelError;
48
49    fn try_from(raw: RawCatalogPackage) -> Result<Self, Self::Error> {
50        let source = raw.source.parse()?;
51
52        let package = Self {
53            id: raw.id.into(),
54            name: raw.name,
55            version: raw.version.parse()?,
56            source,
57            namespace: raw.namespace,
58            source_id: raw.source_id,
59            created_at: None,
60            updated_at: None,
61            description: raw.description,
62            homepage: raw.homepage,
63            license: raw.license,
64            publisher: raw.publisher,
65            locale: raw.locale,
66            moniker: raw.moniker,
67            platform: raw.platform,
68            commands: raw.commands,
69            protocols: raw.protocols,
70            file_extensions: raw.file_extensions,
71            capabilities: raw.capabilities,
72            tags: raw.tags,
73            bin: raw.bin,
74            env_add_path: raw.env_add_path,
75        };
76
77        package.validate()?;
78        Ok(package)
79    }
80}
81
82impl TryFrom<RawCatalogInstaller> for CatalogInstaller {
83    type Error = ModelError;
84
85    fn try_from(raw: RawCatalogInstaller) -> Result<Self, Self::Error> {
86        let installer = Self {
87            package_id: raw.package_id.into(),
88            url: raw.url,
89            hash: raw.hash,
90            hash_algorithm: raw.hash_algorithm,
91            installer_type: raw.installer_type,
92            installer_switches: raw.installer_switches,
93            platform: raw.platform,
94            commands: raw.commands,
95            protocols: raw.protocols,
96            file_extensions: raw.file_extensions,
97            capabilities: raw.capabilities,
98            arch: raw.arch.parse()?,
99            kind: raw.kind.parse()?,
100            nested_kind: raw.nested_kind.map(|kind| kind.parse()).transpose()?,
101            scope: raw.scope,
102        };
103
104        installer.validate()?;
105        Ok(installer)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{CatalogInstaller, CatalogPackage};
112    use crate::catalog::installer_type::CatalogInstallerType;
113    use crate::catalog::raw::{RawCatalogInstaller, RawCatalogPackage};
114    use crate::install::{Architecture, InstallerType};
115    use crate::package::PackageSource;
116
117    #[test]
118    fn raw_catalog_package_converts_and_derives_source() {
119        let package = RawCatalogPackage {
120            id: "winget/Contoso.App".to_string(),
121            name: "Contoso App".to_string(),
122            version: "1.2.3".to_string(),
123            source: "winget".to_string(),
124            namespace: None,
125            source_id: "Contoso.App".to_string(),
126            description: Some("Example package".to_string()),
127            homepage: None,
128            license: None,
129            publisher: Some("Contoso Ltd.".to_string()),
130            locale: Some("en-US".to_string()),
131            moniker: Some("contoso".to_string()),
132            platform: Some("[\"Windows.Desktop\"]".to_string()),
133            commands: Some("[\"contoso\"]".to_string()),
134            protocols: Some("[\"contoso-protocol\"]".to_string()),
135            file_extensions: Some("[\".app\"]".to_string()),
136            capabilities: Some("[\"internetClient\"]".to_string()),
137            tags: Some("[\"utility\"]".to_string()),
138            bin: Some("[\"tool.exe\"]".to_string()),
139            env_add_path: Some("[\"bin\"]".to_string()),
140        };
141
142        let converted = CatalogPackage::try_from(package).expect("raw package should convert");
143
144        assert_eq!(converted.source, PackageSource::Winget);
145        assert_eq!(converted.namespace, None);
146        assert_eq!(converted.source_id, "Contoso.App");
147        assert_eq!(converted.version.to_string(), "1.2.3");
148        assert_eq!(converted.locale.as_deref(), Some("en-US"));
149        assert_eq!(converted.moniker.as_deref(), Some("contoso"));
150        assert_eq!(converted.platform.as_deref(), Some("[\"Windows.Desktop\"]"));
151        assert_eq!(converted.commands.as_deref(), Some("[\"contoso\"]"));
152        assert_eq!(
153            converted.protocols.as_deref(),
154            Some("[\"contoso-protocol\"]")
155        );
156        assert_eq!(converted.file_extensions.as_deref(), Some("[\".app\"]"));
157        assert_eq!(
158            converted.capabilities.as_deref(),
159            Some("[\"internetClient\"]")
160        );
161        assert_eq!(converted.tags.as_deref(), Some("[\"utility\"]"));
162        assert_eq!(converted.bin.as_deref(), Some("[\"tool.exe\"]"));
163        assert_eq!(converted.env_add_path.as_deref(), Some("[\"bin\"]"));
164    }
165
166    #[test]
167    fn raw_catalog_installer_converts() {
168        let installer = RawCatalogInstaller {
169            package_id: "winget/Contoso.App".to_string(),
170            url: "https://example.test/app.exe".to_string(),
171            hash: String::default(),
172            hash_algorithm: crate::shared::HashAlgorithm::Sha256,
173            installer_type: CatalogInstallerType::Zip,
174            installer_switches: Some("/S".to_string()),
175            platform: Some("[\"Windows.Desktop\"]".to_string()),
176            commands: Some("[\"contoso\"]".to_string()),
177            protocols: Some("[\"contoso-protocol\"]".to_string()),
178            file_extensions: Some("[\".exe\"]".to_string()),
179            capabilities: Some("[\"internetClient\"]".to_string()),
180            arch: String::default(),
181            kind: "portable".to_string(),
182            nested_kind: Some("msi".to_string()),
183            scope: Some("machine".to_string()),
184        };
185
186        let converted =
187            CatalogInstaller::try_from(installer).expect("raw installer should convert");
188
189        assert_eq!(converted.arch, Architecture::Any);
190        assert_eq!(converted.kind, InstallerType::Portable);
191        assert_eq!(converted.nested_kind, Some(InstallerType::Msi));
192        assert_eq!(converted.scope.as_deref(), Some("machine"));
193        assert_eq!(converted.platform.as_deref(), Some("[\"Windows.Desktop\"]"));
194        assert_eq!(converted.commands.as_deref(), Some("[\"contoso\"]"));
195        assert_eq!(
196            converted.protocols.as_deref(),
197            Some("[\"contoso-protocol\"]")
198        );
199        assert_eq!(converted.file_extensions.as_deref(), Some("[\".exe\"]"));
200        assert_eq!(
201            converted.capabilities.as_deref(),
202            Some("[\"internetClient\"]")
203        );
204        assert_eq!(
205            converted.hash_algorithm,
206            crate::shared::HashAlgorithm::Sha256
207        );
208        assert_eq!(converted.installer_type, CatalogInstallerType::Zip);
209        assert_eq!(converted.installer_switches.as_deref(), Some("/S"));
210    }
211}