use serde_json::Value;
use super::Error;
use super::artifact::{PackageArtifact, artifact_content_hash, declared_range};
#[derive(Debug, Default)]
pub struct LintReport {
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
pub fn lint_artifact(artifact: &PackageArtifact) -> Result<LintReport, Error> {
if let Some(range) = declared_range(artifact)? {
range.check_this_binary(&format!(
"{}@{}",
artifact.package.name, artifact.package.version
))?;
}
let mut errors: Vec<String> = Vec::new();
if artifact.package.name.trim().is_empty() {
errors.push("package.name is empty".to_string());
}
if artifact.package.version.trim().is_empty() {
errors.push("package.version is empty".to_string());
}
match artifact_content_hash(artifact) {
Ok(actual) if actual != artifact.package.content_hash => errors.push(format!(
"package.content_hash does not match the entities — expected {actual}"
)),
Ok(actual) => {
if let Some(hex) =
crate::storage::content::content_version_hex(&artifact.package.version)
&& let Ok(expected) = crate::storage::content::content_version(&actual, None)
&& expected != artifact.package.version
{
errors.push(format!(
"package.version 'content-{hex}' names content the entities do not hash \
to — expected {expected}"
));
}
}
Err(e) => errors.push(e.to_string()),
}
for (kind, entries) in [("plugins", &artifact.plugins), ("models", &artifact.models)] {
for (index, entry) in entries.iter().enumerate() {
if let Some(signature) = entry.get("signature").and_then(Value::as_str)
&& let Err(e) = crate::crypto::ed25519::normalize_signature(signature)
{
errors.push(format!("{kind}[{index}].signature: {e}"));
}
}
}
let (set, boundary, mut findings) = artifact_as_set(artifact);
let registry = match set.function_registry() {
Ok(registry) => registry,
Err(reason) => {
errors.push(format!("plugins: {reason}"));
crate::engine::FunctionRegistry::builtin()
.with_entries(Vec::new())
.expect("the built-in registry extends by nothing")
}
};
findings.extend(crate::definitions::check(&set, &boundary, true, ®istry));
let warnings = findings
.iter()
.filter(|f| !f.is_error())
.map(ToString::to_string)
.collect();
errors.extend(findings.iter().filter(|f| f.is_error()).map(|f| {
format!("{}: {}", f.entity, f.message)
}));
Ok(LintReport { errors, warnings })
}
pub fn artifact_as_set(
artifact: &PackageArtifact,
) -> (
crate::definitions::DefinitionSet,
crate::definitions::Boundary,
Vec<crate::definitions::Diagnostic>,
) {
use crate::definitions::Entity;
let mut entries = Vec::new();
for (i, doc) in artifact.connectors.iter().enumerate() {
entries.push((Entity::Connector, format!("connectors[{i}]"), doc.clone()));
}
for (i, doc) in artifact.workflows.iter().enumerate() {
entries.push((Entity::Workflow, format!("workflows[{i}]"), doc.clone()));
}
for (i, doc) in artifact.channels.iter().enumerate() {
entries.push((Entity::Channel, format!("channels[{i}]"), doc.clone()));
}
let boundary = crate::definitions::Boundary {
channels: artifact.requires.channels.clone(),
connectors: artifact.requires.connectors.clone(),
models: artifact
.requires
.models
.iter()
.map(|m| m.id.clone())
.collect(),
};
let mut set = crate::definitions::DefinitionSet::from_entries(entries);
let mut findings = Vec::new();
for (i, entry) in artifact.plugins.iter().enumerate() {
match super::artifact::plugin_definition(i, entry) {
Ok(plugin) => set.plugins.push(plugin),
Err(e) => findings.push(crate::definitions::Diagnostic::error(
"parse.plugin",
format!("plugins[{i}]"),
e.to_string(),
)),
}
}
for (i, entry) in artifact.models.iter().enumerate() {
match super::artifact::model_definition(i, entry) {
Ok(model) => set.models.push(model),
Err(e) => findings.push(crate::definitions::Diagnostic::error(
"parse.model",
format!("models[{i}]"),
e.to_string(),
)),
}
}
(set, boundary, findings)
}
#[cfg(test)]
mod tests {
use super::super::artifact::test_support::*;
use super::super::artifact::{
ModelRequirement, activation_intents, literal_model_ids, members,
};
use super::*;
use serde_json::json;
#[test]
fn requires_carries_models_and_storage_and_bounds_the_set() {
let mut with = artifact(vec![model_entry()]);
with.requires.models.push(ModelRequirement {
id: "ada.other".to_string(),
version: 2,
digest: "sha256:def".to_string(),
});
with.requires.storage.push("models".to_string());
with.workflows[0]["tasks"] = json!([
{"id": "a", "name": "a", "function": {"name": "model_infer",
"input": {"model": "ada.c4-tiny", "input": {"var": ""}}}},
{"id": "b", "name": "b", "function": {"name": "model_infer",
"input": {"model": "ada.other", "input": {"var": ""}}}}
]);
let text = serde_json::to_string(&with).expect("serialises");
let back: PackageArtifact = serde_json::from_str(&text).expect("parses");
assert_eq!(back.requires.models, with.requires.models);
assert_eq!(back.requires.storage, ["models"]);
assert_eq!(
literal_model_ids(&back.workflows[0]),
["ada.c4-tiny", "ada.other"]
);
let (set, boundary, findings) = artifact_as_set(&back);
assert!(findings.is_empty(), "{findings:?}");
assert_eq!(set.models.len(), 1);
assert_eq!(set.models[0].origin, "models[0]");
assert!(boundary.allows_model("ada.other"));
assert!(
!boundary.allows_model("ada.c4-tiny"),
"carried, not required"
);
let registry = set.function_registry().expect("registry");
let findings = crate::definitions::check(&set, &boundary, true, ®istry);
assert!(
!findings.iter().any(|f| f.check == "closure.model"),
"{findings:#?}"
);
let old: PackageArtifact = serde_json::from_value(json!({
"package": {"name": "p", "version": "1", "content_hash": "x"},
"requires": {"channels": [], "connectors": []},
}))
.expect("parses");
assert!(old.requires.models.is_empty() && old.requires.storage.is_empty());
assert!(old.models.is_empty());
let intents = activation_intents(&with);
assert_eq!(intents[0].0, "models");
assert_eq!(intents[0].1, "ada.c4-tiny");
assert_eq!(
members(&with).map(|(k, _)| k),
["plugins", "connectors", "models", "workflows", "channels"]
);
}
}