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 enum EmbeddedRequest {
SingleDocument {
kind: TrailerKind,
source_name: String,
document: String,
manifest_json: String,
args: ArtifactArgs,
},
VirtualStore {
kind: TrailerKind,
store: super::store::VirtualDocumentStore,
manifest_json: String,
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::SingleDocument {
kind: trailer.kind,
source_name,
document,
manifest_json,
args,
})
}
pub fn from_v2(v2: trailer::TrailerV2, args: ArtifactArgs) -> Result<Self, CompileError> {
let store = super::store::VirtualDocumentStore::decode(v2.content, &v2.index)
.map_err(|e| CompileError::InvalidDocument(format!("invalid virtual store: {e}")))?;
super::store::validate_typed_references(&store.index, v2.kind)
.map_err(|e| CompileError::InvalidDocument(format!("invalid virtual store: {e}")))?;
let manifest_json =
String::from_utf8(v2.manifest).map_err(|_| CompileError::InvalidUtf8)?;
Ok(Self::VirtualStore {
kind: v2.kind,
store,
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 decoded = match trailer::decode_artifact(&bytes) {
Ok(Some(decoded)) => decoded,
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 decoded {
trailer::DecodedArtifact::V1(v1) => EmbeddedRequest::from_trailer(v1, args),
trailer::DecodedArtifact::V2(v2) => EmbeddedRequest::from_v2(v2, args),
};
match request {
Ok(request) => Some(run_embedded_document_code(request).await),
Err(e) => {
eprintln!("compiled artifact integrity error: {e}");
Some(EXIT_REJECTION)
}
}
}
pub async fn run_embedded_document_code(request: EmbeddedRequest) -> i32 {
let (help, version, manifest, report) = match &request {
EmbeddedRequest::SingleDocument { args, .. }
| EmbeddedRequest::VirtualStore { args, .. } => {
(args.help, args.version, args.manifest, args.report.clone())
}
};
if help {
print_artifact_usage();
return 0;
}
if version {
println!("camel {}", manifest::RUNTIME_VERSION);
return 0;
}
if manifest {
let manifest_json = match &request {
EmbeddedRequest::SingleDocument { manifest_json, .. }
| EmbeddedRequest::VirtualStore { manifest_json, .. } => manifest_json,
};
println!("{manifest_json}");
return 0;
}
match request {
EmbeddedRequest::SingleDocument {
kind,
source_name,
document,
..
} => match kind {
TrailerKind::Route => {
run_embedded_route(&source_name, &document, report.as_deref()).await
}
TrailerKind::Job => {
crate::commands::job::run_embedded_job(&source_name, &document, report).await
}
},
EmbeddedRequest::VirtualStore { kind, store, .. } => match kind {
TrailerKind::Route => run_embedded_store_route(&store, report.as_deref()).await,
TrailerKind::Job => crate::commands::job::run_embedded_job_store(store, 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()),
}
}
#[derive(Debug)]
pub(crate) enum VirtualStoreResolveError {
Discovery(camel_dsl::DiscoveryError),
Config(config::ConfigError),
}
impl fmt::Display for VirtualStoreResolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Discovery(e) => e.fmt(f),
Self::Config(e) => e.fmt(f),
}
}
}
impl std::error::Error for VirtualStoreResolveError {}
pub(crate) fn resolve_virtual_store(
store: &super::store::VirtualDocumentStore,
env: &dyn Fn(&str) -> Option<String>,
) -> Result<
(
camel_config::config::CamelConfig,
Vec<camel_core::RouteDefinition>,
),
VirtualStoreResolveError,
> {
let discovery = camel_dsl::discover_virtual_store(store, env)
.map_err(VirtualStoreResolveError::Discovery)?;
let config = camel_config::config::CamelConfig::from_toml_value_with_env(discovery.config, env)
.map_err(VirtualStoreResolveError::Config)?;
Ok((config, discovery.routes))
}
async fn run_embedded_store_route(
store: &super::store::VirtualDocumentStore,
report: Option<&Path>,
) -> i32 {
let identity = store.index.entry_point.clone();
let ambient = |name: &str| std::env::var(name).ok();
let (config, routes) = match resolve_virtual_store(store, &ambient) {
Ok(resolved) => resolved,
Err(e) => {
eprintln!("compiled://{identity}: {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::VirtualStore { routes },
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, TrailerKind};
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"}"#
);
}
#[test]
fn from_v2_rejects_kind_mismatched_stores() {
use super::super::store::{StoreDocument, StoreEntryKind};
let route_doc = |path: &str| StoreDocument {
path: path.to_string(),
kind: StoreEntryKind::Route,
bytes: b"routes:\n - id: demo\n".to_vec(),
};
let job_doc = |path: &str| StoreDocument {
path: path.to_string(),
kind: StoreEntryKind::Job,
bytes: b"execute:\n mode: one-shot\n".to_vec(),
};
let v2 = |store: &super::super::store::VirtualDocumentStore| {
use super::super::trailer::TrailerV2;
TrailerV2 {
kind: TrailerKind::Route,
content: store.content.clone(),
index: store.index.encode_canonical().expect("canonical index"),
manifest: br#"{"manifest_schema":2,"source_name":"app.yaml"}"#.to_vec(),
}
};
let route_store = super::super::store::VirtualDocumentStore::build(
"app.yaml",
&[route_doc("app.yaml")],
&[],
&["app.yaml".to_string()],
)
.expect("route store builds");
let request = super::EmbeddedRequest::from_v2(v2(&route_store), Default::default())
.expect("kind-agreeing store builds a request");
assert!(matches!(
request,
super::EmbeddedRequest::VirtualStore { .. }
));
let job_store = super::super::store::VirtualDocumentStore::build(
"ingest.job.yaml",
&[job_doc("ingest.job.yaml")],
&[],
&["ingest.job.yaml".to_string()],
)
.expect("job store builds");
let err = super::EmbeddedRequest::from_v2(v2(&job_store), Default::default())
.expect_err("kind-mismatched store must fail closed");
assert!(
err.to_string().contains("expected route"),
"the rejection must name the kind mismatch: {err}"
);
}
}