1use crate::config::DnxConfig;
2use crate::errors::{DnxError, Result};
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::path::Path;
6
7#[derive(Debug, Clone, Deserialize)]
14pub struct CatalogConfig {
15 #[serde(default)]
17 pub default: HashMap<String, String>,
18 #[serde(default)]
20 pub named: HashMap<String, HashMap<String, String>>,
21}
22
23impl CatalogConfig {
24 pub fn load(root: &Path) -> Result<Option<Self>> {
30 let config = DnxConfig::load(root);
32 if !config.catalog_default.is_empty() || !config.catalog_named.is_empty() {
33 return Ok(Some(CatalogConfig {
34 default: config.catalog_default,
35 named: config.catalog_named,
36 }));
37 }
38
39 Self::load_from_catalog_toml(root)
41 }
42
43 pub fn load_from_catalog_toml(root: &Path) -> Result<Option<Self>> {
46 let path = root.join("dnx.catalog.toml");
47 if !path.exists() {
48 return Ok(None);
49 }
50
51 let content = std::fs::read_to_string(&path)
52 .map_err(|e| DnxError::Catalog(format!("Failed to read {}: {}", path.display(), e)))?;
53
54 let config: CatalogConfig = toml::from_str(&content)
55 .map_err(|e| DnxError::Catalog(format!("Failed to parse {}: {}", path.display(), e)))?;
56
57 Ok(Some(config))
58 }
59
60 pub fn from_maps(
62 default: HashMap<String, String>,
63 named: HashMap<String, HashMap<String, String>>,
64 ) -> Self {
65 Self { default, named }
66 }
67
68 pub fn resolve(&self, pkg_name: &str, spec: &str) -> Result<String> {
73 let suffix = spec
74 .strip_prefix("catalog:")
75 .ok_or_else(|| DnxError::Catalog(format!("Not a catalog specifier: {}", spec)))?;
76
77 if suffix.is_empty() {
78 self.default.get(pkg_name).cloned().ok_or_else(|| {
80 DnxError::Catalog(format!(
81 "Package '{}' not found in default catalog",
82 pkg_name
83 ))
84 })
85 } else {
86 let group = self.named.get(suffix).ok_or_else(|| {
88 DnxError::Catalog(format!("Catalog group '{}' not found", suffix))
89 })?;
90 group.get(pkg_name).cloned().ok_or_else(|| {
91 DnxError::Catalog(format!(
92 "Package '{}' not found in catalog group '{}'",
93 pkg_name, suffix
94 ))
95 })
96 }
97 }
98}
99
100pub fn resolve_catalog_deps(
103 deps: &HashMap<String, String>,
104 catalog: &CatalogConfig,
105) -> Result<HashMap<String, String>> {
106 let mut resolved = HashMap::with_capacity(deps.len());
107 for (name, spec) in deps {
108 if spec.starts_with("catalog:") {
109 resolved.insert(name.clone(), catalog.resolve(name, spec)?);
110 } else {
111 resolved.insert(name.clone(), spec.clone());
112 }
113 }
114 Ok(resolved)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn make_catalog() -> CatalogConfig {
122 let mut default = HashMap::new();
123 default.insert("react".to_string(), "^18.3.1".to_string());
124 default.insert("typescript".to_string(), "^5.4.0".to_string());
125 default.insert("lodash".to_string(), "^4.17.21".to_string());
126
127 let mut testing = HashMap::new();
128 testing.insert("jest".to_string(), "^29.0.0".to_string());
129 testing.insert("vitest".to_string(), "^1.0.0".to_string());
130
131 let mut named = HashMap::new();
132 named.insert("testing".to_string(), testing);
133
134 CatalogConfig { default, named }
135 }
136
137 #[test]
138 fn test_resolve_default_catalog() {
139 let catalog = make_catalog();
140 assert_eq!(catalog.resolve("react", "catalog:").unwrap(), "^18.3.1");
141 }
142
143 #[test]
144 fn test_resolve_named_catalog() {
145 let catalog = make_catalog();
146 assert_eq!(
147 catalog.resolve("jest", "catalog:testing").unwrap(),
148 "^29.0.0"
149 );
150 }
151
152 #[test]
153 fn test_resolve_missing_package() {
154 let catalog = make_catalog();
155 assert!(catalog.resolve("nonexistent", "catalog:").is_err());
156 }
157
158 #[test]
159 fn test_resolve_missing_group() {
160 let catalog = make_catalog();
161 assert!(catalog.resolve("jest", "catalog:nope").is_err());
162 }
163
164 #[test]
165 fn test_resolve_catalog_deps() {
166 let catalog = make_catalog();
167 let mut deps = HashMap::new();
168 deps.insert("react".to_string(), "catalog:".to_string());
169 deps.insert("jest".to_string(), "catalog:testing".to_string());
170 deps.insert("axios".to_string(), "^1.6.0".to_string());
171
172 let resolved = resolve_catalog_deps(&deps, &catalog).unwrap();
173 assert_eq!(resolved["react"], "^18.3.1");
174 assert_eq!(resolved["jest"], "^29.0.0");
175 assert_eq!(resolved["axios"], "^1.6.0");
176 }
177
178 #[test]
179 fn test_from_maps() {
180 let mut default = HashMap::new();
181 default.insert("react".to_string(), "^18.0.0".to_string());
182 let catalog = CatalogConfig::from_maps(default, HashMap::new());
183 assert_eq!(catalog.resolve("react", "catalog:").unwrap(), "^18.0.0");
184 }
185
186 #[test]
187 fn test_load_missing_file() {
188 let result = CatalogConfig::load_from_catalog_toml(Path::new("/nonexistent"));
189 assert!(result.unwrap().is_none());
190 }
191}