use crate::{
ast::NetsukeManifest,
localization::{self, keys},
stdlib::{NetworkPolicy, StdlibConfig},
};
use anyhow::{Context, Result};
use minijinja::{Environment, UndefinedBehavior, value::Value};
use serde::de::Error as _;
use std::{path::Path, sync::Arc};
mod diagnostics;
mod expand;
mod glob;
mod hints;
mod jinja_macros;
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 render::render_manifest;
use self::{env_reader::env_var_with, jinja_macros::register_manifest_macros};
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_config: Option<StdlibConfig>,
env_reader: &'a EnvReader,
}
fn from_str_named(
yaml: &str,
parse: ManifestParse<'_>,
on_stage: &mut Option<&mut dyn FnMut(ManifestLoadStage)>,
) -> Result<NetsukeManifest> {
let ManifestParse {
name,
stdlib_config,
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| glob_paths(&pattern));
let _stdlib_state = match stdlib_config {
Some(config) => crate::stdlib::register_with_config(&mut jinja, config),
None => crate::stdlib::register(&mut jinja),
}?;
if let Some(vars_value) = doc.get("vars") {
let vars = vars_value
.as_object()
.cloned()
.ok_or_else(|| ManifestError::Parse {
source: map_data_error(
serde_json::Error::custom(
localization::message(keys::MANIFEST_VARS_NOT_OBJECT).to_string(),
),
name,
),
message: localization::message(keys::MANIFEST_PARSE),
})?;
for (key, value) in vars {
jinja.add_global(key, Value::from_serialize(value));
}
}
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(|e| ManifestError::Parse {
source: map_data_error(e, name),
message: localization::message(keys::MANIFEST_PARSE),
})?;
render_manifest(manifest, &jinja)
}
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_config: 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,
mut on_stage: Option<&mut dyn FnMut(ManifestLoadStage)>,
) -> Result<NetsukeManifest> {
notify_stage(&mut on_stage, ManifestLoadStage::ManifestIngestion);
let path_ref = path.as_ref();
let workspace = open_manifest_workspace(path_ref)?;
let data = workspace
.dir
.read_to_string(&workspace.manifest_file)
.with_context(|| {
localization::message(keys::MANIFEST_READ_FAILED)
.with_arg("path", path_ref.display().to_string())
})?;
let name = ManifestName::new(path_ref.display().to_string());
let config = StdlibConfig::new(workspace.dir)?
.with_workspace_root_path(workspace.root)?
.with_network_policy(policy);
from_str_named(
&data,
ManifestParse {
name: &name,
stdlib_config: Some(config),
env_reader,
},
&mut on_stage,
)
}
mod env_reader;
mod workspace;
#[cfg(test)]
mod tests;