use std::path::PathBuf;
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::{
CommitSha, ConfigKey, DepKind, Diagnostic, Ecosystem, GroupName, PackageId, PublishPlan,
Severity, TagName, Version,
};
pub const SCHEMA_VERSION: u32 = 1;
pub trait Report: Serialize + DeserializeOwned + Send + Sync + 'static {
const COMMAND: &'static str;
fn schema_version(&self) -> u32;
fn diagnostics(&self) -> &[Diagnostic];
}
impl Report for PublishPlan {
const COMMAND: &'static str = "plan-publish";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PublishReport {
pub schema_version: u32,
pub attempts: Vec<PublishAttempt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for PublishReport {
const COMMAND: &'static str = "publish";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct PublishAttempt {
pub package: PackageId,
pub version: Version,
#[serde(flatten)]
pub result: PublishAttemptResult,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum PublishAttemptResult {
Published,
AlreadyPublished,
Failed { error: String },
}
impl PublishAttemptResult {
pub fn is_failure(&self) -> bool {
matches!(self, PublishAttemptResult::Failed { .. })
}
}
impl PublishReport {
pub fn has_failures(&self) -> bool {
self.attempts.iter().any(|a| a.result.is_failure())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PackageId, Version, VersionGrammar};
fn pkg() -> PackageId {
PackageId::parse("test-pkg").unwrap()
}
fn ver() -> Version {
Version::parse("1.0.0", VersionGrammar::SemVer).unwrap()
}
fn attempt(result: PublishAttemptResult) -> PublishAttempt {
PublishAttempt {
package: pkg(),
version: ver(),
result,
}
}
fn report(attempts: Vec<PublishAttempt>) -> PublishReport {
PublishReport {
schema_version: SCHEMA_VERSION,
attempts,
diagnostics: vec![],
}
}
#[test]
fn has_failures_returns_true_when_any_attempt_failed() {
let r = report(vec![
attempt(PublishAttemptResult::Published),
attempt(PublishAttemptResult::Failed {
error: "registry unavailable".to_string(),
}),
]);
assert!(r.has_failures());
}
#[test]
fn has_failures_returns_true_when_all_attempts_failed() {
let r = report(vec![
attempt(PublishAttemptResult::Failed {
error: "auth error".to_string(),
}),
attempt(PublishAttemptResult::Failed {
error: "network error".to_string(),
}),
]);
assert!(r.has_failures());
}
#[test]
fn has_failures_returns_false_when_all_attempts_succeeded() {
let r = report(vec![
attempt(PublishAttemptResult::Published),
attempt(PublishAttemptResult::AlreadyPublished),
]);
assert!(!r.has_failures());
}
#[test]
fn has_failures_returns_false_for_empty_report() {
let r = report(vec![]);
assert!(!r.has_failures());
}
#[test]
fn is_failure_is_true_only_for_failed_variant() {
assert!(PublishAttemptResult::Failed {
error: "oops".to_string()
}
.is_failure());
assert!(!PublishAttemptResult::Published.is_failure());
assert!(!PublishAttemptResult::AlreadyPublished.is_failure());
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VersionReport {
pub schema_version: u32,
pub bumps: Vec<BumpRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lockfile_refresh_results: Option<Vec<LockfileRefreshResult>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for VersionReport {
const COMMAND: &'static str = "version";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BumpRecord {
pub package: PackageId,
pub from: Version,
pub to: Version,
pub severity: Severity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governed_by: Option<ConfigKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<BumpReason>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "camelCase")]
#[non_exhaustive]
pub enum BumpReason {
Changeset {
changesets: Vec<String>,
},
Inference {
commits: usize,
remapped: bool,
},
FixedGroupUnion {
group: GroupName,
},
LinkedGroupUnion {
group: GroupName,
},
Cascade {
via: PackageId,
dep_kind: DepKind,
spec: String,
dependency_to: Version,
},
PeerEscalation {
via: PackageId,
spec: String,
},
PreRelease {
tag: String,
},
NewGroupMember {
group: GroupName,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct LockfileRefreshResult {
pub filename: PathBuf,
pub refresh_command: String,
pub success: bool,
pub exit_code: Option<i32>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct StatusReport {
pub schema_version: u32,
pub packages: Vec<StatusPackageRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for StatusReport {
const COMMAND: &'static str = "status";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct StatusPackageRecord {
pub package: PackageId,
pub current_version: Version,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_tag: Option<TagName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pending_severity: Option<Severity>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub changed_since_last_tag: bool,
pub pending_changesets: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SnapshotReport {
pub schema_version: u32,
pub snapshot_tag: String,
pub bumps: Vec<BumpRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for SnapshotReport {
const COMMAND: &'static str = "snapshot";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ComposePrBodyReport {
pub schema_version: u32,
pub pr_body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for ComposePrBodyReport {
const COMMAND: &'static str = "compose-pr-body";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ValidateReport {
pub schema_version: u32,
pub valid: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for ValidateReport {
const COMMAND: &'static str = "validate";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TagReport {
pub schema_version: u32,
pub created_tags: Vec<CreatedTag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for TagReport {
const COMMAND: &'static str = "tag";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreatedTag {
pub package: PackageId,
pub tag_name: TagName,
pub sha: CommitSha,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct InitReport {
pub schema_version: u32,
pub initialized: bool,
pub config_path: PathBuf,
#[serde(default)]
pub diff: InitDiff,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
}
impl Report for InitReport {
const COMMAND: &'static str = "init";
fn schema_version(&self) -> u32 {
self.schema_version
}
fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct InitDiff {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub new_ecosystems: Vec<Ecosystem>,
#[serde(default)]
pub applied: bool,
}