Skip to main content

cookie_cutter_core/
lib.rs

1#![warn(clippy::pedantic)]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5use ariadne::{Cache, Source};
6use ast::{SourceFile, Template};
7use builtins::{StandardContextWorker, StandardContextualizer, StandardEscaper};
8use itertools::Itertools;
9use maybe_sync::{dyn_maybe_send_sync, Rc};
10#[cfg(feature = "serde")]
11use rendertime::value::serde_values;
12use std::fmt::{Debug, Display};
13use std::hash::Hash;
14use std::marker::PhantomData;
15use std::{collections::HashMap, fmt};
16
17#[doc(hidden)]
18#[allow(non_snake_case)]
19pub mod __internal__tests_util;
20
21mod ast;
22
23mod parse;
24pub use parse::Error as ParseError;
25
26pub(crate) mod rendertime;
27
28pub use rendertime::Error as RendertimeError;
29pub use rendertime::Value;
30
31#[cfg(feature = "serde")]
32pub use serde_values::Error as SerializationError;
33
34mod type_checking;
35pub use type_checking::{Error as TypeCheckingError, Indentation, Type};
36
37use crate::ast::TypeDefinition;
38
39pub mod builtins;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42/// An error that can happen when static text gets contextualized.
43pub enum ContextualizationError {
44    /// When the contextualizer does not know about a text type
45    UnknownTextType(String),
46    /// When the static text is invalid in the given context
47    InvalidStatic,
48    /// When a certain text type cannot be safely included in the given context
49    IncompatibleTextType {
50        /// The current context
51        target_ty: String,
52        /// The text type that cannot be included in that context
53        source_ty: String,
54    },
55    /// When a template or template literal ends in a different context than it started in.
56    StartAndEndDifferentContext(String, String),
57}
58
59impl Display for ContextualizationError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::UnknownTextType(ty) => write!(f, "Text type {ty:?} unknown"),
63            Self::InvalidStatic => write!(f, "Static text invalid for given text type"),
64            Self::IncompatibleTextType {
65                target_ty,
66                source_ty: dynamic_ty,
67            } => write!(
68                f,
69                "The target type {target_ty:?} is incompatible with the source type {dynamic_ty:?}"
70            ),
71            Self::StartAndEndDifferentContext(start, end) => write!(
72                f,
73                "The static text starts in type {start:?} but ends in {end:?}"
74            ),
75        }
76    }
77}
78
79impl std::error::Error for ContextualizationError {}
80
81/// A trait implemented by users wishing to change the way text types behave. This determines
82/// the default text type, how static text get's analyzed and changes the context and how to
83/// escape text to safely get from one text type to another.
84///
85/// The actual contextualizing is done by the implementations [`ContextWorker`].
86///
87/// While the escaping is done by an [`Escaper`] of the implementations choosing.
88pub trait Contextualizer<Esc, Wrk>: Debug
89where
90    Esc: Escaper,
91    Wrk: ContextWorker<Esc>,
92{
93    /// This determines the text type that parameters get when their type is omitted. It is usually
94    /// `text`, but implementors are free to change that.
95    fn default_text_type(&self) -> String;
96
97    /// This returns an appropriate [`ContextWorker`] for the given text type.
98    ///
99    /// # Errors
100    ///
101    /// In case of an unknown text type, this returns an [`ContextualizationError::UnknownTextType`].
102    fn contextualize(&self, text_ty: &str) -> Result<Wrk, ContextualizationError>;
103
104    /// This returns an [`Escaper`] to safely go from the `source_ty` text type to the `target_ty` text type.
105    ///
106    /// It is a shortcut to `self.contextualize(target_ty)?.dynamic(source_ty)`.
107    ///
108    /// # Errors
109    ///
110    /// If either the source or target text types are unknown this will return an
111    /// [`ContextualizationError::UnknownTextType`].
112    ///
113    /// If the two types are incompatible this will return [`ContextualizationError::IncompatibleTextType`].
114    fn escaper(&self, target_ty: &str, source_ty: &str) -> Result<Esc, ContextualizationError> {
115        self.contextualize(target_ty)?.dynamic(source_ty)
116    }
117}
118
119/// A trait to be implemented in tandem with [`Contextualizer`] by users wishing to change how
120/// static text is analysed to determine context and appropriate text type.
121pub trait ContextWorker<Esc: Escaper> {
122    /// Push additional static text to potentially change the current context.
123    ///
124    /// # Errors
125    ///
126    /// The static text may be invalid in the current context in which case a
127    /// [`ContextualizationError::InvalidStatic`] error gets returned.
128    fn push_static(&mut self, s: &str) -> Result<(), ContextualizationError>;
129
130    /// Calling this implies that the template wants to include a dynamic piece of text here. Given
131    /// the text type of this text this shall return an appropriate [`Escaper`] to safely do so.
132    ///
133    /// # Errors
134    ///
135    /// In case of incompatible text types this returns [`ContextualizationError::IncompatibleTextType`].
136    fn dynamic(&mut self, input_ty: &str) -> Result<Esc, ContextualizationError>;
137}
138
139/// A trait to be implemented in tandem with [`Contextualizer`] by users wishing to change how
140/// values are escaped to make it safe to include in a different context.
141pub trait Escaper: Debug + Clone {
142    /// The core functionality. Given a [`Value`] (usually a [`Value::Text`]) this shall return a
143    /// [`Value`] that is safe to include in the context this [`Escaper`] was derived from.
144    ///
145    /// # Errors
146    ///
147    /// In case the given [`Value`] cannot be included safely implementations are permitted to
148    /// instead return a [`String`] describing why that is.
149    fn escape(&self, inp: Value) -> Result<Value, String>;
150}
151
152/// A function to be used by template authors. There are several builtin ones. This can be implemented by users wishing to extend (or change) those.
153pub trait Function {
154    /// This determines whether a given set of parameter [`Type`]s are accepted by the [`Function`]
155    /// at type check time and if so which [`Type`] will be returned by the [`Function`] in that case.
156    /// To not do so when [`Function::run`] is called at render time is a logic error and might
157    /// result in unpredictable outcomes.
158    fn accepts(&self, args: Vec<Type>) -> Option<Type>;
159
160    /// To be called at render time by the engine. Consumes the provided arguments and shall
161    /// produce a value given those.
162    ///
163    /// # Errors
164    ///
165    /// Implementators are permitted to instead return a [`Box`]ed [`std::error::Error`] in case of
166    /// failure which will terminate the rendering and surface the error to the caller.
167    fn run(
168        &self,
169        args: Vec<Value>,
170    ) -> Result<Value, Box<maybe_sync::dyn_maybe_send_sync!(std::error::Error)>>;
171}
172
173/// The core struct of the templating engine. It represents fully type checked and ready to
174/// [`Templates::render`] templates.
175///
176/// It can be obtained several ways:
177/// - Using the `include_templates!` macro
178/// - Using [`Templates::new`]
179/// - Building them via a [`TemplatesBuilder`] obtained by either [`Templates::builder`] or [`TemplatesBuilder::default`].
180pub struct Templates<Esc = StandardEscaper> {
181    /// not just a map to templates, since the indexes get referenced as template calls
182    tmpl_names: HashMap<String, usize>,
183    tmpls: Vec<type_checking::Template<Esc>>,
184}
185
186impl<Esc> Debug for Templates<Esc> {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        f.debug_struct("Templates")
189            .field("templates", &self.tmpl_names.keys().collect_vec())
190            .finish_non_exhaustive()
191    }
192}
193
194/// A span referencing a portion of template source code. Typically returned by errors to indicate
195/// the particular location of the problem.
196#[derive(Clone, Copy, Hash, PartialEq, Eq)]
197pub struct Span<'s> {
198    start: usize,
199    end: usize,
200    path_and_source: (&'s str, &'s str),
201}
202
203impl Debug for Span<'_> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "{start}..{end}", start = self.start, end = self.end)
206    }
207}
208
209impl<'s> chumsky::span::Span for Span<'s> {
210    type Context = (&'s str, &'s str);
211
212    type Offset = usize;
213
214    fn new(context: Self::Context, range: std::ops::Range<Self::Offset>) -> Self {
215        Self {
216            start: range.start,
217            end: range.end,
218            path_and_source: context,
219        }
220    }
221
222    fn context(&self) -> Self::Context {
223        self.path_and_source
224    }
225
226    fn start(&self) -> Self::Offset {
227        self.start
228    }
229
230    fn end(&self) -> Self::Offset {
231        self.end
232    }
233}
234
235impl<'s> ariadne::Span for Span<'s> {
236    type SourceId = (&'s str, &'s str);
237
238    fn source(&self) -> &Self::SourceId {
239        &self.path_and_source
240    }
241
242    fn start(&self) -> usize {
243        self.start
244    }
245
246    fn end(&self) -> usize {
247        self.end
248    }
249}
250
251/// The error type returned if building [`Templates`] failed.
252pub enum Error<'s> {
253    /// If the parsing of one or more source files failed
254    Parse(ParseError<'s>),
255    /// If the final type checking at the end failed
256    TypeChecking(Box<type_checking::Error<'s>>),
257}
258
259struct AriadneCache<'s>(HashMap<&'s str, Source<&'s str>>);
260
261impl<'s> Cache<(&'s str, &'s str)> for AriadneCache<'s> {
262    type Storage = &'s str;
263
264    fn fetch(
265        &mut self,
266        id: &(&'s str, &'s str),
267    ) -> Result<&Source<Self::Storage>, impl fmt::Debug> {
268        Ok::<_, Box<dyn Debug>>(self.0.entry(id.0).or_insert_with(|| Source::from(id.1)))
269    }
270
271    fn display<'a>(&self, id: &'a (&'s str, &'s str)) -> Option<impl fmt::Display + 'a> {
272        Some(Box::new(id.0))
273    }
274}
275
276impl Debug for Error<'_> {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        f.write_str("\n")?;
279        <Self as Display>::fmt(self, f)
280    }
281}
282
283impl Display for Error<'_> {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        match self {
286            Error::Parse(err) => {
287                if f.alternate() {
288                    write!(f, "{err:#}")
289                } else {
290                    write!(f, "{err}")
291                }
292            }
293            Error::TypeChecking(err) => {
294                if f.alternate() {
295                    write!(f, "{err:#}")
296                } else {
297                    write!(f, "{err}")
298                }
299            }
300        }
301    }
302}
303
304impl std::error::Error for Error<'_> {}
305
306/// The main way of obtaining [`Templates`].
307///
308/// Created via [`TemplatesBuilder::default`] or [`TemplatesBuilder::new_with_ctxer`] if a specific
309/// Contextualizer is desired.
310///
311/// Add source files and functions then [`TemplatesBuilder::build`] when ready.
312pub struct TemplatesBuilder<
313    's,
314    Ctx = StandardContextualizer,
315    Esc = StandardEscaper,
316    Wrk = StandardContextWorker,
317> {
318    tmpls: Vec<Template<'s>>,
319    type_definitions: Vec<(TypeDefinition<'s>, Span<'s>)>,
320    funcs: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
321    ctxer: Ctx,
322    _esc: PhantomData<Esc>,
323    _wrk: PhantomData<Wrk>,
324}
325
326impl Debug for TemplatesBuilder<'_> {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        f.debug_struct("TemplatesBuilder")
329            .field("tmpls", &self.tmpls)
330            .field("funcs", &self.funcs.keys())
331            .finish()
332    }
333}
334
335impl<'s, Ctx, Esc, Wrk> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
336    /// Adds a source file with path and content.
337    ///
338    /// # Errors
339    /// Returns [`ParseError`] if the content could not be parsed.
340    pub fn with_source_file(
341        mut self,
342        path: &'s str,
343        source: &'s str,
344    ) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
345        let (tmpls, ty_defs) = SourceFile::parse(path, source)?.into_templates_and_type_defs();
346        self.tmpls.extend(tmpls);
347        self.type_definitions.extend(ty_defs);
348
349        Ok(self)
350    }
351
352    /// Adds multiple source files with paths and contents.
353    ///
354    /// # Errors
355    /// Returns [`ParseError`] if any of the contents could not be parsed.
356    pub fn with_source_files<I: IntoIterator<Item = (&'s str, &'s str)>>(
357        self,
358        sources: I,
359    ) -> Result<TemplatesBuilder<'s, Ctx, Esc, Wrk>, ParseError<'s>> {
360        let mut res = Ok(self);
361
362        for (path, source) in sources {
363            let inter_res =
364                SourceFile::parse(path, source).map(SourceFile::into_templates_and_type_defs);
365
366            res = match (res, inter_res) {
367                (Ok(mut builder), Ok((templates, type_definitions))) => {
368                    builder.tmpls.extend(templates);
369                    builder.type_definitions.extend(type_definitions);
370                    Ok(builder)
371                }
372                (Ok(_), Err(errors)) => Err(errors),
373                (res @ Err(_), Ok(_)) => res,
374                (Err(mut prev_error), Err(next_error)) => {
375                    if let ParseError::Multiple(ref mut prev) = prev_error {
376                        prev.push(next_error);
377                        return Err(prev_error);
378                    }
379
380                    return Err(ParseError::Multiple(vec![prev_error, next_error]));
381                }
382            }
383        }
384
385        res
386    }
387
388    /// Adds a specific function to make it available to the templates. Can be done before / after
389    /// / in the middle of adding the source files.
390    ///
391    /// Requires a name and the implementation.
392    #[must_use]
393    pub fn with_function(
394        mut self,
395        name: &'s str,
396        func: impl Function + 'static + maybe_sync::MaybeSync + maybe_sync::MaybeSend,
397    ) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
398        self.funcs.insert(name, Rc::new(func));
399
400        self
401    }
402
403    /// Adds multiple functions to make them avaiable to the templates. Can also be done at any
404    /// point compared to adding the templates.
405    ///
406    /// Requires a name and implementation for each.
407    #[must_use]
408    pub fn with_functions(
409        mut self,
410        funcs: impl IntoIterator<Item = (&'s str, Rc<dyn_maybe_send_sync!(Function)>)>,
411    ) -> TemplatesBuilder<'s, Ctx, Esc, Wrk> {
412        self.funcs.extend(funcs);
413
414        self
415    }
416
417    /// After adding all source files and functions this actually attempts to build the templates.
418    /// This involves type checking the templates and preparing them for rendering.
419    ///
420    /// The final result is a [`Templates`] ready for rendering.
421    ///
422    /// # Errors
423    ///
424    /// Since type checking can fail so can this. In which case it returns [`Error::TypeChecking`].
425    pub fn build(self) -> Result<Templates<Esc>, Error<'s>>
426    where
427        Esc: Escaper,
428        Ctx: Contextualizer<Esc, Wrk>,
429        Wrk: ContextWorker<Esc>,
430    {
431        let (tmpls, tmpl_names) = type_checking::type_check_templates(
432            self.tmpls,
433            // important: builtin functions first, so they get overwritten by user defined
434            // functions
435            builtins::functions()
436                .into_iter()
437                .chain(self.funcs.into_iter())
438                .collect(),
439            self.type_definitions,
440            &self.ctxer,
441        )
442        .map_err(Box::new)
443        .map_err(Error::TypeChecking)?;
444
445        Ok(Templates { tmpl_names, tmpls })
446    }
447
448    /// Returns an iterator of the names all previously registered templates.
449    ///
450    /// Intended for debugging.
451    #[must_use]
452    pub fn template_names<'a>(&'a self) -> impl ExactSizeIterator<Item = &'s str> + 'a {
453        self.tmpls.iter().map(|tmpl| tmpl.name.0)
454    }
455}
456
457impl<Ctx: Default, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
458    /// Creates a new [`TemplatesBuilder`], optionally with different Contextualization pipeline as
459    /// long as the [`Contextualizer`] implements [`Default`]. If you need a [`Contextualizer`]
460    /// which does not, you can use [`TemplatesBuilder::new_with_ctxer`] to provide one.
461    #[must_use]
462    pub fn new() -> Self {
463        TemplatesBuilder {
464            tmpls: Vec::new(),
465            type_definitions: Vec::new(),
466            funcs: builtins::functions().into_iter().collect(),
467            ctxer: Ctx::default(),
468            _esc: PhantomData,
469            _wrk: PhantomData,
470        }
471    }
472}
473
474impl<Ctx, Esc, Wrk> TemplatesBuilder<'_, Ctx, Esc, Wrk> {
475    /// Creates a [`TemplatesBuilder`] with a specific [`Contextualizer`].
476    #[must_use]
477    pub fn new_with_ctxer(ctxer: Ctx) -> Self {
478        TemplatesBuilder {
479            tmpls: Vec::new(),
480            type_definitions: Vec::new(),
481            funcs: builtins::functions().into_iter().collect(),
482            ctxer,
483            _esc: PhantomData,
484            _wrk: PhantomData,
485        }
486    }
487}
488
489impl<Ctx> Default for TemplatesBuilder<'_, Ctx>
490where
491    Ctx: Default,
492{
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498#[cfg(feature = "serde")]
499/// The error type that gets returned in case a render using a serde argument fails.
500#[derive(Debug)]
501pub enum SerdeRenderError {
502    /// If the serialization failed
503    Serde(serde_values::Error),
504    /// If the rendering itself failed
505    Rendertime(rendertime::Error),
506}
507
508#[cfg(feature = "serde")]
509impl Display for SerdeRenderError {
510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511        match self {
512            Self::Serde(err) => Display::fmt(&err, f),
513            Self::Rendertime(err) => Display::fmt(&err, f),
514        }
515    }
516}
517
518#[cfg(feature = "serde")]
519impl std::error::Error for SerdeRenderError {}
520
521impl<'s, Esc: Escaper> Templates<Esc> {
522    /// Directly creates a [`Templates`] providing source files and functions.
523    ///
524    /// # Errors
525    ///
526    /// Returns an [`Error`] in case parsing or type checking fails.
527    pub fn new<'f, Ctx, Wrk>(
528        source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
529        functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
530    ) -> Result<Templates<Esc>, Error<'s>>
531    where
532        Ctx: Contextualizer<Esc, Wrk> + Default,
533        Esc: Escaper,
534        Wrk: ContextWorker<Esc>,
535    {
536        TemplatesBuilder::<Ctx, Esc, Wrk>::new()
537            .with_source_files(source_files)
538            .map_err(Error::Parse)?
539            .with_functions(functions)
540            .build()
541    }
542
543    /// Creates a new [`TemplatesBuilder`] to build a [`Templates`].
544    #[must_use]
545    pub fn builder<Ctx, Wrk>() -> TemplatesBuilder<'s, Esc, Ctx, Wrk>
546    where
547        Esc: Default,
548    {
549        TemplatesBuilder::new()
550    }
551
552    /// Directly creates a [`Templates`] providing source files and functions as well as a specific
553    /// [`Contextualizer`].
554    ///
555    /// # Errors
556    ///
557    /// Returns an [`Error`] in case parsing or type checking fails.
558    pub fn new_with_ctxer<'f, Ctx, Wrk>(
559        source_files: impl IntoIterator<Item = (&'s str, &'s str)>,
560        functions: HashMap<&'s str, Rc<dyn_maybe_send_sync!(Function)>>,
561        ctxer: Ctx,
562    ) -> Result<Templates<Esc>, Error<'s>>
563    where
564        Ctx: Contextualizer<Esc, Wrk> + Default,
565        Esc: Escaper,
566        Wrk: ContextWorker<Esc>,
567    {
568        TemplatesBuilder::<Ctx, Esc, Wrk>::new_with_ctxer(ctxer)
569            .with_source_files(source_files)
570            .map_err(Error::Parse)?
571            .with_functions(functions)
572            .build()
573    }
574
575    /// Renders the specified template using the specified arguments, running the template and the
576    /// functions it invokes. The argument needs to be a [`Value::Struct`] containing the parameters.
577    /// Returns the output of the template as a [`String`].
578    ///
579    /// # Errors
580    ///
581    /// Templates can fail to render, such as when a wrong value was provided
582    /// ([`RendertimeError::WrongParameters`] or [`RendertimeError::NonStructRootValue`]), an escaper
583    /// reported an error ([`RendertimeError::Escape`]) or a function did so ([`RendertimeError::Function`]).
584    pub fn render<V: Into<Value>>(&self, name: &str, args: V) -> Result<String, RendertimeError> {
585        let Value::Struct(args) = args.into() else {
586            return Err(rendertime::Error::NonStructRootValue);
587        };
588        rendertime::tmpl_output(self, self.tmpl_names[name], args)
589    }
590
591    /// Renders the specified template using the specified arguments, running the template and the
592    /// functions it invokes. The argument needs to be a [`Value::Struct`] containing the parameters.
593    /// Writes the output of the template into a user provided [`fmt::Write`] implementation.
594    ///
595    /// # Errors
596    ///
597    /// Templates can fail to render, such as when a wrong value was provided
598    /// ([`RendertimeError::WrongParameters`] or [`RendertimeError::NonStructRootValue`]),
599    /// the [`fmt::Write`] instance failed to write ([`RendertimeError::Fmt`]),
600    /// an escaper reported an error ([`RendertimeError::Escape`])
601    /// or a function did so ([`RendertimeError::Function`]).
602    pub fn render_fmt<V: Into<Value>>(
603        &self,
604        name: &str,
605        args: V,
606        writer: impl fmt::Write,
607    ) -> Result<(), rendertime::Error> {
608        let Value::Struct(args) = args.into() else {
609            return Err(rendertime::Error::NonStructRootValue);
610        };
611        rendertime::write_tmpl(writer, self, self.tmpl_names[name], args)
612    }
613
614    /// Renders the specified template using the given serializable argument.
615    ///
616    /// See [`Templates::render`] for more.
617    ///
618    /// # Errors
619    ///
620    /// Can fail like [`Templates::render`] in the [`SerdeRenderError::Rendertime`] case,
621    /// but can also fail if the serialization failed ([`SerdeRenderError::Serde`],
622    /// [`serde_values::Error`]).
623    #[cfg(feature = "serde")]
624    pub fn render_serde(
625        &self,
626        name: &str,
627        args: impl serde::ser::Serialize,
628    ) -> Result<String, SerdeRenderError> {
629        self.render(
630            name,
631            args.serialize(serde_values::ValueSerializer)
632                .map_err(SerdeRenderError::Serde)?,
633        )
634        .map_err(SerdeRenderError::Rendertime)
635    }
636
637    #[cfg(feature = "serde")]
638    /// Renders the specified template using the given serializable argument into the user
639    /// specified [`fmt::Write`] implementation.
640    ///
641    /// See [`Templates::render_serde`] and [`Templates::render_fmt`] for more.
642    ///
643    /// # Errors
644    ///
645    /// Can fail like [`Templates::render_serde`] in the [`SerdeRenderError::Serde`] case,
646    /// but can also fail like [`Templates::render_fmt`] in the [`SerdeRenderError::Rendertime`] case.
647    pub fn render_serde_fmt(
648        &self,
649        name: &str,
650        args: impl serde::ser::Serialize,
651        writer: impl fmt::Write,
652    ) -> Result<(), SerdeRenderError> {
653        self.render_fmt(
654            name,
655            args.serialize(serde_values::ValueSerializer)
656                .map_err(SerdeRenderError::Serde)?,
657            writer,
658        )
659        .map_err(SerdeRenderError::Rendertime)
660    }
661}