use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OrbDefinition {
#[serde(default)]
pub version: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub display: Option<DisplayInfo>,
#[serde(default)]
pub orbs: HashMap<String, String>,
#[serde(default)]
pub commands: HashMap<String, Command>,
#[serde(default)]
pub jobs: HashMap<String, Job>,
#[serde(default)]
pub executors: HashMap<String, Executor>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DisplayInfo {
#[serde(default)]
pub home_url: Option<String>,
#[serde(default)]
pub source_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Command {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub parameters: HashMap<String, Parameter>,
#[serde(default)]
pub steps: Vec<Step>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExecutorConfig {
#[serde(default)]
pub docker: Option<Vec<DockerImage>>,
#[serde(default)]
pub machine: Option<MachineConfig>,
#[serde(default)]
pub macos: Option<MacOsConfig>,
#[serde(default)]
pub resource_class: Option<String>,
#[serde(default)]
pub working_directory: Option<String>,
#[serde(default)]
pub environment: HashMap<String, String>,
#[serde(default)]
pub shell: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Job {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub executor: Option<ExecutorRef>,
#[serde(flatten)]
pub config: ExecutorConfig,
#[serde(default)]
pub parameters: HashMap<String, Parameter>,
#[serde(default)]
pub steps: Vec<Step>,
#[serde(default)]
pub parallelism: Option<u32>,
#[serde(default)]
pub circleci_ip_ranges: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Executor {
#[serde(default)]
pub description: Option<String>,
#[serde(flatten)]
pub config: ExecutorConfig,
#[serde(default)]
pub parameters: HashMap<String, Parameter>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExecutorRef {
Name(String),
WithParams {
name: String,
#[serde(flatten)]
parameters: HashMap<String, serde_yaml::Value>,
},
}
impl Default for ExecutorRef {
fn default() -> Self {
Self::Name(String::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Parameter {
#[serde(rename = "type")]
pub param_type: ParameterType,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub default: Option<serde_yaml::Value>,
#[serde(default, rename = "enum")]
pub enum_values: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ParameterType {
#[default]
String,
Boolean,
Integer,
Enum,
#[serde(rename = "env_var_name")]
EnvVarName,
Steps,
Executor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Step {
Simple(String),
Structured(StructuredStep),
}
impl Default for Step {
fn default() -> Self {
Self::Simple(String::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StructuredStep {
Run(RunStep),
Checkout(CheckoutStep),
#[serde(rename = "restore_cache")]
RestoreCache(CacheStep),
#[serde(rename = "save_cache")]
SaveCache(SaveCacheStep),
When(ConditionalStep),
Unless(ConditionalStep),
#[serde(rename = "persist_to_workspace")]
PersistToWorkspace(WorkspaceStep),
#[serde(rename = "attach_workspace")]
AttachWorkspace(AttachWorkspaceStep),
#[serde(rename = "store_test_results")]
StoreTestResults(StoreTestResultsStep),
#[serde(rename = "store_artifacts")]
StoreArtifacts(StoreArtifactsStep),
#[serde(rename = "add_ssh_keys")]
AddSshKeys(AddSshKeysStep),
#[serde(rename = "setup_remote_docker")]
SetupRemoteDocker(SetupRemoteDockerStep),
#[serde(untagged)]
CommandInvocation(HashMap<String, serde_yaml::Value>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RunStep {
Simple(String),
Full {
command: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
working_directory: Option<String>,
#[serde(default)]
environment: HashMap<String, String>,
#[serde(default)]
shell: Option<String>,
#[serde(default)]
background: Option<bool>,
#[serde(default)]
no_output_timeout: Option<String>,
#[serde(default)]
when: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CheckoutStep {
#[serde(default)]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CacheStep {
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub keys: Option<Vec<String>>,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SaveCacheStep {
pub key: String,
#[serde(default)]
pub paths: Vec<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub when: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConditionalStep {
pub condition: serde_yaml::Value,
#[serde(default)]
pub steps: Vec<Step>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceStep {
pub root: String,
#[serde(default)]
pub paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AttachWorkspaceStep {
pub at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StoreTestResultsStep {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StoreArtifactsStep {
pub path: String,
#[serde(default)]
pub destination: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AddSshKeysStep {
#[serde(default)]
pub fingerprints: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SetupRemoteDockerStep {
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub docker_layer_caching: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DockerImage {
Simple(String),
Full(Box<DockerImageFull>),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DockerImageFull {
pub image: String,
#[serde(default)]
pub auth: Option<DockerAuth>,
#[serde(default)]
pub aws_auth: Option<AwsAuth>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub entrypoint: Option<Vec<String>>,
#[serde(default)]
pub command: Option<Vec<String>>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub environment: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DockerAuth {
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AwsAuth {
#[serde(default)]
pub aws_access_key_id: Option<String>,
#[serde(default)]
pub aws_secret_access_key: Option<String>,
#[serde(default)]
pub oidc_role_arn: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MachineConfig {
Enabled(bool),
Image {
image: String,
#[serde(default)]
docker_layer_caching: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MacOsConfig {
pub xcode: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parameter_type_deserialize() {
let yaml = r#"string"#;
let pt: ParameterType = serde_yaml::from_str(yaml).unwrap();
assert_eq!(pt, ParameterType::String);
let yaml = r#"boolean"#;
let pt: ParameterType = serde_yaml::from_str(yaml).unwrap();
assert_eq!(pt, ParameterType::Boolean);
let yaml = r#"env_var_name"#;
let pt: ParameterType = serde_yaml::from_str(yaml).unwrap();
assert_eq!(pt, ParameterType::EnvVarName);
}
#[test]
fn test_simple_command_deserialize() {
let yaml = r#"
description: "Run tests"
parameters:
coverage:
type: boolean
default: false
description: "Enable coverage"
steps:
- checkout
- run: cargo test
"#;
let cmd: Command = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cmd.description, Some("Run tests".to_string()));
assert!(cmd.parameters.contains_key("coverage"));
assert_eq!(cmd.steps.len(), 2);
}
#[test]
fn test_docker_image_simple() {
let yaml = r#""rust:1.75""#;
let img: DockerImage = serde_yaml::from_str(yaml).unwrap();
matches!(img, DockerImage::Simple(s) if s == "rust:1.75");
}
#[test]
fn test_docker_image_full() {
let yaml = r#"
image: rust:1.75
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
"#;
let img: DockerImage = serde_yaml::from_str(yaml).unwrap();
match img {
DockerImage::Full(full) => {
assert_eq!(full.image, "rust:1.75");
assert!(full.auth.is_some());
}
_ => panic!("Expected Full variant"),
}
}
#[test]
fn test_executor_ref_simple() {
let yaml = r#""default""#;
let exec: ExecutorRef = serde_yaml::from_str(yaml).unwrap();
matches!(exec, ExecutorRef::Name(s) if s == "default");
}
#[test]
fn test_step_simple() {
let yaml = r#""checkout""#;
let step: Step = serde_yaml::from_str(yaml).unwrap();
matches!(step, Step::Simple(s) if s == "checkout");
}
#[test]
fn test_run_step_simple() {
let yaml = r#"
run: echo hello
"#;
let step: StructuredStep = serde_yaml::from_str(yaml).unwrap();
match step {
StructuredStep::Run(RunStep::Simple(cmd)) => {
assert_eq!(cmd, "echo hello");
}
_ => panic!("Expected Run with Simple variant"),
}
}
#[test]
fn test_run_step_full() {
let yaml = r#"
run:
name: Run tests
command: cargo test
working_directory: ~/project
"#;
let step: StructuredStep = serde_yaml::from_str(yaml).unwrap();
match step {
StructuredStep::Run(RunStep::Full { command, name, .. }) => {
assert_eq!(command, "cargo test");
assert_eq!(name, Some("Run tests".to_string()));
}
_ => panic!("Expected Run with Full variant"),
}
}
#[test]
fn test_orb_definition_empty() {
let yaml = r#"
version: "2.1"
"#;
let orb: OrbDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(orb.version, "2.1");
assert!(orb.commands.is_empty());
assert!(orb.jobs.is_empty());
assert!(orb.executors.is_empty());
}
}