use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::{
is_known_publication_status, is_present_version, is_valid_version, ExtensionMap,
GovernanceMetadata, SecurityMetadata,
};
use crate::diagnostics::{categories, Diagnostic, ValidationReport};
use crate::error::{Error, Result};
use crate::DPCS_SPEC_VERSION;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct RegistryReference {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifact_id: Option<String>,
#[serde(default, flatten)]
pub extensions: ExtensionMap,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct Registry {
pub id: String,
pub version: String,
pub dpcs_version: String,
pub owner: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publication_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governance: Option<GovernanceMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security: Option<SecurityMetadata>,
#[serde(default)]
pub artifacts: Vec<RegisteredArtifact>,
#[serde(default, flatten)]
pub extensions: ExtensionMap,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct RegisteredArtifact {
pub id: String,
#[serde(rename = "type")]
pub artifact_type: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compatibility: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publication_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(default, flatten)]
pub extensions: ExtensionMap,
}
impl Registry {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let contents = std::fs::read_to_string(path).map_err(|err| Error::Io {
path: PathBuf::from(path),
source: err,
})?;
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
let result = if matches!(ext.as_str(), "json") {
Self::from_json_str(&contents)
} else {
Self::from_yaml_str(&contents)
};
match result {
Ok(registry) => Ok(registry),
Err(Error::InvalidDocument { mut report }) => {
let location = path.display().to_string();
for diagnostic in &mut report.diagnostics {
if diagnostic.source_location.is_none() {
diagnostic.source_location = Some(location.clone());
}
}
Err(Error::InvalidDocument { report })
}
Err(err) => Err(err),
}
}
pub fn from_yaml_str(input: &str) -> Result<Self> {
serde_yaml::from_str(input).map_err(|err| registry_parse_error(err.to_string()))
}
pub fn from_json_str(input: &str) -> Result<Self> {
serde_json::from_str(input).map_err(|err| registry_parse_error(err.to_string()))
}
}
pub fn validate_registry(registry: &Registry) -> ValidationReport {
let mut report = ValidationReport::new();
if registry.id.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-001",
categories::REGISTRY,
"registry id must not be empty",
)
.with_object_ref("id"),
);
}
if !is_present_version(®istry.version) {
report.push(
Diagnostic::error(
"DPCS-REG-002",
categories::REGISTRY,
"registry version must not be empty",
)
.with_object_ref("version"),
);
} else if !is_valid_version(®istry.version) {
report.push(
Diagnostic::error(
"DPCS-REG-003",
categories::REGISTRY,
format!(
"registry version `{}` is not SemVer-compatible",
registry.version
),
)
.with_object_ref("version"),
);
}
if registry.dpcs_version.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-004",
categories::REGISTRY,
"registry must declare dpcsVersion",
)
.with_object_ref("dpcsVersion"),
);
} else if !is_valid_version(®istry.dpcs_version) {
report.push(
Diagnostic::error(
"DPCS-REG-005",
categories::REGISTRY,
format!(
"registry dpcsVersion `{}` is not SemVer-compatible",
registry.dpcs_version
),
)
.with_object_ref("dpcsVersion"),
);
} else if registry.dpcs_version != DPCS_SPEC_VERSION && !registry.dpcs_version.starts_with("1.")
{
report.push(
Diagnostic::warning(
"DPCS-REG-006",
categories::REGISTRY,
format!(
"registry dpcsVersion `{}` may not be supported",
registry.dpcs_version
),
)
.with_object_ref("dpcsVersion"),
);
}
if registry.owner.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-007",
categories::REGISTRY,
"registry must declare an owner",
)
.with_object_ref("owner"),
);
}
if let Some(status) = ®istry.publication_status {
if status.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-015",
categories::REGISTRY,
"registry publicationStatus must not be empty when declared",
)
.with_object_ref("publicationStatus"),
);
} else if !is_known_publication_status(status) {
report.push(
Diagnostic::warning(
"DPCS-REG-008",
categories::REGISTRY,
format!("registry publicationStatus `{status}` is not recognized"),
)
.with_object_ref("publicationStatus"),
);
}
}
let mut seen: BTreeMap<(String, String), usize> = BTreeMap::new();
for (index, artifact) in registry.artifacts.iter().enumerate() {
let object_ref = format!("artifacts[{index}]");
if artifact.id.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-009",
categories::REGISTRY,
"registered artifact id must not be empty",
)
.with_object_ref(format!("{object_ref}.id")),
);
continue;
}
if artifact.artifact_type.trim().is_empty() {
report.push(
Diagnostic::error(
"DPCS-REG-010",
categories::REGISTRY,
"registered artifact type must not be empty",
)
.with_object_ref(format!("{object_ref}.type")),
);
}
if !is_present_version(&artifact.version) {
report.push(
Diagnostic::error(
"DPCS-REG-011",
categories::REGISTRY,
"registered artifact version must not be empty",
)
.with_object_ref(format!("{object_ref}.version")),
);
} else if !is_valid_version(&artifact.version) {
report.push(
Diagnostic::error(
"DPCS-REG-012",
categories::REGISTRY,
format!(
"registered artifact version `{}` is not SemVer-compatible",
artifact.version
),
)
.with_object_ref(format!("{object_ref}.version")),
);
}
if let Some(status) = &artifact.publication_status {
if !is_known_publication_status(status) {
report.push(
Diagnostic::warning(
"DPCS-REG-013",
categories::REGISTRY,
format!("artifact publicationStatus `{status}` is not recognized"),
)
.with_object_ref(format!("{object_ref}.publicationStatus")),
);
}
}
let key = (artifact.id.clone(), artifact.version.clone());
if let Some(first) = seen.get(&key).copied() {
report.push(
Diagnostic::error(
"DPCS-REG-014",
categories::REGISTRY,
format!(
"duplicate registered artifact `{}@{}`",
artifact.id, artifact.version
),
)
.with_object_ref(object_ref)
.with_remediation(format!("Artifact was first declared at artifacts[{first}]")),
);
} else {
seen.insert(key, index);
}
}
report.sort_deterministic();
report
}
fn registry_parse_error(message: String) -> Error {
let mut report = ValidationReport::new();
report.push(
Diagnostic::parse_error(
"DPCS-PARSE-002",
format!("invalid registry document: {message}"),
)
.with_remediation(
"Provide id, version, dpcsVersion, owner, and artifacts[] with unique id+version",
),
);
Error::InvalidDocument { report }
}