use std::fmt;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use serde::Serialize;
use super::CompileError;
use super::manifest;
use super::trailer::{self, Trailer, TrailerKind};
use crate::commands::run::{Discover, LifecycleFailure, LifecycleSpec};
const EXIT_REJECTION: i32 = 2;
const IDLE_NOTE: &str = "compiled artifact running (hot-reload disabled). Press Ctrl+C to stop.";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ArtifactArgs {
pub report: Option<PathBuf>,
pub help: bool,
pub version: bool,
pub manifest: bool,
}
impl ArtifactArgs {
fn first_set(&self) -> Option<&'static str> {
if self.report.is_some() {
Some("--report")
} else if self.help {
Some("--help")
} else if self.version {
Some("--version")
} else if self.manifest {
Some("--manifest")
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArtifactArgError {
Duplicate(&'static str),
Exclusive(&'static str, &'static str),
MissingValue(&'static str),
Unknown(String),
Positional(String),
}
impl fmt::Display for ArtifactArgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Duplicate(flag) => write!(f, "duplicate argument '{flag}'"),
Self::Exclusive(a, b) => {
write!(f, "arguments '{a}' and '{b}' are mutually exclusive")
}
Self::MissingValue(flag) => {
write!(f, "argument '{flag}' requires a value")
}
Self::Unknown(arg) => write!(f, "unknown argument '{arg}'"),
Self::Positional(arg) => write!(f, "unexpected positional argument '{arg}'"),
}
}
}
impl std::error::Error for ArtifactArgError {}
impl ArtifactArgs {
pub fn parse(args: &[String]) -> Result<Self, ArtifactArgError> {
let mut parsed = Self::default();
let mut idx = 0;
while idx < args.len() {
let arg = args[idx].as_str();
match arg {
"--report" => {
if let Some(prev) = parsed.first_set() {
return Err(if prev == "--report" {
ArtifactArgError::Duplicate(prev)
} else {
ArtifactArgError::Exclusive(prev, "--report")
});
}
let Some(value) = args.get(idx + 1) else {
return Err(ArtifactArgError::MissingValue("--report"));
};
if value.starts_with('-') {
return Err(ArtifactArgError::MissingValue("--report"));
}
parsed.report = Some(PathBuf::from(value));
idx += 2;
}
"--help" | "--version" | "--manifest" => {
let flag: &'static str = match arg {
"--help" => "--help",
"--version" => "--version",
_ => "--manifest",
};
if let Some(prev) = parsed.first_set() {
return Err(if prev == flag {
ArtifactArgError::Duplicate(flag)
} else {
ArtifactArgError::Exclusive(prev, flag)
});
}
match flag {
"--help" => parsed.help = true,
"--version" => parsed.version = true,
_ => parsed.manifest = true,
}
idx += 1;
}
other if other.starts_with('-') => {
return Err(ArtifactArgError::Unknown(other.to_string()));
}
other => {
return Err(ArtifactArgError::Positional(other.to_string()));
}
}
}
Ok(parsed)
}
}
#[derive(Debug, Serialize)]
pub struct RouteReport {
kind: &'static str,
status: &'static str,
error: Option<String>,
}
impl RouteReport {
fn completed() -> Self {
Self {
kind: "route",
status: "completed",
error: None,
}
}
fn failed(error: String) -> Self {
Self {
kind: "route",
status: "failed",
error: Some(error),
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("route report serialization cannot fail") }
}
#[derive(Debug, Clone)]
pub struct EmbeddedRequest {
pub kind: TrailerKind,
pub source_name: String,
pub document: String,
pub manifest_json: String,
pub args: ArtifactArgs,
}
impl EmbeddedRequest {
pub fn from_trailer(trailer: Trailer, args: ArtifactArgs) -> Result<Self, CompileError> {
let document = String::from_utf8(trailer.payload).map_err(|_| CompileError::InvalidUtf8)?;
let manifest_json =
String::from_utf8(trailer.manifest).map_err(|_| CompileError::InvalidUtf8)?;
let source_name = serde_json::from_str::<serde_json::Value>(&manifest_json)
.ok()
.and_then(|value| {
value
.get("source_name")
.and_then(|name| name.as_str())
.map(str::to_string)
})
.ok_or_else(|| {
CompileError::InvalidDocument("manifest carries no source_name".to_string())
})?;
Ok(Self {
kind: trailer.kind,
source_name,
document,
manifest_json,
args,
})
}
}
pub async fn run_embedded_document(request: EmbeddedRequest) -> ExitCode {
ExitCode::from(run_embedded_document_code(request).await as u8)
}
pub async fn self_detect_artifact() -> Option<i32> {
let exe = std::env::current_exe().ok()?;
let bytes = std::fs::read(exe).ok()?;
let trailer = match trailer::decode(&bytes) {
Ok(Some(trailer)) => trailer,
Ok(None) => return None,
Err(e) => {
eprintln!("compiled artifact integrity error: {e}");
return Some(EXIT_REJECTION);
}
};
let argv: Vec<String> = std::env::args().skip(1).collect();
let args = match ArtifactArgs::parse(&argv) {
Ok(args) => args,
Err(e) => {
eprintln!("{e}");
return Some(EXIT_REJECTION);
}
};
let request = match EmbeddedRequest::from_trailer(trailer, args) {
Ok(request) => request,
Err(e) => {
eprintln!("compiled artifact integrity error: {e}");
return Some(EXIT_REJECTION);
}
};
Some(run_embedded_document_code(request).await)
}
pub async fn run_embedded_document_code(request: EmbeddedRequest) -> i32 {
let EmbeddedRequest {
kind,
source_name,
document,
manifest_json,
args,
} = request;
if args.help {
print_artifact_usage();
return 0;
}
if args.version {
println!("camel {}", manifest::RUNTIME_VERSION);
return 0;
}
if args.manifest {
println!("{manifest_json}");
return 0;
}
match kind {
TrailerKind::Route => {
run_embedded_route(&source_name, &document, args.report.as_deref()).await
}
TrailerKind::Job => {
crate::commands::job::run_embedded_job(&source_name, &document, args.report).await
}
}
}
fn print_artifact_usage() {
println!("camel compiled artifact usage:");
println!(" --report <path> write the run report to <path>");
println!(" --manifest print the operational manifest and exit");
println!(" --version print the runtime version and exit");
println!(" --help print this usage and exit");
}
async fn run_embedded_route(source_name: &str, document: &str, report: Option<&Path>) -> i32 {
let config = match crate::commands::run::in_memory_default_config() {
Ok(config) => config,
Err(e) => return fail_route(report, e.to_string()),
};
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let spec = LifecycleSpec {
config,
project_root,
discover: Discover::Embedded {
text: document.to_string(),
source_name: source_name.to_string(),
kind: camel_dsl::EmbeddedDocumentKind::Route,
},
watch: None,
trust_note: false,
idle_note: IDLE_NOTE,
};
match crate::commands::run::drive_lifecycle(spec).await {
Ok(()) => {
if let Err(e) = write_route_report(report, &RouteReport::completed()) {
eprintln!("failed to write route report: {e}");
return EXIT_REJECTION;
}
0
}
Err(LifecycleFailure::Discovery(e)) => fail_route(report, e.to_string()),
Err(LifecycleFailure::Boot(e)) => fail_route(report, e.to_string()),
}
}
fn fail_route(report: Option<&Path>, error: String) -> i32 {
if let Err(e) = write_route_report(report, &RouteReport::failed(error.clone())) {
eprintln!("failed to write route report: {e}");
}
EXIT_REJECTION
}
fn write_route_report(report: Option<&Path>, value: &RouteReport) -> std::io::Result<()> {
match report {
Some(path) => std::fs::write(path, format!("{}\n", value.to_json())),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::{ArtifactArgError, ArtifactArgs, RouteReport};
fn argv(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
#[test]
fn artifact_args_accept_the_documented_surface() {
let args = ArtifactArgs::parse(&argv(&["--report", "out.json"])).expect("report parses");
assert_eq!(args.report, Some(std::path::PathBuf::from("out.json")));
assert!(ArtifactArgs::parse(&argv(&["--help"])).expect("help").help);
assert!(
ArtifactArgs::parse(&argv(&["--version"]))
.expect("version")
.version
);
assert!(
ArtifactArgs::parse(&argv(&["--manifest"]))
.expect("manifest")
.manifest
);
assert!(
ArtifactArgs::parse(&argv(&[]))
.expect("bare run")
.report
.is_none()
);
}
#[test]
fn artifact_args_reject_misuse() {
assert_eq!(
ArtifactArgs::parse(&argv(&["--report", "a", "--report", "b"])),
Err(ArtifactArgError::Duplicate("--report"))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["--report"])),
Err(ArtifactArgError::MissingValue("--report"))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["--report", "--manifest"])),
Err(ArtifactArgError::MissingValue("--report"))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["--help", "--version"])),
Err(ArtifactArgError::Exclusive("--help", "--version"))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["--report", "r", "--manifest"])),
Err(ArtifactArgError::Exclusive("--report", "--manifest"))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["--watch"])),
Err(ArtifactArgError::Unknown("--watch".to_string()))
);
assert_eq!(
ArtifactArgs::parse(&argv(&["routes.yaml"])),
Err(ArtifactArgError::Positional("routes.yaml".to_string()))
);
}
#[test]
fn route_report_serializes_exact_json() {
assert_eq!(
RouteReport::completed().to_json(),
r#"{"kind":"route","status":"completed","error":null}"#
);
assert_eq!(
RouteReport::failed("boom".to_string()).to_json(),
r#"{"kind":"route","status":"failed","error":"boom"}"#
);
}
}