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 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 use glob::glob_paths;
pub(crate) use expand::expand_foreach;
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;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ManifestLoadStage {
ManifestIngestion,
InitialYamlParsing,
TemplateExpansion,
FinalRendering,
}
fn notify_stage(
on_stage: &mut Option<&mut dyn FnMut(ManifestLoadStage)>,
stage: ManifestLoadStage,
) {
if let Some(cb) = on_stage.as_mut() {
cb(stage);
}
}
struct ManifestParse<'a> {
name: &'a ManifestName,
stdlib_registration: Option<StdlibRegistration>,
env_reader: &'a EnvReader,
}
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,
} = parse;
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))
});
jinja.add_function("glob", |pattern: String| {
let expansion = glob::expand_glob(&pattern)?;
glob::record_expansion(&expansion);
Ok(expansion.into_paths())
});
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)?;
expand_foreach(&mut doc, &jinja)?;
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),
})?;
render_manifest(manifest, &jinja)
}
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,
},
&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;
mod workspace;
#[cfg(test)]
mod tests;