mod entry;
mod job;
mod resource;
mod step;
use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::{Deserialize, Serialize};
pub use entry::*;
pub use job::*;
pub use resource::*;
pub use step::*;
use tracing::{debug, info, span, Level};
use crate::{
directory, store::Store, transformer::Transformer, BlobLoader, JsonLoader, Loader,
TemplateLoader, TextLoader, TextWithFrontmatterLoader, Value, YamlLoader,
};
#[derive(Debug, Deserialize, Serialize)]
pub struct Pipeline {
pub name: String,
pub entry: Entry,
pub steps: Vec<Step>,
pub output_extension: Option<String>,
pub output_key: String,
}
impl Pipeline {
pub fn accepts(&self, path: impl AsRef<Path>) -> bool {
self.entry
.match_regex
.is_match(&path.as_ref().to_string_lossy().replace('\\', "/"))
}
#[tracing::instrument(err, skip(self, project_root), fields(pipeline = self.name))]
pub fn prefetch_resources(&self, project_root: impl AsRef<Path>) -> anyhow::Result<Resource> {
info!("start prepare");
let ret = Resource::load_for(self, project_root);
info!("finish prepare");
ret
}
fn execute(
&self,
entry_bytes: Vec<u8>,
resource: &Resource,
entry_directory: Option<String>,
) -> anyhow::Result<Vec<u8>> {
let mut store = Store::new();
debug!("start loading entry with {:?} Loader", self.entry.type_);
let value = match self.entry.type_ {
EnumLoader::TextWithFrontmatter => TextWithFrontmatterLoader::load(&entry_bytes[..]),
EnumLoader::Json => JsonLoader::load(&entry_bytes[..]),
EnumLoader::Blob => BlobLoader::load(&entry_bytes[..]),
EnumLoader::Template => TemplateLoader::load(&entry_bytes[..]),
EnumLoader::Text => TextLoader::load(&entry_bytes[..]),
EnumLoader::Yaml => YamlLoader::load(&entry_bytes[..]),
}
.with_context(|| format!("failed to load entry with {:?} Loader", self.entry.type_))?;
store.set("entry".to_string(), value);
if let Some(entry_directory) = entry_directory {
store.set(
"___entry_directory".to_string(),
Value::JSON(serde_json::Value::String(entry_directory)),
);
}
debug!("finish loading entry");
for (index, step) in self.steps.iter().enumerate() {
let step_span = span!(
Level::INFO,
"step",
index = index + 1,
max = self.steps.len()
);
let _enter = step_span.enter();
debug!("start");
match step {
Step::Load { key, .. } => {
if let Some(value) = resource.get_value(&index) {
store.set(key.to_string(), value.clone());
} else {
anyhow::bail!("no value prefetched for key {}", key);
}
}
Step::Transform {
input,
output,
with,
} => {
if let Some(input) = store.get_combined(input) {
debug!("transform input type: {}", input.get_type_name());
let value = match with {
EnumTransformer::TemplateRenderer(t) => t.transform(&input, &store),
EnumTransformer::JsonPath(t) => t.transform(&input, &store),
EnumTransformer::JsonPathAll(t) => t.transform(&input, &store),
EnumTransformer::Reloader(t) => t.transform(&input, &store),
}
.with_context(|| "transformer failed".to_string())?;
debug!("transform output type: {}", value.get_type_name());
store.set(output.to_string(), value);
} else {
anyhow::bail!("no value found in the store for input key {:?}", input,)
}
}
Step::DumpStore => {
println!("{store}");
}
}
debug!("finish");
}
let bytes = match store.get_owned(&self.output_key) {
Some(output_value) => match output_value {
Value::JSON(serde_json::Value::String(string)) => string.into_bytes(),
Value::Bytes(bytes) => bytes,
value => {
anyhow::bail!(
"output value type should be string or bytes, but it was {} (output_key={})",
value.get_type_name(),
self.output_key
)
}
},
None => {
anyhow::bail!("no output value found output_key={}", self.output_key);
}
};
Ok(bytes)
}
fn get_output_path(
&self,
input_path: impl AsRef<Path>,
project_root: impl AsRef<Path>,
) -> PathBuf {
let src_dir = directory::get_src_directory(&project_root);
let output_dir = directory::get_output_directory(&project_root);
let mut output_filepath =
output_dir.join(directory::get_relative_path(&input_path, src_dir));
if let Some(output_extension) = &self.output_extension {
output_filepath.set_extension(output_extension);
}
output_filepath
}
pub fn to_job(&self, input_path: impl AsRef<Path>, project_root: impl AsRef<Path>) -> Job<'_> {
Job {
input_path: input_path.as_ref().to_path_buf(),
output_path: self.get_output_path(&input_path, &project_root),
pipeline: self,
}
}
}