use crate::{
ast::{EMPTY_COMMAND_LIST_ERROR, NetsukeManifest},
localization::{self, keys},
stdlib::{NetworkPolicy, StdlibConfig},
};
use anyhow::Result;
use minijinja::{Environment, UndefinedBehavior, value::Value};
use serde::de::Error as _;
use std::{path::Path, sync::Arc};
mod diagnostics;
mod expand;
#[deny(unreachable_pub)]
mod glob;
mod hints;
mod jinja_macros;
mod load_stage;
mod loading;
mod parse_with_config;
mod query;
mod render;
pub type ManifestValue = serde_json::Value;
pub type ManifestMap = serde_json::Map<String, ManifestValue>;
pub use diagnostics::{
ManifestError, ManifestName, ManifestSource, map_data_error, map_yaml_error,
};
pub use env_reader::{EnvReadError, EnvReader, process_env_reader};
pub(crate) use expand::expand_foreach;
pub use glob::glob_paths;
pub use load_stage::ManifestLoadStage;
use loading::{notify_stage, trace_expansion_report};
pub use parse_with_config::from_str_with_env_and_config;
pub(crate) use query::from_path_for_manifest_query;
pub use render::render_manifest;
use self::{env_reader::env_var_with, jinja_macros::register_manifest_macros};
#[cfg(test)]
use workspace::open_manifest_workspace;
type ExpansionReportObserver = fn(&expand::ExpansionReport);
struct ManifestParse<'a> {
name: &'a ManifestName,
stdlib_registration: Option<StdlibRegistration>,
env_reader: &'a EnvReader,
manifest_root: Option<camino::Utf8PathBuf>,
expansion_report_observer: Option<ExpansionReportObserver>,
}
enum StdlibRegistration {
Full(Box<StdlibConfig>),
ManifestQuery,
}
fn from_str_named(
yaml: &str,
parse: ManifestParse<'_>,
on_stage: &mut Option<&mut dyn FnMut(ManifestLoadStage)>,
) -> Result<NetsukeManifest> {
let ManifestParse {
name,
stdlib_registration,
env_reader,
manifest_root,
expansion_report_observer,
} = parse;
let is_manifest_query = matches!(stdlib_registration, Some(StdlibRegistration::ManifestQuery));
notify_stage(on_stage, ManifestLoadStage::InitialYamlParsing);
let mut doc: ManifestValue =
serde_saphyr::from_str(yaml).map_err(|e| ManifestError::Parse {
source: map_yaml_error(e, &ManifestSource::from(yaml), name),
message: localization::message(keys::MANIFEST_PARSE),
})?;
let mut jinja = Environment::new();
jinja.set_undefined_behavior(UndefinedBehavior::Strict);
let reader = Arc::clone(env_reader);
jinja.add_function("env", move |var_name: String| {
env_var_with(&var_name, |key| reader(key))
});
let glob_base = glob::GlobBaseCache::new(manifest_root);
jinja.add_function("glob", move |pattern: String| {
let expansion = glob::expand_manifest_template_glob(&pattern, &glob_base)?;
expansion.into_template_paths(&pattern)
});
let _stdlib_state = match stdlib_registration {
Some(StdlibRegistration::Full(config)) => {
crate::stdlib::register_with_config(&mut jinja, *config)
}
Some(StdlibRegistration::ManifestQuery) => {
Ok(crate::stdlib::register_manifest_query(&mut jinja))
}
None => crate::stdlib::register(&mut jinja),
}?;
register_manifest_vars(&doc, &mut jinja, name)?;
notify_stage(on_stage, ManifestLoadStage::TemplateExpansion);
register_manifest_macros(&doc, &mut jinja)?;
let expansion_report = expand_foreach(&mut doc, &jinja)?;
if let Some(observe_expansion_report) = expansion_report_observer {
observe_expansion_report(&expansion_report);
}
notify_stage(on_stage, ManifestLoadStage::FinalRendering);
let manifest: NetsukeManifest =
serde_json::from_value(doc).map_err(|error| ManifestError::Parse {
source: map_data_error(localize_recipe_error(error), name),
message: localization::message(keys::MANIFEST_PARSE),
})?;
let rendered_manifest = if is_manifest_query {
render::render_manifest_for_manifest_query(manifest, &jinja)?
} else {
render_manifest(manifest, &jinja)?
};
rendered_manifest
.validate_recipes()
.map_err(|detail| ManifestError::Parse {
source: map_data_error(serde_json::Error::custom(detail), name),
message: localization::message(keys::MANIFEST_PARSE),
})?;
Ok(rendered_manifest)
}
fn localize_recipe_error(error: serde_json::Error) -> serde_json::Error {
if error.to_string().starts_with(EMPTY_COMMAND_LIST_ERROR) {
serde_json::Error::custom(
localization::message(keys::MANIFEST_COMMAND_LIST_EMPTY).to_string(),
)
} else {
error
}
}
const RESERVED_VAR_NAMES: [&str; 2] = ["env", "glob"];
fn manifest_structure_error(
detail: &localization::LocalizedMessage,
name: &ManifestName,
) -> ManifestError {
ManifestError::Parse {
source: map_data_error(serde_json::Error::custom(detail.to_string()), name),
message: localization::message(keys::MANIFEST_PARSE),
}
}
fn register_manifest_vars(
doc: &ManifestValue,
jinja: &mut Environment<'_>,
name: &ManifestName,
) -> Result<(), ManifestError> {
let Some(vars_value) = doc.get("vars") else {
return Ok(());
};
let vars = vars_value.as_object().ok_or_else(|| {
manifest_structure_error(&localization::message(keys::MANIFEST_VARS_NOT_OBJECT), name)
})?;
if let Some(reserved) = vars
.keys()
.find(|key| RESERVED_VAR_NAMES.contains(&key.as_str()))
{
return Err(manifest_structure_error(
&localization::message(keys::MANIFEST_VARS_RESERVED_NAME).with_arg("name", reserved),
name,
));
}
for (key, value) in vars {
jinja.add_global(key.clone(), Value::from_serialize(value));
}
Ok(())
}
pub fn from_str(yaml: &str) -> Result<NetsukeManifest> {
from_str_with_env(yaml, &process_env_reader())
}
pub fn from_str_with_env(yaml: &str, env_reader: &EnvReader) -> Result<NetsukeManifest> {
from_str_named(
yaml,
ManifestParse {
name: &ManifestName::new("Netsukefile"),
stdlib_registration: None,
env_reader,
manifest_root: None,
expansion_report_observer: Some(trace_expansion_report),
},
&mut None,
)
}
pub fn from_path(path: impl AsRef<Path>) -> Result<NetsukeManifest> {
from_path_with_policy(path, NetworkPolicy::default(), None)
}
pub fn from_path_with_policy(
path: impl AsRef<Path>,
policy: NetworkPolicy,
on_stage: Option<&mut dyn FnMut(ManifestLoadStage)>,
) -> Result<NetsukeManifest> {
from_path_with_policy_and_env(path, policy, &process_env_reader(), on_stage)
}
pub fn from_path_with_policy_and_env(
path: impl AsRef<Path>,
policy: NetworkPolicy,
env_reader: &EnvReader,
on_stage: Option<&mut dyn FnMut(ManifestLoadStage)>,
) -> Result<NetsukeManifest> {
query::from_path_with_policy_and_env(path, policy, env_reader, on_stage)
}
mod env_reader;
#[cfg(test)]
mod tests;
mod workspace;