use liquid::ObjectView;
use liquid::ValueView;
use serde::Deserialize;
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum ReplacementObject {
File { file: String },
Liquid { liquid: String },
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum ReplacementValue {
String(String),
Object(ReplacementObject),
}
#[derive(Deserialize, Debug)]
pub struct ConfigurableString {
pub base: ReplacementValue,
pub arguments: Option<HashMap<String, ReplacementValue>>,
}
pub type LoadedFiles = HashMap<String, String>;
struct DynamicArguments<'a> {
cache: RefCell<HashMap<String, liquid::model::Value>>,
errors: RefCell<Vec<String>>,
files: &'a LoadedFiles,
parser: liquid::Parser,
arguments: &'a HashMap<String, ReplacementValue>,
}
impl<'a> DynamicArguments<'a> {
fn new(arguments: &'a HashMap<String, ReplacementValue>, files: &'a LoadedFiles) -> Self {
Self {
cache: RefCell::new(HashMap::new()),
errors: RefCell::new(Vec::new()),
files,
parser: liquid::ParserBuilder::with_stdlib().build().unwrap(),
arguments,
}
}
fn resolve_value(&self, key: &str) -> Option<String> {
let replacement = self.arguments.get(key);
match replacement {
Some(r) => match r {
ReplacementValue::String(s) => Some(s.into()),
ReplacementValue::Object(replacement_object) => match replacement_object {
ReplacementObject::File { file } => match self.files.get(file) {
Some(contents) => {
if file.ends_with(".liquid") {
return self.render_liquid(contents);
}
Some(contents.into())
}
None => {
self.errors
.borrow_mut()
.push(format!("File '{}' not found for key '{}'.", file, key));
None
}
},
ReplacementObject::Liquid { liquid } => self.render_liquid(liquid),
},
},
None => None,
}
}
fn render_liquid(&self, liquid: &str) -> Option<String> {
match self.parser.parse(liquid) {
Ok(template) => match template.render(self) {
Ok(result) => Some(result),
Err(e) => {
let error_msg = format!("Liquid render error: {}", e);
self.errors.borrow_mut().push(error_msg);
None
}
},
Err(e) => {
let error_msg = format!("Liquid parse error: {}", e);
self.errors.borrow_mut().push(error_msg);
None
}
}
}
fn has_errors(&self) -> bool {
!self.errors.borrow().is_empty()
}
fn get_errors(&self) -> Vec<String> {
self.errors.borrow().clone()
}
fn ensure_cached(&self, key: &str) {
{
if self.cache.borrow().contains_key(key) {
return;
}
}
if let Some(value) = self.resolve_value(key) {
self.cache
.borrow_mut()
.insert(key.to_string(), liquid::model::Value::scalar(value));
}
}
}
impl<'a> ObjectView for DynamicArguments<'a> {
fn as_value(&self) -> &dyn ValueView {
self
}
fn size(&self) -> i64 {
self.arguments.len() as i64
}
fn keys<'k>(&'k self) -> Box<dyn Iterator<Item = liquid::model::KStringCow<'k>> + 'k> {
Box::new(
self.arguments
.keys()
.map(|k| liquid::model::KStringCow::from_ref(k.as_str())),
)
}
fn values<'k>(&'k self) -> Box<dyn Iterator<Item = &'k dyn ValueView> + 'k> {
Box::new(std::iter::empty())
}
fn iter<'k>(
&'k self,
) -> Box<dyn Iterator<Item = (liquid::model::KStringCow<'k>, &'k dyn ValueView)> + 'k> {
Box::new(std::iter::empty())
}
fn contains_key(&self, index: &str) -> bool {
self.arguments.contains_key(index)
}
fn get<'s>(&'s self, index: &str) -> Option<&'s dyn ValueView> {
self.ensure_cached(index);
unsafe {
let cache_ptr = self.cache.as_ptr();
(*cache_ptr).get(index).map(|v| v as &dyn ValueView)
}
}
}
impl<'a> ValueView for DynamicArguments<'a> {
fn as_debug(&self) -> &dyn std::fmt::Debug {
self
}
fn render(&self) -> liquid::model::DisplayCow<'_> {
liquid::model::DisplayCow::Owned(Box::new("DynamicArguments".to_string()))
}
fn source(&self) -> liquid::model::DisplayCow<'_> {
self.render()
}
fn type_name(&self) -> &'static str {
"object"
}
fn query_state(&self, state: liquid::model::State) -> bool {
match state {
liquid::model::State::Truthy => true,
liquid::model::State::DefaultValue => false,
liquid::model::State::Empty => self.arguments.is_empty(),
liquid::model::State::Blank => false,
}
}
fn to_kstr(&self) -> liquid::model::KStringCow<'_> {
liquid::model::KStringCow::from_ref("DynamicArguments")
}
fn to_value(&self) -> liquid::model::Value {
let cache = self.cache.borrow();
let mut obj = liquid::object!({});
for (k, v) in cache.iter() {
obj.insert(k.clone().into(), v.clone());
}
liquid::model::Value::Object(obj)
}
fn as_object(&self) -> Option<&dyn ObjectView> {
Some(self)
}
}
impl<'a> std::fmt::Debug for DynamicArguments<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DynamicArguments")
.field("arguments", &self.arguments.len())
.field("cache", &self.cache.borrow().len())
.finish()
}
}
impl ConfigurableString {
fn collect_file_references(&self, value: &ReplacementValue, files: &mut Vec<String>) {
match value {
ReplacementValue::String(_) => {}
ReplacementValue::Object(ReplacementObject::File { file }) => {
files.push(file.clone());
}
ReplacementValue::Object(ReplacementObject::Liquid { .. }) => {}
}
}
fn process_replacement_value(
&self,
value: &ReplacementValue,
files: &LoadedFiles,
) -> Result<String, String> {
match value {
ReplacementValue::String(s) => Ok(s.into()),
ReplacementValue::Object(obj) => self.process_replacement_object(obj, files),
}
}
fn process_replacement_object(
&self,
obj: &ReplacementObject,
files: &LoadedFiles,
) -> Result<String, String> {
match obj {
ReplacementObject::File { file } => {
match files.get(file) {
Some(contents) => {
if file.ends_with(".liquid") {
self.render_liquid_template(contents, files)
} else {
Ok(contents.clone())
}
}
None => Err(format!("File '{}' not found.", file)),
}
}
ReplacementObject::Liquid { liquid } => self.render_liquid_template(liquid, files),
}
}
fn render_liquid_template(
&self,
template_str: &str,
files: &LoadedFiles,
) -> Result<String, String> {
let parser = liquid::ParserBuilder::with_stdlib()
.build()
.map_err(|e| format!("Failed to build liquid parser: {}", e))?;
let template = parser
.parse(template_str)
.map_err(|e| format!("Failed to parse template: {}", e))?;
let empty_context;
let context = match &self.arguments {
Some(r) => r,
None => {
empty_context = HashMap::new();
&empty_context
}
};
let dynamic_arguments = DynamicArguments::new(context, files);
let result = template
.render(&dynamic_arguments)
.map_err(|e| format!("Failed to render template: {}", e))?;
if dynamic_arguments.has_errors() {
let errors = dynamic_arguments.get_errors();
Err(format!(
"Errors during template processing:\n{}",
errors.join("\n")
))
} else {
Ok(result)
}
}
pub fn build(&self, files: &LoadedFiles) -> Result<String, String> {
self.process_replacement_value(&self.base, files)
}
pub fn get_referenced_files(&self) -> Vec<String> {
let mut result = Vec::new();
self.collect_file_references(&self.base, &mut result);
if let Some(args) = &self.arguments {
for value in args.values() {
self.collect_file_references(value, &mut result);
}
}
result
}
}