use serde::{Deserialize, Serialize};
use super::{InternalVerb, VerbCategory};
pub struct CiPipelineRun;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiPipelineRunInput {
pub pipeline: String,
#[serde(rename = "ref")]
pub git_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variables: Option<std::collections::BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiPipelineRunOutput {
pub run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl InternalVerb for CiPipelineRun {
type Input = CiPipelineRunInput;
type Output = CiPipelineRunOutput;
const ID: &'static str = "ci.pipeline.run";
const CATEGORY: VerbCategory = VerbCategory::Ci;
}
pub struct CiPipelineStatus;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiPipelineStatusInput {
pub run_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PipelinePhase {
Pending,
Running,
Passing,
Failing,
Cancelled,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiPipelineStatusOutput {
pub phase: PipelinePhase,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl InternalVerb for CiPipelineStatus {
type Input = CiPipelineStatusInput;
type Output = CiPipelineStatusOutput;
const ID: &'static str = "ci.pipeline.status";
const CATEGORY: VerbCategory = VerbCategory::Ci;
}
pub struct CiArtifactFetch;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiArtifactFetchInput {
pub run_id: String,
pub artifact_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct CiArtifactFetchOutput {
pub download_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<u64>,
}
impl InternalVerb for CiArtifactFetch {
type Input = CiArtifactFetchInput;
type Output = CiArtifactFetchOutput;
const ID: &'static str = "ci.artifact.fetch";
const CATEGORY: VerbCategory = VerbCategory::Ci;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verb_ids_match_canonical_namespace() {
for id in [CiPipelineRun::ID, CiPipelineStatus::ID, CiArtifactFetch::ID] {
assert!(id.starts_with("ci."), "{id}");
}
}
#[test]
fn verbs_are_under_ci_category() {
assert_eq!(CiPipelineRun::CATEGORY, VerbCategory::Ci);
assert_eq!(CiPipelineStatus::CATEGORY, VerbCategory::Ci);
assert_eq!(CiArtifactFetch::CATEGORY, VerbCategory::Ci);
}
#[test]
fn pipeline_run_input_ref_renamed_in_wire() {
let wire = r#"{"pipeline":"release.yml","ref":"main"}"#;
let parsed: CiPipelineRunInput = serde_json::from_str(wire).unwrap();
assert_eq!(parsed.git_ref, "main");
let back = serde_json::to_value(&parsed).unwrap();
assert!(back.get("ref").is_some(), "must serialize as 'ref'");
assert!(back.get("git_ref").is_none());
}
#[test]
fn pipeline_run_variables_optional() {
let no_vars = r#"{"pipeline":"build.yml","ref":"main"}"#;
let parsed: CiPipelineRunInput = serde_json::from_str(no_vars).unwrap();
assert!(parsed.variables.is_none());
let with_vars = r#"{"pipeline":"build.yml","ref":"main","variables":{"ENV":"staging"}}"#;
let parsed: CiPipelineRunInput = serde_json::from_str(with_vars).unwrap();
assert_eq!(
parsed
.variables
.as_ref()
.unwrap()
.get("ENV")
.map(|s| s.as_str()),
Some("staging")
);
}
#[test]
fn pipeline_phase_is_snake_case() {
assert_eq!(
serde_json::to_string(&PipelinePhase::Passing).unwrap(),
"\"passing\""
);
assert_eq!(
serde_json::to_string(&PipelinePhase::Unknown).unwrap(),
"\"unknown\""
);
}
#[test]
fn pipeline_status_omits_detail_when_absent() {
let out = CiPipelineStatusOutput {
phase: PipelinePhase::Running,
url: None,
detail: None,
};
let wire = serde_json::to_value(&out).unwrap();
assert!(!wire.as_object().unwrap().contains_key("detail"));
}
#[test]
fn artifact_fetch_output_omits_optional_fields_when_absent() {
let out = CiArtifactFetchOutput {
download_url: "https://example.com/artifact.tar.gz".into(),
expires_at: None,
size_bytes: None,
};
let wire = serde_json::to_value(&out).unwrap();
assert!(!wire.as_object().unwrap().contains_key("expires_at"));
assert!(!wire.as_object().unwrap().contains_key("size_bytes"));
}
#[cfg(feature = "json-schema")]
#[test]
fn verbs_emit_schemas_via_for_verb() {
use super::super::VerbDescriptor;
let run = VerbDescriptor::for_verb::<CiPipelineRun>();
assert_eq!(run.id, "ci.pipeline.run");
assert!(run.input_schema.to_string().contains("pipeline"));
let status = VerbDescriptor::for_verb::<CiPipelineStatus>();
assert_eq!(status.id, "ci.pipeline.status");
assert!(status.output_schema.to_string().contains("phase"));
let fetch = VerbDescriptor::for_verb::<CiArtifactFetch>();
assert_eq!(fetch.id, "ci.artifact.fetch");
assert!(fetch.output_schema.to_string().contains("download_url"));
}
}