use serde::Serialize;
use crate::model::{
AuditResult, CheckResult, CheckStatus, CommandResult, UpdateResult, UpdateStatus, Vulnerability,
};
#[derive(Debug, Serialize)]
pub struct JsonResult {
pub ecosystem: String,
pub property: String,
pub current: String,
pub latest: Option<String>,
pub status: String,
pub kind: String,
pub managed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact: Option<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub source: String,
}
impl From<&CheckResult> for JsonResult {
fn from(r: &CheckResult) -> Self {
let (status, error) = match &r.status {
CheckStatus::UpToDate { .. } => ("up-to-date", None),
CheckStatus::Outdated { .. } => ("outdated", None),
CheckStatus::Skipped => ("skipped", None),
CheckStatus::Error { message } => ("error", Some(message.clone())),
};
Self {
ecosystem: r.ecosystem().to_string().to_lowercase(),
property: r.property().unwrap_or(r.artifact()).to_string(),
current: r.current_version.clone(),
latest: r.latest_version().map(ToString::to_string),
status: status.to_string(),
kind: r.kind().to_string().to_lowercase(),
managed: r.has_property(),
error,
artifact: Some(r.artifact().to_string()),
source: r.source().to_string(),
}
}
}
#[derive(Debug, Serialize)]
pub struct UpdateJsonResult {
pub ecosystem: String,
pub property: String,
pub old_version: String,
pub new_version: String,
pub kind: String,
pub managed: bool,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artifact: Option<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub source: String,
}
impl From<&UpdateResult> for UpdateJsonResult {
fn from(r: &UpdateResult) -> Self {
let (status, error) = match &r.status {
UpdateStatus::Updated => ("updated", None),
UpdateStatus::Error { message } => ("error", Some(message.clone())),
};
Self {
ecosystem: r.ecosystem().to_string().to_lowercase(),
property: r.property().unwrap_or(r.artifact()).to_string(),
old_version: r.old_version.clone(),
new_version: r.new_version.clone(),
kind: r.kind().to_string().to_lowercase(),
managed: r.has_property(),
status: status.to_string(),
error,
artifact: Some(r.artifact().to_string()),
source: r.source().to_string(),
}
}
}
#[derive(Debug, Serialize)]
pub struct AuditJsonResult {
pub ecosystem: String,
pub artifact: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub property: Option<String>,
pub version: String,
pub kind: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub source: String,
pub vulnerable: bool,
pub vulnerabilities: Vec<VulnerabilityJson>,
}
#[derive(Debug, Serialize)]
pub struct VulnerabilityJson {
pub id: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub summary: String,
pub severity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl From<&AuditResult> for AuditJsonResult {
fn from(r: &AuditResult) -> Self {
Self {
ecosystem: r.ecosystem().to_string().to_lowercase(),
artifact: r.artifact().to_string(),
property: r.property().map(ToString::to_string),
version: r.version.clone(),
kind: r.kind().to_string().to_lowercase(),
source: r.source().to_string(),
vulnerable: r.is_vulnerable(),
vulnerabilities: r
.vulnerabilities
.iter()
.map(VulnerabilityJson::from)
.collect(),
}
}
}
impl From<&Vulnerability> for VulnerabilityJson {
fn from(v: &Vulnerability) -> Self {
Self {
id: v.id.clone(),
aliases: v.aliases.clone(),
summary: v.summary.clone(),
severity: v.severity.to_string().to_lowercase(),
url: v.url.clone(),
}
}
}
impl UpdateJsonResult {
pub fn would_update(r: &CheckResult) -> Self {
Self {
ecosystem: r.ecosystem().to_string().to_lowercase(),
property: r.property().unwrap_or(r.artifact()).to_string(),
old_version: r.current_version.clone(),
new_version: r.latest_version().unwrap_or("").to_string(),
kind: r.kind().to_string().to_lowercase(),
managed: r.has_property(),
status: "would_update".to_string(),
error: None,
artifact: Some(r.artifact().to_string()),
source: r.source().to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Dependency, DependencyKind, Ecosystem};
#[test]
fn json_result_round_trip() {
let r = CheckResult::checked(
Dependency::new(
Ecosystem::Maven,
DependencyKind::Dependency,
"org.junit.jupiter:junit-jupiter".to_string(),
Some("version.junit".to_string()),
"pom.xml".to_string(),
),
"5.10.0".to_string(),
"5.12.0".to_string(),
true,
);
let json_result = JsonResult::from(&r);
let serialized = serde_json::to_string(&json_result).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert_eq!(parsed["ecosystem"], "maven");
assert_eq!(parsed["property"], "version.junit");
assert_eq!(parsed["current"], "5.10.0");
assert_eq!(parsed["latest"], "5.12.0");
assert_eq!(parsed["status"], "outdated");
assert_eq!(parsed["kind"], "dependency");
assert_eq!(parsed["artifact"], "org.junit.jupiter:junit-jupiter");
assert!(parsed.get("error").is_none());
}
#[test]
fn json_result_error_includes_error_field() {
let r = CheckResult::error(
Dependency::new(
Ecosystem::Npm,
DependencyKind::NpmDep,
"react".to_string(),
None,
String::new(),
),
"18.0.0".to_string(),
"not found".to_string(),
);
let json_result = JsonResult::from(&r);
let serialized = serde_json::to_string(&json_result).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert_eq!(parsed["status"], "error");
assert_eq!(parsed["error"], "not found");
}
#[test]
fn update_json_result_updated() {
let check = CheckResult::checked(
Dependency::new(
Ecosystem::Maven,
DependencyKind::Dependency,
"org.junit.jupiter:junit-jupiter".to_string(),
Some("version.junit".to_string()),
"pom.xml".to_string(),
),
"5.10.0".to_string(),
"5.12.0".to_string(),
true,
);
let update = UpdateResult::updated(&check, "5.12.0".to_string());
let json = UpdateJsonResult::from(&update);
let serialized = serde_json::to_string(&json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert_eq!(parsed["status"], "updated");
assert_eq!(parsed["old_version"], "5.10.0");
assert_eq!(parsed["new_version"], "5.12.0");
assert_eq!(parsed["ecosystem"], "maven");
assert_eq!(parsed["kind"], "dependency");
assert!(parsed.get("error").is_none());
}
#[test]
fn update_json_result_would_update() {
let check = CheckResult::checked(
Dependency::new(
Ecosystem::Maven,
DependencyKind::Plugin,
"org.apache.maven.plugins:maven-compiler-plugin".to_string(),
Some("version.compiler".to_string()),
"pom.xml".to_string(),
),
"3.11.0".to_string(),
"3.13.0".to_string(),
true,
);
let json = UpdateJsonResult::would_update(&check);
let serialized = serde_json::to_string(&json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert_eq!(parsed["status"], "would_update");
assert_eq!(parsed["kind"], "plugin");
}
#[test]
fn json_result_skips_none_fields() {
let r = CheckResult::checked(
Dependency::new(
Ecosystem::Maven,
DependencyKind::Dependency,
"g:a".to_string(),
None,
String::new(),
),
"1.0".to_string(),
"1.0".to_string(),
false,
);
let json_result = JsonResult::from(&r);
let serialized = serde_json::to_string(&json_result).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
assert!(parsed.get("error").is_none());
assert_eq!(parsed["artifact"], "g:a");
}
}