conduit_cli/core/engine/resolver/
addon.rs1use crate::core::engine::resolver::Resolver;
2use crate::core::domain::addon::AddonType;
3use crate::core::domain::loader::Loader;
4use crate::core::domain::source::{AddonSource, Hash, SourceType};
5use crate::errors::{ConduitError, ConduitResult};
6use std::collections::{HashMap, HashSet};
7
8pub struct ResolvedAddon {
9 pub id: String,
10 pub slug: String,
11 pub file_name: String,
12 pub r#type: AddonType,
13 pub loaders: Vec<Loader>,
14 pub download_url: String,
15 pub source: AddonSource,
16 pub dependencies: Vec<String>,
17}
18
19impl Resolver {
20 pub async fn resolve_recursively(
21 &self,
22 identifier: &str,
23 mc_version: &str,
24 loader: &Loader,
25 expected_type: AddonType,
26 ) -> ConduitResult<Vec<ResolvedAddon>> {
27 let mut resolved_map: HashMap<String, ResolvedAddon> = HashMap::new();
28 let mut seen_ids: HashSet<String> = HashSet::new();
29 let mut to_resolve = vec![identifier.to_string()];
30
31 while let Some(current_id) = to_resolve.pop() {
32 let resolved = self
33 .resolve_modrinth_addon(¤t_id, mc_version, loader, expected_type.clone())
34 .await?;
35
36 if seen_ids.contains(&resolved.id) {
37 continue;
38 }
39
40 let project_id = resolved.id.clone();
41
42 for dep_id in &resolved.dependencies {
43 if !seen_ids.contains(dep_id) {
44 to_resolve.push(dep_id.clone());
45 }
46 }
47
48 seen_ids.insert(project_id.clone());
49 resolved_map.insert(project_id, resolved);
50 }
51
52 Ok(resolved_map.into_values().collect())
53 }
54
55 pub async fn resolve_modrinth_addon(
56 &self,
57 id_or_slug: &str,
58 mc_version: &str,
59 loader: &Loader,
60 expected_type: AddonType,
61 ) -> ConduitResult<ResolvedAddon> {
62 let project = self.api.modrinth.get_project(id_or_slug).await?;
63
64 let mut loaders = vec![
65 match loader {
66 Loader::Vanilla => "minecraft",
67 Loader::Fabric => "fabric",
68 Loader::Forge { .. } => "forge",
69 Loader::Neoforge { .. } => "neoforge",
70 Loader::Paper => "paper",
71 Loader::Purpur => "purpur",
72 }
73 .to_string(),
74 ];
75
76 if expected_type == AddonType::Datapack {
77 loaders.push("datapack".to_string());
78 }
79
80 let versions = self
81 .api
82 .modrinth
83 .get_project_versions(&project.id, &loaders, &[mc_version.to_string()])
84 .await?;
85
86 let version = versions
87 .into_iter()
88 .find(|v| {
89 let is_datapack = v.loaders.contains(&"datapack".to_string());
90
91 match expected_type {
92 AddonType::Datapack => is_datapack,
93 AddonType::Plugin => {
94 v.loaders.contains(&"paper".to_string())
95 || v.loaders.contains(&"spigot".to_string())
96 }
97 AddonType::Mod => !is_datapack,
98 }
99 })
100 .ok_or_else(|| {
101 ConduitError::NotFound(format!(
102 "No compatible {expected_type:?} found for {id_or_slug} on {mc_version}"
103 ))
104 })?;
105
106 let file = version
107 .files
108 .iter()
109 .find(|f| f.primary)
110 .or_else(|| version.files.first())
111 .ok_or_else(|| ConduitError::NotFound(format!("No files found for {id_or_slug}")))?;
112
113 Ok(ResolvedAddon {
114 id: project.id.clone(),
115 slug: project.slug.clone(),
116 file_name: file.filename.clone(),
117 r#type: expected_type,
118 loaders: vec![loader.clone()],
119 download_url: file.url.clone(),
120 source: AddonSource {
121 r#type: SourceType::Modrinth {
122 id: project.id,
123 slug: project.slug,
124 },
125 hash: Hash {
126 sha1: Some(file.hashes.sha1.clone()),
127 sha256: None,
128 sha512: Some(file.hashes.sha512.clone()),
129 },
130 },
131 dependencies: version
132 .dependencies
133 .into_iter()
134 .filter(|dep| dep.dependency_type == "required")
135 .filter_map(|dep| dep.project_id)
136 .collect(),
137 })
138 }
139}