slumber_template/
lib.rs

1//! Generate strings (and bytes) from user-written templates with dynamic data.
2//! This engine is focused on rendering templates, and is generally agnostic of
3//! its usage in the rest of the app. As such, there is no logic in here
4//! relating to HTTP or other Slumber concepts.
5
6mod cereal;
7mod display;
8mod error;
9mod expression;
10mod parse;
11#[cfg(test)]
12mod test_util;
13
14pub use error::{
15    Expected, RenderError, TemplateParseError, ValueError, WithValue,
16};
17pub use expression::{Expression, FunctionCall, Identifier, Literal};
18
19use crate::{
20    error::RenderErrorContext,
21    parse::{FALSE, NULL, TRUE},
22};
23use bytes::{Bytes, BytesMut};
24use derive_more::From;
25use futures::future;
26use indexmap::IndexMap;
27use itertools::Itertools;
28#[cfg(test)]
29use proptest::{arbitrary::any, strategy::Strategy};
30use serde::{Deserialize, Serialize};
31use slumber_util::NEW_ISSUE_LINK;
32use std::{collections::VecDeque, fmt::Debug, sync::Arc};
33
34/// `Context` defines how template fields and functions are resolved. Both
35/// field resolution and function calls can be asynchronous.
36pub trait Context: Sized + Send + Sync {
37    /// Get the value of a field from the context. The implementor can decide
38    /// where fields are derived from. Fields can also be computed dynamically
39    /// and be `async`. For example, fields can be loaded from a map of nested
40    /// templates, in which case the nested template would need to be rendered
41    /// before this can be returned.
42    fn get(
43        &self,
44        identifier: &Identifier,
45    ) -> impl Future<Output = Result<Value, RenderError>> + Send;
46
47    /// Call a function by name
48    fn call(
49        &self,
50        function_name: &Identifier,
51        arguments: Arguments<'_, Self>,
52    ) -> impl Future<Output = Result<Value, RenderError>> + Send;
53}
54
55/// A parsed template, which can contain raw and/or templated content. The
56/// string is parsed during creation to identify template keys, hence the
57/// immutability.
58///
59/// The original string is *not* stored. To recover the source string, use the
60/// [Display] implementation.
61///
62/// Invariants:
63/// - Two templates with the same source string will have the same set of
64///   chunks, and vice versa
65/// - No two raw chunks will ever be consecutive
66/// - Raw chunks cannot not be empty
67#[derive(Clone, Debug, Default, PartialEq)]
68#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
69pub struct Template {
70    /// Pre-parsed chunks of the template. For raw chunks we store the
71    /// presentation text (which is not necessarily the source text, as escape
72    /// sequences will be eliminated). For keys, just store the needed
73    /// metadata.
74    #[cfg_attr(
75        test,
76        proptest(
77            strategy = "any::<Vec<TemplateChunk>>().prop_map(test_util::join_raw)"
78        )
79    )]
80    chunks: Vec<TemplateChunk>,
81}
82
83impl Template {
84    /// Compile a template from its composite chunks
85    ///
86    /// ## Panics
87    ///
88    /// Panic if the chunk list is invalid:
89    ///
90    /// - If there are consecutive raw chunks
91    /// - If any raw chunk is empty
92    ///
93    /// These panics are necessary to maintain the invariants documented on the
94    /// struct definition.
95    pub fn from_chunks(chunks: Vec<TemplateChunk>) -> Self {
96        // Since the chunks are constructed externally, we need to enforce our
97        // invariants. This will short-circuit any bugs in chunk construction
98        assert!(
99            // Look for empty raw chunks
100            !chunks.iter().any(
101                |chunk| matches!(chunk, TemplateChunk::Raw(s) if s.is_empty())
102            )
103            // Look for consecutive raw chunks
104            && !chunks.iter().tuple_windows().any(|pair| matches!(
105                pair,
106                (TemplateChunk::Raw(_), TemplateChunk::Raw(_))
107            )),
108            "Invalid chunks in generated template {chunks:?} This is a bug! \
109            Please report it. {NEW_ISSUE_LINK}"
110        );
111        Self { chunks }
112    }
113
114    /// Create a new template from a raw string, without parsing it at all.
115    /// Useful when importing from external formats where the string isn't
116    /// expected to be a valid Slumber template
117    pub fn raw(template: String) -> Template {
118        let chunks = if template.is_empty() {
119            vec![]
120        } else {
121            // This may seem too easy, but the hard part comes during
122            // stringification, when we need to add backslashes to get the
123            // string to parse correctly later
124            vec![TemplateChunk::Raw(template.into())]
125        };
126        Self { chunks }
127    }
128
129    /// Create a template that loads a file
130    ///
131    /// ```
132    /// use slumber_template::Template;
133    ///
134    /// let template = Template::file("path/to/file".into());
135    /// assert_eq!(template.display(), "{{ file('path/to/file') }}");
136    /// ```
137    pub fn file(path: String) -> Template {
138        Self::function_call("file", [path.into()], [])
139    }
140
141    /// Create a new template that contains a single chunk, which is an
142    /// expression that invokes a function with position arguments and optional
143    /// keyword arguments.
144    ///
145    /// ```
146    /// # use slumber_template::Template;
147    /// let template = Template::function_call(
148    ///     "hello",
149    ///     ["john".into()],
150    ///     [("mode", Some("caps".into()))],
151    /// );
152    /// assert_eq!(template.display(), "{{ hello('john', mode='caps') }}");
153    /// ```
154    pub fn function_call(
155        name: &'static str,
156        position: impl IntoIterator<Item = Expression>,
157        keyword: impl IntoIterator<Item = (&'static str, Option<Expression>)>,
158    ) -> Self {
159        let chunks = vec![TemplateChunk::Expression(Expression::call(
160            name, position, keyword,
161        ))];
162        Self { chunks }
163    }
164
165    pub fn is_empty(&self) -> bool {
166        self.chunks.is_empty()
167    }
168
169    /// Render the template using values from the given context. If any chunk
170    /// failed to render, return an error. The render output is converted to a
171    /// [Value] by these rules:
172    /// - If the template is a single dynamic chunk, the output value will be
173    ///   directly converted to JSON, allowing non-string JSON values
174    /// - Any other template will be rendered to a string by stringifying each
175    ///   dynamic chunk and concatenating them all together
176    /// - If rendering to a string fails because the bytes are not valid UTF-8,
177    ///   concatenate into a bytes object instead
178    ///
179    /// Return an error iff any chunk failed to render. This will never fail on
180    /// output conversion because it can always fall back to returning raw
181    /// bytes.
182    pub async fn render_value<Ctx: Context>(
183        &self,
184        context: &Ctx,
185    ) -> Result<Value, RenderError> {
186        let mut chunks = self.render_chunks(context).await;
187
188        // If we have a single dynamic chunk, return its value directly instead
189        // of stringifying
190        if let &[RenderedChunk::Rendered(_)] = chunks.as_slice() {
191            let Some(RenderedChunk::Rendered(value)) = chunks.pop() else {
192                // Checked pattern above
193                unreachable!()
194            };
195            return Ok(value);
196        }
197
198        // Stitch together into bytes. Attempt to convert that UTF-8, but if
199        // that fails fall back to just returning the bytes
200        let bytes = chunks_to_bytes(chunks)?;
201        match String::from_utf8(bytes.into()) {
202            Ok(s) => Ok(Value::String(s)),
203            Err(error) => Ok(Value::Bytes(error.into_bytes().into())),
204        }
205    }
206
207    /// Render the template using values from the given context. If any chunk
208    /// failed to render, return an error. The output is returned as bytes,
209    /// meaning it can safely render to non-UTF-8 content. Use
210    /// [Self::render_string] if you want the bytes converted to a string.
211    pub async fn render_bytes<Ctx: Context>(
212        &self,
213        context: &Ctx,
214    ) -> Result<Bytes, RenderError> {
215        let chunks = self.render_chunks(context).await;
216        chunks_to_bytes(chunks)
217    }
218
219    /// Render the template using values from the given context. If any chunk
220    /// failed to render, return an error. The output will be converted from raw
221    /// bytes to UTF-8. If it is not valid UTF-8, return an error.
222    pub async fn render_string<Ctx: Context>(
223        &self,
224        context: &Ctx,
225    ) -> Result<String, RenderError> {
226        let bytes = self.render_bytes(context).await?;
227        String::from_utf8(bytes.into()).map_err(RenderError::other)
228    }
229
230    /// Render the template using values from the given context, returning the
231    /// individual rendered chunks rather than stitching them together into a
232    /// string. If any individual chunk fails to render, its error will be
233    /// returned inline as [RenderedChunk::Error] and the rest of the template
234    /// will still be rendered.
235    pub async fn render_chunks<Ctx: Context>(
236        &self,
237        context: &Ctx,
238    ) -> Vec<RenderedChunk> {
239        // Map over each parsed chunk, and render the expressions into values.
240        // because raw text uses Arc and expressions just contain metadata
241        // The raw text chunks will be mapped 1:1. This clone is pretty cheap
242        let futures = self.chunks.iter().map(|chunk| async move {
243            match chunk {
244                TemplateChunk::Raw(text) => {
245                    RenderedChunk::Raw(Arc::clone(text))
246                }
247                TemplateChunk::Expression(expression) => expression
248                    .render(context)
249                    .await
250                    .map_or_else(RenderedChunk::Error, RenderedChunk::Rendered),
251            }
252        });
253
254        // Concurrency!
255        future::join_all(futures).await
256    }
257}
258
259#[cfg(any(test, feature = "test"))]
260impl From<&str> for Template {
261    fn from(value: &str) -> Self {
262        value.parse().unwrap()
263    }
264}
265
266#[cfg(any(test, feature = "test"))]
267impl From<String> for Template {
268    fn from(value: String) -> Self {
269        value.as_str().into()
270    }
271}
272
273#[cfg(any(test, feature = "test"))]
274impl<const N: usize> From<[TemplateChunk; N]> for Template {
275    fn from(chunks: [TemplateChunk; N]) -> Self {
276        Self {
277            chunks: chunks.into(),
278        }
279    }
280}
281
282/// A parsed piece of a template. After parsing, each chunk is either raw text
283/// or a parsed key, ready to be rendered.
284#[derive(Clone, Debug, PartialEq)]
285#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
286pub enum TemplateChunk {
287    /// Raw unprocessed text, i.e. something **outside** the `{{ }}`. This is
288    /// stored in an `Arc` so we can share cheaply in each render without
289    /// having to clone text. This works because templates are immutable. Any
290    /// non-empty string is a valid raw chunk. This text represents what the
291    /// user wants to see, i.e. it does *not* including any escape chars.
292    Raw(
293        #[cfg_attr(test, proptest(strategy = "\".+\".prop_map(String::into)"))]
294        Arc<str>,
295    ),
296    /// Dynamic expression to be computed at render time
297    Expression(
298        #[cfg_attr(
299            test,
300            proptest(strategy = "test_util::expression_arbitrary()")
301        )]
302        Expression,
303    ),
304}
305
306#[cfg(test)]
307impl From<Expression> for TemplateChunk {
308    fn from(expression: Expression) -> Self {
309        Self::Expression(expression)
310    }
311}
312
313/// A runtime template value. This very similar to a JSON value, except:
314/// - Numbers do not support arbitrary size
315/// - Bytes are supported
316#[derive(Clone, Debug, From, PartialEq, Serialize, Deserialize)]
317#[serde(untagged)]
318pub enum Value {
319    Null,
320    Boolean(bool),
321    Integer(i64),
322    Float(f64),
323    String(String),
324    #[from(skip)] // We use a generic impl instead
325    Array(Vec<Self>),
326    Object(IndexMap<String, Self>),
327    // Put this at the end so int arrays deserialize as Array instead of Bytes
328    Bytes(Bytes),
329}
330
331impl Value {
332    /// Convert this value to a boolean, according to its truthiness.
333    /// Truthiness/falsiness is defined for each type as:
334    /// - `null` - `false`
335    /// - `bool` - Own value
336    /// - `integer` - `false` if zero
337    /// - `float` - `false` if zero
338    /// - `string` - `false` if empty
339    /// - `bytes` - `false` if empty
340    /// - `array` - `false` if empty
341    /// - `object` - `false` if empty
342    ///
343    /// These correspond to the truthiness rules from Python.
344    pub fn to_bool(&self) -> bool {
345        match self {
346            Self::Null => false,
347            Self::Boolean(b) => *b,
348            Self::Integer(i) => *i != 0,
349            Self::Float(f) => *f != 0.0,
350            Self::String(s) => !s.is_empty(),
351            Self::Bytes(bytes) => !bytes.is_empty(),
352            Self::Array(array) => !array.is_empty(),
353            Self::Object(object) => !object.is_empty(),
354        }
355    }
356
357    /// Attempt to convert this value to a string. This can fail only if the
358    /// value contains non-UTF-8 bytes, or if it is a collection that contains
359    /// non-UTF-8 bytes.
360    pub fn try_into_string(self) -> Result<String, WithValue<ValueError>> {
361        match self {
362            Self::Null => Ok(NULL.into()),
363            Self::Boolean(false) => Ok(FALSE.into()),
364            Self::Boolean(true) => Ok(TRUE.into()),
365            Self::Integer(i) => Ok(i.to_string()),
366            Self::Float(f) => Ok(f.to_string()),
367            Self::String(s) => Ok(s),
368            Self::Bytes(bytes) => String::from_utf8(bytes.into())
369                // We moved the value to convert it, so we have to reconstruct
370                // it for the error
371                .map_err(|error| {
372                    WithValue::new(
373                        Self::Bytes(error.as_bytes().to_owned().into()),
374                        error.utf8_error(),
375                    )
376                }),
377            // Use the display impl
378            Self::Array(_) | Self::Object(_) => Ok(self.to_string()),
379        }
380    }
381
382    /// Convert this value to a byte string. Bytes values are returned as is.
383    /// Anything else is converted to a string first, then encoded as UTF-8.
384    pub fn into_bytes(self) -> Bytes {
385        match self {
386            Self::Null => NULL.into(),
387            Self::Boolean(false) => FALSE.into(),
388            Self::Boolean(true) => TRUE.into(),
389            Self::Integer(i) => i.to_string().into(),
390            Self::Float(f) => f.to_string().into(),
391            Self::String(s) => s.into(),
392            Self::Bytes(bytes) => bytes,
393            // Use the display impl
394            Self::Array(_) | Self::Object(_) => self.to_string().into(),
395        }
396    }
397
398    /// Convert a JSON value to a template value. This is infallible because
399    /// [Value] is a superset of JSON
400    pub fn from_json(json: serde_json::Value) -> Self {
401        serde_json::from_value(json).unwrap()
402    }
403}
404
405impl From<&Literal> for Value {
406    fn from(literal: &Literal) -> Self {
407        match literal {
408            Literal::Null => Value::Null,
409            Literal::Boolean(b) => Value::Boolean(*b),
410            Literal::Integer(i) => Value::Integer(*i),
411            Literal::Float(f) => Value::Float(*f),
412            Literal::String(s) => Value::String(s.clone()),
413            Literal::Bytes(bytes) => Value::Bytes(bytes.clone()),
414        }
415    }
416}
417
418impl From<&str> for Value {
419    fn from(value: &str) -> Self {
420        Self::String(value.into())
421    }
422}
423
424impl<T> From<Vec<T>> for Value
425where
426    Value: From<T>,
427{
428    fn from(value: Vec<T>) -> Self {
429        Self::Array(value.into_iter().map(Self::from).collect())
430    }
431}
432
433impl<K, V> From<Vec<(K, V)>> for Value
434where
435    String: From<K>,
436    Value: From<V>,
437{
438    fn from(value: Vec<(K, V)>) -> Self {
439        Self::Object(
440            value
441                .into_iter()
442                .map(|(key, value)| (key.into(), value.into()))
443                .collect(),
444        )
445    }
446}
447
448/// A piece of a rendered template string. A collection of chunks collectively
449/// constitutes a rendered string when displayed contiguously.
450#[derive(Debug)]
451pub enum RenderedChunk {
452    /// Raw unprocessed text, i.e. something **outside** the `{{ }}`. This is
453    /// stored in an `Arc` so we can reference the text in the parsed input
454    /// without having to clone it.
455    Raw(Arc<str>),
456    /// Outcome of rendering a template key
457    Rendered(Value),
458    /// An error occurred while rendering a template key
459    Error(RenderError),
460}
461
462#[cfg(test)]
463impl PartialEq for RenderedChunk {
464    fn eq(&self, other: &Self) -> bool {
465        match (self, other) {
466            (Self::Raw(raw1), Self::Raw(raw2)) => raw1 == raw2,
467            (Self::Rendered(value1), Self::Rendered(value2)) => {
468                value1 == value2
469            }
470            (Self::Error(error1), Self::Error(error2)) => {
471                // RenderError doesn't have a PartialEq impl, so we have to
472                // do a string comparison.
473                error1.to_string() == error2.to_string()
474            }
475            _ => false,
476        }
477    }
478}
479
480/// Arguments passed to a function call
481///
482/// This container holds all the data a template function may need to construct
483/// its own arguments. All given positional and keyword arguments are expected
484/// to be used, and [assert_consumed](Self::assert_consumed) should be called
485/// after extracting arguments to ensure no additional ones were passed.
486#[derive(Debug)]
487pub struct Arguments<'ctx, Ctx> {
488    /// Arbitrary user-provided context available to every template render and
489    /// function call
490    context: &'ctx Ctx,
491    /// Position arguments. This queue will be drained from the front as
492    /// arguments are converted, and additional arguments not accepted by the
493    /// function will trigger an error.
494    position: VecDeque<Value>,
495    /// Number of arguments that have been popped off so far. Used to provide
496    /// better error messages
497    num_popped: usize,
498    /// Keyword arguments. All keyword arguments are optional. Ordering has no
499    /// impact on semantics, but we use an `IndexMap` so the order in error
500    /// messages will match what the user passed.
501    keyword: IndexMap<String, Value>,
502}
503
504impl<'ctx, Ctx> Arguments<'ctx, Ctx> {
505    pub fn new(
506        context: &'ctx Ctx,
507        position: VecDeque<Value>,
508        keyword: IndexMap<String, Value>,
509    ) -> Self {
510        Self {
511            context,
512            position,
513            num_popped: 0,
514            keyword,
515        }
516    }
517
518    /// Get a reference to the template context
519    pub fn context(&self) -> &'ctx Ctx {
520        self.context
521    }
522
523    /// Pop the next positional argument off the front of the queue and convert
524    /// it to type `T` using its [TryFromValue] implementation. Return an error
525    /// if there are no positional arguments left or the conversion fails.
526    pub fn pop_position<T: TryFromValue>(&mut self) -> Result<T, RenderError> {
527        let value = self
528            .position
529            .pop_front()
530            .ok_or(RenderError::TooFewArguments)?;
531        let arg_index = self.num_popped;
532        self.num_popped += 1;
533        T::try_from_value(value).map_err(|error| {
534            RenderError::Value(error.error).context(
535                RenderErrorContext::ArgumentConvert {
536                    argument: arg_index.to_string(),
537                    value: error.value,
538                },
539            )
540        })
541    }
542
543    /// Remove a keyword argument from the argument set, converting it to type
544    /// `T` using its [TryFromValue] implementation. Return an error if the
545    /// keyword argument does not exist or the conversion fails.
546    pub fn pop_keyword<T: Default + TryFromValue>(
547        &mut self,
548        name: &str,
549    ) -> Result<T, RenderError> {
550        match self.keyword.shift_remove(name) {
551            Some(value) => T::try_from_value(value).map_err(|error| {
552                RenderError::Value(error.error).context(
553                    RenderErrorContext::ArgumentConvert {
554                        argument: name.to_owned(),
555                        value: error.value,
556                    },
557                )
558            }),
559            // Kwarg not provided - use the default value
560            None => Ok(T::default()),
561        }
562    }
563
564    /// Ensure that all positional and keyword arguments have been consumed.
565    /// Return an error if any arguments were passed by the user but not
566    /// consumed by the function implementation.
567    pub fn ensure_consumed(self) -> Result<(), RenderError> {
568        if self.position.is_empty() && self.keyword.is_empty() {
569            Ok(())
570        } else {
571            Err(RenderError::TooManyArguments {
572                position: self.position.into(),
573                keyword: self.keyword,
574            })
575        }
576    }
577}
578
579/// Convert [Value] to a type fallibly
580///
581/// This is used for converting function arguments to the static types expected
582/// by the function implementations.
583pub trait TryFromValue: Sized {
584    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>>;
585}
586
587impl TryFromValue for Value {
588    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
589        Ok(value)
590    }
591}
592
593impl TryFromValue for bool {
594    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
595        Ok(value.to_bool())
596    }
597}
598
599impl TryFromValue for f64 {
600    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
601        match value {
602            Value::Float(f) => Ok(f),
603            _ => Err(WithValue::new(
604                value,
605                ValueError::Type {
606                    expected: Expected::Float,
607                },
608            )),
609        }
610    }
611}
612
613impl TryFromValue for i64 {
614    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
615        match value {
616            Value::Integer(i) => Ok(i),
617            _ => Err(WithValue::new(
618                value,
619                ValueError::Type {
620                    expected: Expected::Integer,
621                },
622            )),
623        }
624    }
625}
626
627impl TryFromValue for String {
628    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
629        // This will succeed for anything other than invalid UTF-8 bytes
630        value.try_into_string()
631    }
632}
633
634impl TryFromValue for Bytes {
635    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
636        Ok(value.into_bytes())
637    }
638}
639
640impl<T> TryFromValue for Option<T>
641where
642    T: TryFromValue,
643{
644    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
645        if let Value::Null = value {
646            Ok(None)
647        } else {
648            T::try_from_value(value).map(Some)
649        }
650    }
651}
652
653/// Convert an array to a list
654impl<T> TryFromValue for Vec<T>
655where
656    T: TryFromValue,
657{
658    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
659        if let Value::Array(array) = value {
660            array.into_iter().map(T::try_from_value).collect()
661        } else {
662            Err(WithValue::new(
663                value,
664                ValueError::Type {
665                    expected: Expected::Array,
666                },
667            ))
668        }
669    }
670}
671
672impl From<serde_json::Value> for Value {
673    fn from(value: serde_json::Value) -> Self {
674        Self::from_json(value)
675    }
676}
677
678/// Convert a template value to JSON. If the value is bytes, this will
679/// deserialize it as JSON, otherwise it will convert directly. This allows us
680/// to parse response bodies as JSON while accepting anything else as a native
681/// JSON value
682impl TryFromValue for serde_json::Value {
683    fn try_from_value(value: Value) -> Result<Self, WithValue<ValueError>> {
684        match value {
685            Value::Null => Ok(serde_json::Value::Null),
686            Value::Boolean(b) => Ok(b.into()),
687            Value::Integer(i) => Ok(i.into()),
688            Value::Float(f) => Ok(f.into()),
689            Value::String(s) => Ok(s.into()),
690            Value::Array(array) => array
691                .into_iter()
692                .map(serde_json::Value::try_from_value)
693                .collect(),
694            Value::Object(map) => map
695                .into_iter()
696                .map(|(k, v)| Ok((k, serde_json::Value::try_from_value(v)?)))
697                .collect(),
698            Value::Bytes(_) => {
699                // Bytes are probably a string. If it's not UTF-8 there's no way
700                // to make JSON from it
701                value.try_into_string().map(serde_json::Value::String)
702            }
703        }
704    }
705}
706
707/// Implement [TryFromValue] for the given type by converting the [Value] to a
708/// [String], then using `T`'s [FromStr] implementation to convert to `T`.
709///
710/// This could be a derive macro, but decl is much simpler
711#[macro_export]
712macro_rules! impl_try_from_value_str {
713    ($type:ty) => {
714        impl TryFromValue for $type {
715            fn try_from_value(
716                value: $crate::Value,
717            ) -> Result<Self, $crate::WithValue<$crate::ValueError>> {
718                let s = String::try_from_value(value)?;
719                s.parse().map_err(|error| {
720                    $crate::WithValue::new(
721                        s.into(),
722                        $crate::ValueError::other(error),
723                    )
724                })
725            }
726        }
727    };
728}
729
730/// Convert any value into `Result<Value, RenderError>`
731///
732/// This is used for converting function outputs back to template values.
733pub trait FunctionOutput {
734    fn into_result(self) -> Result<Value, RenderError>;
735}
736
737impl<T> FunctionOutput for T
738where
739    Value: From<T>,
740{
741    fn into_result(self) -> Result<Value, RenderError> {
742        Ok(self.into())
743    }
744}
745
746impl<T, E> FunctionOutput for Result<T, E>
747where
748    T: Into<Value> + Send + Sync,
749    E: Into<RenderError> + Send + Sync,
750{
751    fn into_result(self) -> Result<Value, RenderError> {
752        self.map(T::into).map_err(E::into)
753    }
754}
755
756impl<T: FunctionOutput> FunctionOutput for Option<T> {
757    fn into_result(self) -> Result<Value, RenderError> {
758        self.map(T::into_result).unwrap_or(Ok(Value::Null))
759    }
760}
761
762/// Concatenate rendered chunks into bytes. If any chunk is an error, return an
763/// error
764fn chunks_to_bytes(chunks: Vec<RenderedChunk>) -> Result<Bytes, RenderError> {
765    // Take an educated guess at the needed capacity to avoid reallocations
766    let capacity = chunks
767        .iter()
768        .map(|chunk| match chunk {
769            RenderedChunk::Raw(s) => s.len(),
770            RenderedChunk::Rendered(Value::Bytes(bytes)) => bytes.len(),
771            RenderedChunk::Rendered(Value::String(s)) => s.len(),
772            // Take a rough guess for anything other than bytes/string
773            RenderedChunk::Rendered(_) => 5,
774            RenderedChunk::Error(_) => 0,
775        })
776        .sum();
777    chunks
778        .into_iter()
779        .try_fold(BytesMut::with_capacity(capacity), |mut acc, chunk| {
780            match chunk {
781                RenderedChunk::Raw(s) => acc.extend(s.as_bytes()),
782                RenderedChunk::Rendered(value) => {
783                    acc.extend(value.into_bytes());
784                }
785                RenderedChunk::Error(error) => return Err(error),
786            }
787            Ok(acc)
788        })
789        .map(Bytes::from)
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795    use indexmap::indexmap;
796    use rstest::rstest;
797    use slumber_util::assert_err;
798
799    /// Test simple expression rendering
800    #[rstest]
801    #[case::object(
802        "{{ {'a': 1, 1: 2, ['a',1]: ['b',2]} }}",
803        vec![
804            ("a", Value::from(1)),
805            ("1", 2.into()),
806            // Note the whitespace in the key: it was parsed and restringified
807            ("['a', 1]", vec![Value::from("b"), 2.into()].into()),
808        ].into(),
809    )]
810    #[case::object_dupe_key(
811        // Latest entry takes precedence
812        "{{ {'Mike': 1, name: 2, 10: 3, '10': 4} }}",
813        vec![("Mike", 2), ("10", 4)].into(),
814    )]
815    #[tokio::test]
816    async fn test_expression(
817        #[case] template: Template,
818        #[case] expected: Value,
819    ) {
820        assert_eq!(
821            template.render_value(&TestContext).await.unwrap(),
822            expected
823        );
824    }
825
826    /// Render to a value. Templates with a single dynamic chunk are allowed to
827    /// produce non-string values. This is specifically testing the behavior
828    /// of [Template::render_value], rather than expression evaluation.
829    #[rstest]
830    #[case::unpack("{{ array }}", vec!["a", "b", "c"].into())]
831    #[case::string("my name is {{ name }}", "my name is Mike".into())]
832    #[case::bytes(
833        "my name is {{ invalid_utf8 }}",
834        Value::Bytes(b"my name is \xc3\x28".as_slice().into(),
835    ))]
836    #[tokio::test]
837    async fn test_render_value(
838        #[case] template: Template,
839        #[case] expected: Value,
840    ) {
841        assert_eq!(
842            template.render_value(&TestContext).await.unwrap(),
843            expected
844        );
845    }
846
847    /// Convert JSON values to template values
848    #[rstest]
849    #[case::null(serde_json::Value::Null, Value::Null)]
850    #[case::bool_true(serde_json::Value::Bool(true), Value::Boolean(true))]
851    #[case::bool_false(serde_json::Value::Bool(false), Value::Boolean(false))]
852    #[case::number_positive_int(serde_json::json!(42), Value::Integer(42))]
853    #[case::number_negative_int(serde_json::json!(-17), Value::Integer(-17))]
854    #[case::number_zero(serde_json::json!(0), Value::Integer(0))]
855    #[case::number_float(serde_json::json!(1.23), Value::Float(1.23))]
856    #[case::number_negative_float(serde_json::json!(-2.5), Value::Float(-2.5))]
857    #[case::number_zero_float(serde_json::json!(0.0), Value::Float(0.0))]
858    #[case::string_empty(serde_json::json!(""), "".into())]
859    #[case::string_simple(serde_json::json!("hello"), "hello".into())]
860    #[case::string_with_spaces(serde_json::json!("hello world"), "hello world".into())]
861    #[case::string_with_unicode(serde_json::json!("héllo 🌍"), "héllo 🌍".into())]
862    #[case::string_with_escapes(serde_json::json!("line1\nline2\ttab"), "line1\nline2\ttab".into())]
863    #[case::array(
864        serde_json::json!([null, true, 42, "hello"]),
865        Value::Array(vec![
866            Value::Null,
867            Value::Boolean(true),
868            Value::Integer(42),
869            "hello".into(),
870        ])
871    )]
872    // Array of numbers should *not* be interpreted as bytes
873    #[case::array_numbers(serde_json::json!([1, 2, 3]), vec![1, 2, 3].into())]
874    #[case::array_nested(
875        serde_json::json!([[1, 2], [3, 4]]),
876        vec![Value::from(vec![1, 2]), Value::from(vec![3, 4])].into()
877    )]
878    #[case::object(
879        serde_json::json!({"name": "John", "age": 30, "active": true}),
880        Value::Object(indexmap! {
881            "name".into() => "John".into(),
882            "age".into() => Value::Integer(30),
883            "active".into() => Value::Boolean(true),
884        })
885    )]
886    #[case::object_nested(
887        serde_json::json!({"user": {"name": "Alice", "scores": [95, 87]}}),
888        Value::Object(indexmap! {
889            "user".into() => Value::Object(indexmap! {
890                "name".into() => "Alice".into(),
891                "scores".into() =>
892                    Value::Array(vec![Value::Integer(95), Value::Integer(87)]),
893            })
894        })
895    )]
896    fn test_from_json(
897        #[case] json: serde_json::Value,
898        #[case] expected: Value,
899    ) {
900        let actual = Value::from_json(json);
901        assert_eq!(actual, expected);
902    }
903
904    #[rstest]
905    #[case::one_arg("{{ 1 | identity() }}", "1")]
906    #[case::multiple_args("{{ 'cd' | concat('ab') }}", "abcd")]
907    // Piped value is the last positional arg, before kwargs
908    #[case::kwargs("{{ 'cd' | concat('ab', reverse=true) }}", "dcba")]
909    #[tokio::test]
910    async fn test_pipe(#[case] template: Template, #[case] expected: &str) {
911        assert_eq!(
912            template.render_string(&TestContext).await.unwrap(),
913            expected
914        );
915    }
916
917    /// Test error context on a variety of error cases in function calls
918    #[rstest]
919    #[case::unknown_function("{{ fake() }}", "fake(): Unknown function")]
920    #[case::extra_arg(
921        "{{ identity('a', 'b') }}",
922        "identity(): Extra arguments 'b'"
923    )]
924    #[case::missing_arg("{{ add(1) }}", "add(): Not enough arguments")]
925    #[case::arg_render(
926        // Argument fails to render
927        "{{ add(f(), 2) }}",
928        "add(): argument 0=f(): f(): Unknown function"
929    )]
930    #[case::arg_convert(
931        // Argument renders but doesn't convert to what the func wants
932        "{{ add(1, 'b') }}",
933        "add(): argument 1='b': Expected integer"
934    )]
935    #[tokio::test]
936    async fn test_function_error(
937        #[case] template: Template,
938        #[case] expected_error: &str,
939    ) {
940        assert_err!(
941            // Use anyhow to get the error message to include the whole chain
942            template
943                .render_string(&TestContext)
944                .await
945                .map_err(anyhow::Error::from),
946            expected_error
947        );
948    }
949
950    struct TestContext;
951
952    impl Context for TestContext {
953        async fn get(
954            &self,
955            identifier: &Identifier,
956        ) -> Result<Value, RenderError> {
957            match identifier.as_str() {
958                "name" => Ok("Mike".into()),
959                "array" => Ok(vec!["a", "b", "c"].into()),
960                "invalid_utf8" => {
961                    Ok(Value::Bytes(b"\xc3\x28".as_slice().into()))
962                }
963                _ => Err(RenderError::FieldUnknown {
964                    field: identifier.clone(),
965                }),
966            }
967        }
968
969        async fn call(
970            &self,
971            function_name: &Identifier,
972            mut arguments: Arguments<'_, Self>,
973        ) -> Result<Value, RenderError> {
974            match function_name.as_str() {
975                "identity" => {
976                    let value: Value = arguments.pop_position()?;
977                    arguments.ensure_consumed()?;
978                    Ok(value)
979                }
980                "add" => {
981                    let a: i64 = arguments.pop_position()?;
982                    let b: i64 = arguments.pop_position()?;
983                    arguments.ensure_consumed()?;
984                    Ok((a + b).into())
985                }
986                "concat" => {
987                    let mut a: String = arguments.pop_position()?;
988                    let b: String = arguments.pop_position()?;
989                    let reverse: bool = arguments.pop_keyword("reverse")?;
990                    arguments.ensure_consumed()?;
991                    a.push_str(&b);
992                    if reverse {
993                        Ok(a.chars().rev().collect::<String>().into())
994                    } else {
995                        Ok(a.into())
996                    }
997                }
998                _ => Err(RenderError::FunctionUnknown),
999            }
1000        }
1001    }
1002}