use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
static YAML_OUTPUT: AtomicBool = AtomicBool::new(false);
pub fn set_yaml_output(yaml: bool) {
YAML_OUTPUT.store(yaml, Ordering::Relaxed);
}
pub const SCHEMA_VERSION: &str = "0.1";
fn print_to_stdout(value: &impl Serialize) {
if YAML_OUTPUT.load(Ordering::Relaxed) {
print!(
"{}",
serde_yaml::to_string(value).expect("YAML serialization failed")
);
} else {
println!(
"{}",
serde_json::to_string(value).expect("JSON serialization failed")
);
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Response<T: Serialize> {
pub schema_version: &'static str,
pub ok: bool,
#[serde(rename = "type")]
pub kind: &'static str,
#[serde(flatten)]
pub data: T,
}
impl<T: Serialize> Response<T> {
pub fn new(kind: &'static str, data: T) -> Self {
Response {
schema_version: SCHEMA_VERSION,
ok: true,
kind,
data,
}
}
pub fn print(&self) {
print_to_stdout(self);
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub schema_version: &'static str,
pub ok: bool,
#[serde(rename = "type")]
pub kind: &'static str,
pub error: ErrorDetail,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorDetail {
pub code: String,
pub message: String,
pub retryable: bool,
}
impl ErrorResponse {
pub fn new(code: impl Into<String>, message: impl Into<String>, retryable: bool) -> Self {
ErrorResponse {
schema_version: SCHEMA_VERSION,
ok: false,
kind: "error",
error: ErrorDetail {
code: code.into(),
message: message.into(),
retryable,
},
}
}
pub fn print(&self) {
print_to_stdout(self);
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateData {
pub job_id: String,
pub state: String,
pub stdout_log_path: String,
pub stderr_log_path: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RunData {
pub job_id: String,
pub state: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub env_vars: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub snapshot: Option<Snapshot>,
pub stdout_log_path: String,
pub stderr_log_path: String,
pub waited_ms: u64,
pub elapsed_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finished_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub final_snapshot: Option<Snapshot>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusData {
pub job_id: String,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finished_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TailData {
pub job_id: String,
pub stdout_tail: String,
pub stderr_tail: String,
pub truncated: bool,
pub encoding: String,
pub stdout_log_path: String,
pub stderr_log_path: String,
pub stdout_observed_bytes: u64,
pub stderr_observed_bytes: u64,
pub stdout_included_bytes: u64,
pub stderr_included_bytes: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WaitData {
pub job_id: String,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KillData {
pub job_id: String,
pub signal: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SchemaData {
pub schema_format: String,
pub schema: serde_json::Value,
pub generated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JobSummary {
pub job_id: String,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
pub created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finished_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TagSetData {
pub job_id: String,
pub tags: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListData {
pub root: String,
pub jobs: Vec<JobSummary>,
pub truncated: bool,
pub skipped: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GcJobResult {
pub job_id: String,
pub state: String,
pub action: String,
pub reason: String,
pub bytes: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GcData {
pub root: String,
pub dry_run: bool,
pub older_than: String,
pub older_than_source: String,
pub deleted: u64,
pub skipped: u64,
pub freed_bytes: u64,
pub jobs: Vec<GcJobResult>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteJobResult {
pub job_id: String,
pub state: String,
pub action: String,
pub reason: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteData {
pub root: String,
pub dry_run: bool,
pub deleted: u64,
pub skipped: u64,
pub jobs: Vec<DeleteJobResult>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InstalledSkillSummary {
pub name: String,
pub source_type: String,
pub path: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NotifySetData {
pub job_id: String,
pub notification: NotificationConfig,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InstallSkillsData {
pub skills: Vec<InstalledSkillSummary>,
pub global: bool,
pub lock_file_path: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Snapshot {
pub stdout_tail: String,
pub stderr_tail: String,
pub truncated: bool,
pub encoding: String,
pub stdout_observed_bytes: u64,
pub stderr_observed_bytes: u64,
pub stdout_included_bytes: u64,
pub stderr_included_bytes: u64,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OutputMatchType {
#[default]
Contains,
Regex,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OutputMatchStream {
Stdout,
Stderr,
#[default]
Either,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputMatchConfig {
pub pattern: String,
#[serde(default)]
pub match_type: OutputMatchType,
#[serde(default)]
pub stream: OutputMatchStream,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct NotificationConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub notify_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notify_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_output_match: Option<OutputMatchConfig>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompletionEvent {
pub schema_version: String,
pub event_type: String,
pub job_id: String,
pub state: String,
pub command: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
pub started_at: String,
pub finished_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signal: Option<String>,
pub stdout_log_path: String,
pub stderr_log_path: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SinkDeliveryResult {
pub sink_type: String,
pub target: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub attempted_at: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CompletionEventRecord {
#[serde(flatten)]
pub event: CompletionEvent,
pub delivery_results: Vec<SinkDeliveryResult>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputMatchEvent {
pub schema_version: String,
pub event_type: String,
pub job_id: String,
pub pattern: String,
pub match_type: String,
pub stream: String,
pub line: String,
pub stdout_log_path: String,
pub stderr_log_path: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputMatchEventRecord {
#[serde(flatten)]
pub event: OutputMatchEvent,
pub delivery_results: Vec<SinkDeliveryResult>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobMetaJob {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobMeta {
pub job: JobMetaJob,
pub schema_version: String,
pub command: Vec<String>,
pub created_at: String,
pub root: String,
pub env_keys: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub env_vars: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub env_vars_runtime: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub mask: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub notification: Option<NotificationConfig>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default = "default_inherit_env")]
pub inherit_env: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub env_files: Vec<String>,
#[serde(default)]
pub timeout_ms: u64,
#[serde(default)]
pub kill_after_ms: u64,
#[serde(default)]
pub progress_every_ms: u64,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub shell_wrapper: Option<Vec<String>>,
}
fn default_inherit_env() -> bool {
true
}
impl JobMeta {
pub fn job_id(&self) -> &str {
&self.job.id
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobStateJob {
pub id: String,
pub status: JobStatus,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub started_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobStateResult {
pub exit_code: Option<i32>,
pub signal: Option<String>,
pub duration_ms: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct JobState {
pub job: JobStateJob,
pub result: JobStateResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finished_at: Option<String>,
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub windows_job_name: Option<String>,
}
impl JobState {
pub fn job_id(&self) -> &str {
&self.job.id
}
pub fn status(&self) -> &JobStatus {
&self.job.status
}
pub fn started_at(&self) -> Option<&str> {
self.job.started_at.as_deref()
}
pub fn exit_code(&self) -> Option<i32> {
self.result.exit_code
}
pub fn signal(&self) -> Option<&str> {
self.result.signal.as_deref()
}
pub fn duration_ms(&self) -> Option<u64> {
self.result.duration_ms
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
Created,
Running,
Exited,
Killed,
Failed,
}
impl JobStatus {
pub fn as_str(&self) -> &'static str {
match self {
JobStatus::Created => "created",
JobStatus::Running => "running",
JobStatus::Exited => "exited",
JobStatus::Killed => "killed",
JobStatus::Failed => "failed",
}
}
pub fn is_non_terminal(&self) -> bool {
matches!(self, JobStatus::Created | JobStatus::Running)
}
}