use std::path::Path;
use camel_api::CamelError;
use camel_bundles::BootHandle;
use camel_config::config::CamelConfig;
use camel_core::CamelContext;
use crate::document::{RouteSource, ScenarioDocument};
use crate::env_layers::LayeredEnv;
pub struct ScenarioRun {
pub ctx: CamelContext,
pub boot: BootHandle,
pub inbound_bound: Option<std::net::SocketAddr>,
}
pub async fn boot_scenario(
doc: &ScenarioDocument,
root: &Path,
env: &LayeredEnv,
) -> Result<ScenarioRun, CamelError> {
let doc_dir = doc
.source_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| Path::new(".").to_path_buf());
let config_path = root.join("Camel.toml");
let config = CamelConfig::from_file_sealed(
config_path.to_str().ok_or_else(|| {
CamelError::Config(format!(
"scenario config path is not valid utf-8: {}",
config_path.display()
))
})?,
doc.profile.as_deref().unwrap_or("default"),
&|name| env.lookup(name),
)
.map_err(|e| {
CamelError::Config(format!(
"failed to load scenario config {}: {e}",
config_path.display()
))
})?;
crate::sql_action::ensure_sqlite_memory_shared(&config)?;
let mut ctx = CamelConfig::configure_context_with_beans(&config, None).await?;
if config.security.keycloak.is_some() || config.security.oidc.is_some() {
return Err(CamelError::AuthProviderUnavailable(
"scenario tier runs offline (no network): keycloak/oidc security requires \
a network-prefetching auth provider; v1 supports the native provider only"
.to_string(),
));
}
if config.security.policies.is_some() || config.security.permissions.is_some() {
return Err(CamelError::Config(
"wasm security policies/permissions are not supported in the scenario \
tier in v1 (offline tier; later wave)"
.to_string(),
));
}
#[cfg(feature = "security")]
let security_ctx = camel_bundles::security_boot::build_security_compile_context_from_config(
&config,
ctx.registry_arc(),
)
.await?;
#[cfg(not(feature = "security"))]
let security_ctx = {
camel_bundles::security_boot::ensure_security_supported(&config)?;
camel_dsl::SecurityCompileContext::default()
};
camel_bundles::security_boot::install_bind_exposure_acks(&mut ctx, &config).await;
#[cfg(feature = "http")]
let provisioned = match doc.inbound.as_ref() {
Some(entry) => {
let bound = crate::inbound::provision_inbound(entry).await?;
Some((
env.with_harness_var(&entry.bind_var, format!("http://{bound}")),
bound,
))
}
None => None,
};
#[cfg(feature = "http")]
let (discovery_env, inbound_bound) = match &provisioned {
Some((extended, bound)) => (extended, Some(*bound)),
None => (env, None),
};
#[cfg(not(feature = "http"))]
let (discovery_env, inbound_bound) = if doc.inbound.is_some() {
return Err(CamelError::Config(
"inbound listeners need the `http` feature to boot: enable it to \
provision the staged listener (the load-time doc gate now fires \
first; this rejection is defense-in-depth for directly-constructed \
documents)"
.to_string(),
));
} else {
(env, None)
};
let boot = camel_bundles::boot(&mut ctx, &config, root).await?;
let defs = camel_dsl::discover_routes_with_threshold_security_and_env(
&route_patterns(doc, root, &doc_dir)?,
config.stream_caching.threshold,
security_ctx,
&|name| discovery_env.lookup(name),
)
.map_err(map_discovery_error)?;
camel_bundles::security_boot::install_sql_startup_checks(&mut ctx, &defs);
for def in defs {
ctx.add_route_definition(def).await?;
}
ctx.start().await?;
Ok(ScenarioRun {
ctx,
boot,
inbound_bound,
})
}
fn route_patterns(
doc: &ScenarioDocument,
root: &Path,
doc_dir: &Path,
) -> Result<Vec<String>, CamelError> {
fn anchored(base: &Path, file: &Path) -> Result<String, CamelError> {
let full = base.join(file);
std::fs::metadata(&full).map_err(|e| CamelError::Io(format!("{}: {e}", full.display())))?;
if camel_dsl::discovery::is_test_document(&full) {
return Err(CamelError::Config(format!(
"{}: test documents (*.test.yaml, *.test.yml) belong to \
`camel test`, not scenario routeFiles",
full.display()
)));
}
Ok(full.display().to_string())
}
match &doc.route_source {
RouteSource::RouteFiles(files) => {
files.iter().map(|file| anchored(doc_dir, file)).collect()
}
RouteSource::RouteFilesFromRoot(files) => {
files.iter().map(|file| anchored(root, file)).collect()
}
RouteSource::Inline(_) => Err(CamelError::Config(
"inline route sources cannot boot in v1: declare routeFiles \
(the load-time doc gate now fires first; this rejection is \
defense-in-depth for directly-constructed documents)"
.to_string(),
)),
}
}
fn map_discovery_error(err: camel_dsl::DiscoveryError) -> CamelError {
match err {
camel_dsl::DiscoveryError::Env { path, var_name } => CamelError::Config(format!(
"{path}: unresolved ${{env:{var_name}}} placeholder \
(no layer of the scenario environment defines it)"
)),
camel_dsl::DiscoveryError::Io { path, source } => {
CamelError::Io(format!("{path}: {source}"))
}
camel_dsl::DiscoveryError::Yaml { path, error } => {
CamelError::RouteError(format!("{path}: {error}"))
}
camel_dsl::DiscoveryError::Json { path, error } => {
CamelError::RouteError(format!("{path}: {error}"))
}
other => CamelError::RouteError(other.to_string()),
}
}