use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use url::Url;
use crate::error::Result;
use crate::fs::Fs;
const LSD_BUNDLES_PREFIX: &str = "https://linkedsoftwaredependencies.org/bundles/npm/";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageJson {
#[serde(default)]
pub name: String,
#[serde(default)]
pub version: String,
#[serde(rename = "lsd:module", default)]
pub lsd_module: Option<LsdModule>,
#[serde(rename = "lsd:components", default)]
pub lsd_components: Option<String>,
#[serde(rename = "lsd:contexts", default)]
pub lsd_contexts: Option<HashMap<String, String>>,
#[serde(rename = "lsd:importPaths", default)]
pub lsd_import_paths: Option<HashMap<String, String>>,
#[serde(rename = "lsd:basePath", default)]
pub lsd_base_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum LsdModule {
Bool(bool),
Iri(String),
}
impl LsdModule {
pub fn as_iri(&self) -> Option<&str> {
match self {
LsdModule::Iri(s) => Some(s.as_str()),
LsdModule::Bool(_) => None,
}
}
pub fn is_enabled(&self) -> bool {
match self {
LsdModule::Bool(b) => *b,
LsdModule::Iri(_) => true,
}
}
}
fn join_dir(base: &Url, segment: &str) -> Option<Url> {
if segment.is_empty() {
return Some(base.clone());
}
let seg = if segment.ends_with('/') {
segment.to_string()
} else {
format!("{}/", segment)
};
base.join(&seg).ok()
}
pub async fn read_package_jsons(
fs: &dyn Fs,
module_paths: &[Url],
) -> Result<HashMap<Url, PackageJson>> {
tracing::info!("[package_json] read_package_jsons called with {} paths", module_paths.len());
let mut result = HashMap::new();
for module_path in module_paths {
let pkg_url = match module_path.join("package.json") {
Ok(u) => u,
Err(e) => {
tracing::warn!("[package_json] failed to join package.json onto {}: {}", module_path.as_str(), e);
continue;
}
};
let contents = match fs.read_to_string(&pkg_url).await {
Ok(c) => c,
Err(e) => {
tracing::debug!("[package_json] read failed for {}: {}", pkg_url.as_str(), e);
continue;
}
};
let pkg: PackageJson = match serde_json::from_str(&contents) {
Ok(p) => p,
Err(e) => {
tracing::warn!("[package_json] skipping {}: {}", pkg_url.as_str(), e);
continue;
}
};
result.insert(module_path.clone(), pkg);
}
tracing::info!("[package_json] read_package_jsons parsed {} package.json files", result.len());
Ok(result)
}
pub async fn preprocess_package_json(fs: &dyn Fs, package_path: &Url, pkg: &mut PackageJson) {
let needs_expansion = matches!(pkg.lsd_module, Some(LsdModule::Bool(true)));
if !needs_expansion {
return;
}
let module_iri = format!("{LSD_BUNDLES_PREFIX}{}", pkg.name);
pkg.lsd_module = Some(LsdModule::Iri(module_iri.clone()));
let base_path = pkg.lsd_base_path.as_deref().unwrap_or("");
let base_dir = join_dir(package_path, base_path).unwrap_or_else(|| package_path.clone());
if let Ok(components_file) = base_dir.join("components/components.jsonld") {
if fs.is_file(&components_file).await {
pkg.lsd_components = Some(format!("{base_path}components/components.jsonld"));
}
}
let major = parse_major_version(&pkg.version).unwrap_or(0);
let base_iri = format!("{module_iri}/^{major}.0.0/");
if let Ok(context_file) = base_dir.join("components/context.jsonld") {
if fs.is_file(&context_file).await {
let mut contexts = HashMap::new();
contexts.insert(
format!("{base_iri}components/context.jsonld"),
format!("{base_path}components/context.jsonld"),
);
pkg.lsd_contexts = Some(contexts);
}
}
let mut import_paths = HashMap::new();
if let Ok(components_dir) = base_dir.join("components/") {
if fs.is_dir(&components_dir).await {
import_paths.insert(
format!("{base_iri}components/"),
format!("{base_path}components/"),
);
}
}
if let Ok(config_dir) = base_dir.join("config/") {
if fs.is_dir(&config_dir).await {
import_paths.insert(format!("{base_iri}config/"), format!("{base_path}config/"));
}
}
pkg.lsd_import_paths = Some(import_paths);
}
pub async fn preprocess_all(fs: &dyn Fs, package_jsons: &mut HashMap<Url, PackageJson>) {
let keys: Vec<Url> = package_jsons.keys().cloned().collect();
for path in keys {
if let Some(pkg) = package_jsons.get_mut(&path) {
preprocess_package_json(fs, &path, pkg).await;
}
}
}
fn parse_major_version(version: &str) -> Option<u64> {
semver::Version::parse(version).ok().map(|v| v.major)
}
pub fn get_module_iri(pkg: &PackageJson) -> Option<String> {
match &pkg.lsd_module {
Some(LsdModule::Iri(iri)) => Some(iri.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lsd_module_deserialization_bool() {
let json = r#"{"name":"test","version":"1.0.0","lsd:module":true}"#;
let pkg: PackageJson = serde_json::from_str(json).unwrap();
assert!(matches!(pkg.lsd_module, Some(LsdModule::Bool(true))));
}
#[test]
fn test_lsd_module_deserialization_string() {
let json = r#"{"name":"test","version":"1.0.0","lsd:module":"https://example.org/test"}"#;
let pkg: PackageJson = serde_json::from_str(json).unwrap();
assert!(matches!(pkg.lsd_module, Some(LsdModule::Iri(_))));
assert_eq!(
pkg.lsd_module.unwrap().as_iri(),
Some("https://example.org/test")
);
}
#[test]
fn test_no_lsd_module() {
let json = r#"{"name":"test","version":"1.0.0"}"#;
let pkg: PackageJson = serde_json::from_str(json).unwrap();
assert!(pkg.lsd_module.is_none());
}
}