mod instantiate;
pub mod parameters;
mod ranges;
use indexmap::IndexMap;
use openjd_expr::path_mapping::PathFormat;
use crate::error::ModelError;
use crate::job;
use crate::template::validate_v2023_09::EffectiveLimits;
use crate::template::JobTemplate;
use crate::types::{JobParameterValues, ValidationContext};
pub use instantiate::{
convert_environment, convert_environment_with_symtab, evaluate_let_bindings,
};
pub use parameters::{
build_symbol_table, merge_job_parameter_definitions, preprocess_job_parameters,
MergedParameterDefinition, PathParameterOptions,
};
pub fn create_job(
job_template: &JobTemplate,
job_parameter_values: &JobParameterValues,
ctx: &ValidationContext,
) -> Result<job::Job, ModelError> {
let merged = parameters::merge_job_parameter_definitions(job_template, &[])?;
for param in &merged {
if let Some(jpv) = job_parameter_values.get(¶m.name) {
param.check_constraints(&jpv.value)?;
}
}
let mut symtab = build_symbol_table(job_parameter_values)?;
let has_expr = ctx
.profile
.has_extension(crate::types::ModelExtension::Expr);
let limits = EffectiveLimits::from_context(ctx);
let job_name = job_template
.name
.resolve_string_with(
&symtab,
&openjd_expr::FormatStringOptions::new().with_path_format(PathFormat::Posix),
)
.map_err(|e| ModelError::FormatStringError {
message: format!("Failed to resolve job name: {e}"),
input: Some(job_template.name.raw().to_string()),
start: None,
end: None,
})?;
if job_name.len() > limits.max_job_name_len {
return Err(ModelError::DecodeValidation(format!(
"Job name exceeds maximum length of {} characters (got {})",
limits.max_job_name_len,
job_name.len()
)));
}
if has_expr {
symtab.set("Job.Name", openjd_expr::ExprValue::String(job_name.clone()))?;
}
let parameters: IndexMap<String, job::JobParameter> = job_parameter_values
.iter()
.map(|(name, pv)| {
(
name.clone(),
job::JobParameter {
name: name.clone(),
param_type: pv.param_type,
value: pv.value.clone(),
},
)
})
.collect();
let steps = job_template
.steps
.iter()
.map(|st| instantiate::instantiate_step(st, &symtab, has_expr, &limits, ctx))
.collect::<Result<Vec<_>, _>>()?;
if let Some(max_task_count) = ctx.caller_limits.max_task_count {
let mut total: u64 = 0;
for step in &steps {
let step_tasks = step
.parameter_space
.as_ref()
.map(|ps| {
crate::job::step_param_space::StepParameterSpaceIterator::new_with_chunk_override(ps, Some(1))
.map(|iter| iter.len() as u64)
})
.transpose()?
.unwrap_or(1); total = total.saturating_add(step_tasks);
}
if total > max_task_count {
return Err(ModelError::ModelValidation(
crate::error::ValidationErrors::single(format!(
"Total task count ({total}) exceeds caller limit of {max_task_count}."
)),
));
}
}
let job_environments = job_template.job_environments.as_ref().map(|envs| {
envs.iter()
.map(|e| instantiate::convert_environment_with_symtab(e, Some(&symtab)))
.collect()
});
let extensions = job_template.extensions.as_ref().map(|exts| {
exts.iter()
.filter_map(|e| std::str::FromStr::from_str(e.as_str()).ok())
.collect()
});
if let Some(max) = ctx.caller_limits.max_step_script_size {
for step in &steps {
let size = serde_json::to_string(&step.script)
.map(|s| s.len())
.unwrap_or(0);
if size > max {
return Err(ModelError::ModelValidation(
crate::error::ValidationErrors::single(format!(
"Step '{}' script size ({size} bytes) exceeds caller limit of {max} bytes.",
step.name
)),
));
}
}
}
if let Some(max) = ctx.caller_limits.max_environment_size {
let all_envs = steps
.iter()
.flat_map(|s| s.step_environments.iter().flatten())
.chain(job_environments.iter().flatten());
for env in all_envs {
let size = serde_json::to_string(env).map(|s| s.len()).unwrap_or(0);
if size > max {
return Err(ModelError::ModelValidation(
crate::error::ValidationErrors::single(format!(
"Environment '{}' size ({size} bytes) exceeds caller limit of {max} bytes.",
env.name
)),
));
}
}
}
Ok(job::Job {
name: job_name,
description: job_template.description.as_ref().map(|d| d.0.clone()),
extensions,
parameters,
steps,
job_environments,
})
}