components_rs/discovery/
package_json.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4
5use crate::error::{ComponentsJsError, Result};
6use crate::fs::Fs;
7
8const LSD_BUNDLES_PREFIX: &str = "https://linkedsoftwaredependencies.org/bundles/npm/";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PackageJson {
13 pub name: String,
14 #[serde(default)]
15 pub version: String,
16
17 #[serde(rename = "lsd:module", default)]
19 pub lsd_module: Option<LsdModule>,
20
21 #[serde(rename = "lsd:components", default)]
23 pub lsd_components: Option<String>,
24
25 #[serde(rename = "lsd:contexts", default)]
27 pub lsd_contexts: Option<HashMap<String, String>>,
28
29 #[serde(rename = "lsd:importPaths", default)]
31 pub lsd_import_paths: Option<HashMap<String, String>>,
32
33 #[serde(rename = "lsd:basePath", default)]
35 pub lsd_base_path: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(untagged)]
41pub enum LsdModule {
42 Bool(bool),
43 Iri(String),
44}
45
46impl LsdModule {
47 pub fn as_iri(&self) -> Option<&str> {
48 match self {
49 LsdModule::Iri(s) => Some(s.as_str()),
50 LsdModule::Bool(_) => None,
51 }
52 }
53
54 pub fn is_enabled(&self) -> bool {
55 match self {
56 LsdModule::Bool(b) => *b,
57 LsdModule::Iri(_) => true,
58 }
59 }
60}
61
62pub async fn read_package_jsons(
64 fs: &dyn Fs,
65 module_paths: &[PathBuf],
66) -> Result<HashMap<PathBuf, PackageJson>> {
67 let mut result = HashMap::new();
68 for module_path in module_paths {
69 let pkg_path = module_path.join("package.json");
70 if fs.is_file(&pkg_path).await {
71 let contents = fs.read_to_string(&pkg_path).await?;
72 let pkg: PackageJson =
73 serde_json::from_str(&contents).map_err(|e| ComponentsJsError::JsonParse {
74 path: pkg_path.display().to_string(),
75 source: e,
76 })?;
77 result.insert(module_path.clone(), pkg);
78 }
79 }
80 Ok(result)
81}
82
83pub async fn preprocess_package_json(fs: &dyn Fs, package_path: &Path, pkg: &mut PackageJson) {
87 let needs_expansion = matches!(pkg.lsd_module, Some(LsdModule::Bool(true)));
88 if !needs_expansion {
89 return;
90 }
91
92 let module_iri = format!("{LSD_BUNDLES_PREFIX}{}", pkg.name);
94 pkg.lsd_module = Some(LsdModule::Iri(module_iri.clone()));
95
96 let base_path = pkg.lsd_base_path.as_deref().unwrap_or("");
97
98 let components_file = package_path
100 .join(base_path)
101 .join("components/components.jsonld");
102 if fs.is_file(&components_file).await {
103 pkg.lsd_components = Some(format!("{base_path}components/components.jsonld"));
104 }
105
106 let major = parse_major_version(&pkg.version).unwrap_or(0);
108 let base_iri = format!("{module_iri}/^{major}.0.0/");
109
110 let context_file = package_path
112 .join(base_path)
113 .join("components/context.jsonld");
114 if fs.is_file(&context_file).await {
115 let mut contexts = HashMap::new();
116 contexts.insert(
117 format!("{base_iri}components/context.jsonld"),
118 format!("{base_path}components/context.jsonld"),
119 );
120 pkg.lsd_contexts = Some(contexts);
121 }
122
123 let mut import_paths = HashMap::new();
125 let components_dir = package_path.join(base_path).join("components");
126 if fs.is_dir(&components_dir).await {
127 import_paths.insert(
128 format!("{base_iri}components/"),
129 format!("{base_path}components/"),
130 );
131 }
132 let config_dir = package_path.join(base_path).join("config");
133 if fs.is_dir(&config_dir).await {
134 import_paths.insert(format!("{base_iri}config/"), format!("{base_path}config/"));
135 }
136 pkg.lsd_import_paths = Some(import_paths);
137}
138
139pub async fn preprocess_all(fs: &dyn Fs, package_jsons: &mut HashMap<PathBuf, PackageJson>) {
141 let keys: Vec<PathBuf> = package_jsons.keys().cloned().collect();
142 for path in keys {
143 if let Some(pkg) = package_jsons.get_mut(&path) {
144 preprocess_package_json(fs, &path, pkg).await;
145 }
146 }
147}
148
149fn parse_major_version(version: &str) -> Option<u64> {
150 semver::Version::parse(version).ok().map(|v| v.major)
151}
152
153pub fn get_module_iri(pkg: &PackageJson) -> Option<String> {
155 match &pkg.lsd_module {
156 Some(LsdModule::Iri(iri)) => Some(iri.clone()),
157 _ => None,
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_lsd_module_deserialization_bool() {
167 let json = r#"{"name":"test","version":"1.0.0","lsd:module":true}"#;
168 let pkg: PackageJson = serde_json::from_str(json).unwrap();
169 assert!(matches!(pkg.lsd_module, Some(LsdModule::Bool(true))));
170 }
171
172 #[test]
173 fn test_lsd_module_deserialization_string() {
174 let json = r#"{"name":"test","version":"1.0.0","lsd:module":"https://example.org/test"}"#;
175 let pkg: PackageJson = serde_json::from_str(json).unwrap();
176 assert!(matches!(pkg.lsd_module, Some(LsdModule::Iri(_))));
177 assert_eq!(
178 pkg.lsd_module.unwrap().as_iri(),
179 Some("https://example.org/test")
180 );
181 }
182
183 #[test]
184 fn test_no_lsd_module() {
185 let json = r#"{"name":"test","version":"1.0.0"}"#;
186 let pkg: PackageJson = serde_json::from_str(json).unwrap();
187 assert!(pkg.lsd_module.is_none());
188 }
189}