components_rs/discovery/
package_json.rs1use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20use url::Url;
21
22use crate::error::Result;
23use crate::fs::Fs;
24
25const LSD_BUNDLES_PREFIX: &str = "https://linkedsoftwaredependencies.org/bundles/npm/";
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PackageJson {
30 #[serde(default)]
31 pub name: String,
32 #[serde(default)]
33 pub version: String,
34
35 #[serde(rename = "lsd:module", default)]
37 pub lsd_module: Option<LsdModule>,
38
39 #[serde(rename = "lsd:components", default)]
41 pub lsd_components: Option<String>,
42
43 #[serde(rename = "lsd:contexts", default)]
45 pub lsd_contexts: Option<HashMap<String, String>>,
46
47 #[serde(rename = "lsd:importPaths", default)]
49 pub lsd_import_paths: Option<HashMap<String, String>>,
50
51 #[serde(rename = "lsd:basePath", default)]
53 pub lsd_base_path: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(untagged)]
59pub enum LsdModule {
60 Bool(bool),
61 Iri(String),
62}
63
64impl LsdModule {
65 pub fn as_iri(&self) -> Option<&str> {
66 match self {
67 LsdModule::Iri(s) => Some(s.as_str()),
68 LsdModule::Bool(_) => None,
69 }
70 }
71
72 pub fn is_enabled(&self) -> bool {
73 match self {
74 LsdModule::Bool(b) => *b,
75 LsdModule::Iri(_) => true,
76 }
77 }
78}
79
80fn join_dir(base: &Url, segment: &str) -> Option<Url> {
83 if segment.is_empty() {
84 return Some(base.clone());
85 }
86 let seg = if segment.ends_with('/') {
87 segment.to_string()
88 } else {
89 format!("{}/", segment)
90 };
91 base.join(&seg).ok()
92}
93
94pub async fn read_package_jsons(
96 fs: &dyn Fs,
97 module_paths: &[Url],
98) -> Result<HashMap<Url, PackageJson>> {
99 tracing::info!("[package_json] read_package_jsons called with {} paths", module_paths.len());
100 let mut result = HashMap::new();
101 for module_path in module_paths {
102 let pkg_url = match module_path.join("package.json") {
103 Ok(u) => u,
104 Err(e) => {
105 tracing::warn!("[package_json] failed to join package.json onto {}: {}", module_path.as_str(), e);
106 continue;
107 }
108 };
109 let contents = match fs.read_to_string(&pkg_url).await {
110 Ok(c) => c,
111 Err(e) => {
112 tracing::debug!("[package_json] read failed for {}: {}", pkg_url.as_str(), e);
113 continue;
114 }
115 };
116 let pkg: PackageJson = match serde_json::from_str(&contents) {
117 Ok(p) => p,
118 Err(e) => {
119 tracing::warn!("[package_json] skipping {}: {}", pkg_url.as_str(), e);
120 continue;
121 }
122 };
123 result.insert(module_path.clone(), pkg);
124 }
125 tracing::info!("[package_json] read_package_jsons parsed {} package.json files", result.len());
126 Ok(result)
127}
128
129pub async fn preprocess_package_json(fs: &dyn Fs, package_path: &Url, pkg: &mut PackageJson) {
133 let needs_expansion = matches!(pkg.lsd_module, Some(LsdModule::Bool(true)));
134 if !needs_expansion {
135 return;
136 }
137
138 let module_iri = format!("{LSD_BUNDLES_PREFIX}{}", pkg.name);
140 pkg.lsd_module = Some(LsdModule::Iri(module_iri.clone()));
141
142 let base_path = pkg.lsd_base_path.as_deref().unwrap_or("");
143 let base_dir = join_dir(package_path, base_path).unwrap_or_else(|| package_path.clone());
144
145 if let Ok(components_file) = base_dir.join("components/components.jsonld") {
147 if fs.is_file(&components_file).await {
148 pkg.lsd_components = Some(format!("{base_path}components/components.jsonld"));
149 }
150 }
151
152 let major = parse_major_version(&pkg.version).unwrap_or(0);
154 let base_iri = format!("{module_iri}/^{major}.0.0/");
155
156 if let Ok(context_file) = base_dir.join("components/context.jsonld") {
158 if fs.is_file(&context_file).await {
159 let mut contexts = HashMap::new();
160 contexts.insert(
161 format!("{base_iri}components/context.jsonld"),
162 format!("{base_path}components/context.jsonld"),
163 );
164 pkg.lsd_contexts = Some(contexts);
165 }
166 }
167
168 let mut import_paths = HashMap::new();
170 if let Ok(components_dir) = base_dir.join("components/") {
171 if fs.is_dir(&components_dir).await {
172 import_paths.insert(
173 format!("{base_iri}components/"),
174 format!("{base_path}components/"),
175 );
176 }
177 }
178 if let Ok(config_dir) = base_dir.join("config/") {
179 if fs.is_dir(&config_dir).await {
180 import_paths.insert(format!("{base_iri}config/"), format!("{base_path}config/"));
181 }
182 }
183 pkg.lsd_import_paths = Some(import_paths);
184}
185
186pub async fn preprocess_all(fs: &dyn Fs, package_jsons: &mut HashMap<Url, PackageJson>) {
188 let keys: Vec<Url> = package_jsons.keys().cloned().collect();
189 for path in keys {
190 if let Some(pkg) = package_jsons.get_mut(&path) {
191 preprocess_package_json(fs, &path, pkg).await;
192 }
193 }
194}
195
196fn parse_major_version(version: &str) -> Option<u64> {
197 semver::Version::parse(version).ok().map(|v| v.major)
198}
199
200pub fn get_module_iri(pkg: &PackageJson) -> Option<String> {
202 match &pkg.lsd_module {
203 Some(LsdModule::Iri(iri)) => Some(iri.clone()),
204 _ => None,
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_lsd_module_deserialization_bool() {
214 let json = r#"{"name":"test","version":"1.0.0","lsd:module":true}"#;
215 let pkg: PackageJson = serde_json::from_str(json).unwrap();
216 assert!(matches!(pkg.lsd_module, Some(LsdModule::Bool(true))));
217 }
218
219 #[test]
220 fn test_lsd_module_deserialization_string() {
221 let json = r#"{"name":"test","version":"1.0.0","lsd:module":"https://example.org/test"}"#;
222 let pkg: PackageJson = serde_json::from_str(json).unwrap();
223 assert!(matches!(pkg.lsd_module, Some(LsdModule::Iri(_))));
224 assert_eq!(
225 pkg.lsd_module.unwrap().as_iri(),
226 Some("https://example.org/test")
227 );
228 }
229
230 #[test]
231 fn test_no_lsd_module() {
232 let json = r#"{"name":"test","version":"1.0.0"}"#;
233 let pkg: PackageJson = serde_json::from_str(json).unwrap();
234 assert!(pkg.lsd_module.is_none());
235 }
236}