mod cereal;
mod display;
mod error;
mod expression;
mod parse;
#[cfg(test)]
mod test_util;
#[cfg(test)]
mod tests;
mod value;
pub use error::{
Expected, RenderError, TemplateParseError, ValueError, WithValue,
};
pub use expression::{Expression, FunctionCall, Identifier, Literal};
pub use value::{
Arguments, FunctionOutput, RenderValue, StreamSource, TryFromValue, Value,
ValueStream,
};
use bytes::Bytes;
use futures::{Stream, StreamExt, future, stream};
use itertools::Itertools;
#[cfg(test)]
use proptest::{arbitrary::any, strategy::Strategy};
use slumber_util::NEW_ISSUE_LINK;
use std::{fmt::Debug, slice, sync::Arc};
pub trait Context<V>: Sized {
async fn get_field(
&self,
identifier: &Identifier,
) -> Result<V, RenderError>;
async fn call(
&self,
function_name: &Identifier,
arguments: Arguments<'_, Self>,
) -> Result<V, RenderError>;
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub struct Template {
#[cfg_attr(
test,
proptest(
strategy = "any::<Vec<TemplateChunk>>().prop_map(test_util::join_raw)"
)
)]
chunks: Vec<TemplateChunk>,
}
impl Template {
pub fn from_chunks(chunks: Vec<TemplateChunk>) -> Self {
assert!(
!chunks.iter().any(
|chunk| matches!(chunk, TemplateChunk::Raw(s) if s.is_empty())
)
&& !chunks.iter().tuple_windows().any(|pair| matches!(
pair,
(TemplateChunk::Raw(_), TemplateChunk::Raw(_))
)),
"Invalid chunks in generated template {chunks:?} This is a bug! \
Please report it. {NEW_ISSUE_LINK}"
);
Self { chunks }
}
pub fn into_chunks(self) -> Vec<TemplateChunk> {
self.chunks
}
pub fn raw(template: String) -> Self {
let chunks = if template.is_empty() {
vec![]
} else {
vec![TemplateChunk::Raw(template.into())]
};
Self { chunks }
}
pub fn file(path: String) -> Template {
Self::function_call("file", [path.into()], [])
}
pub fn function_call(
name: &'static str,
position: impl IntoIterator<Item = Expression>,
keyword: impl IntoIterator<Item = (&'static str, Option<Expression>)>,
) -> Self {
let chunks = vec![TemplateChunk::Expression(Expression::call(
name, position, keyword,
))];
Self { chunks }
}
pub fn is_empty(&self) -> bool {
self.chunks.is_empty()
}
pub fn is_dynamic(&self) -> bool {
self.chunks
.iter()
.any(|chunk| matches!(chunk, TemplateChunk::Expression(_)))
}
pub async fn render_chunks<Ctx>(
&self,
context: &Ctx,
) -> RenderedChunks<Value>
where
Ctx: Context<Value>,
{
self.render_chunks_inner(context).await
}
pub async fn render_chunks_stream<Ctx>(
&self,
context: &Ctx,
) -> RenderedChunks<ValueStream>
where
Ctx: Context<ValueStream>,
{
self.render_chunks_inner(context).await
}
pub async fn render_bytes<Ctx: Context<Value>>(
&self,
context: &Ctx,
) -> Result<Bytes, RenderError> {
self.render_chunks(context).await.try_into_bytes()
}
pub async fn render_string<Ctx: Context<Value>>(
&self,
context: &Ctx,
) -> Result<String, RenderError> {
let bytes = self.render_bytes(context).await?;
String::from_utf8(bytes.into()).map_err(RenderError::other)
}
async fn render_chunks_inner<Ctx, V>(
&self,
context: &Ctx,
) -> RenderedChunks<V>
where
Ctx: Context<V>,
V: RenderValue,
{
let futures = self.chunks.iter().map(|chunk| async move {
match chunk {
TemplateChunk::Raw(text) => {
RenderedChunk::Raw(Arc::clone(text))
}
TemplateChunk::Expression(expression) => {
match expression.render(context).await {
Ok(value) => RenderedChunk::Dynamic(value),
Err(error) => RenderedChunk::Error(error),
}
}
}
});
let chunks = future::join_all(futures).await;
RenderedChunks(chunks)
}
}
impl From<Expression> for Template {
fn from(expression: Expression) -> Self {
Self::from_chunks(vec![TemplateChunk::Expression(expression)])
}
}
#[cfg(any(test, feature = "test"))]
impl From<&'static str> for Template {
fn from(value: &'static str) -> Self {
value.parse().unwrap()
}
}
#[cfg(any(test, feature = "test"))]
impl<const N: usize> From<[TemplateChunk; N]> for Template {
fn from(chunks: [TemplateChunk; N]) -> Self {
Self {
chunks: chunks.into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub enum TemplateChunk {
Raw(
#[cfg_attr(test, proptest(strategy = "\".+\".prop_map(String::into)"))]
Arc<str>,
),
Expression(
#[cfg_attr(
test,
proptest(strategy = "test_util::expression_arbitrary()")
)]
Expression,
),
}
#[cfg(test)]
impl From<Expression> for TemplateChunk {
fn from(expression: Expression) -> Self {
Self::Expression(expression)
}
}
#[derive(Debug)]
pub struct RenderedChunks<V>(Vec<RenderedChunk<V>>);
impl<V: RenderValue> RenderedChunks<V> {
pub fn into_chunks(self) -> Vec<RenderedChunk<V>> {
self.0
}
pub fn chunks(&self) -> &[RenderedChunk<V>] {
&self.0
}
pub fn iter(&self) -> impl Iterator<Item = &RenderedChunk<V>> {
self.0.iter()
}
}
impl RenderedChunks<Value> {
pub fn try_into_bytes(self) -> Result<Bytes, RenderError> {
self.into_iter()
.map(|chunk| match chunk {
RenderedChunk::Raw(s) => {
Ok(Bytes::copy_from_slice(s.as_bytes()))
}
RenderedChunk::Dynamic(value) => Ok(value.into_bytes()),
RenderedChunk::Error(error) => Err(error),
})
.flatten_ok()
.try_collect()
}
}
impl RenderedChunks<ValueStream> {
pub fn stream_source(&self) -> Option<&StreamSource> {
if let [RenderedChunk::Dynamic(ValueStream::Stream { source, .. })] =
self.0.as_slice()
{
Some(source)
} else {
None
}
}
pub fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, RenderError>> + Send,
RenderError,
> {
let stream_value =
|bytes| Ok(stream::once(future::ready(Ok(bytes))).boxed());
let chunks = self
.0
.into_iter()
.map(move |chunk| match chunk {
RenderedChunk::Raw(s) => {
stream_value(Bytes::from(s.to_string()))
}
RenderedChunk::Dynamic(value) => match value {
ValueStream::Value(value) => {
stream_value(value.into_bytes())
}
ValueStream::Stream { stream, .. } => Ok(stream.boxed()),
},
RenderedChunk::Error(error) => Err(error),
})
.collect::<Result<Vec<_>, _>>()?;
Ok(stream::iter(chunks).flatten())
}
}
impl<V> IntoIterator for RenderedChunks<V> {
type Item = RenderedChunk<V>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a, V> IntoIterator for &'a RenderedChunks<V> {
type Item = &'a RenderedChunk<V>;
type IntoIter = slice::Iter<'a, RenderedChunk<V>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug)]
pub enum RenderedChunk<V> {
Raw(Arc<str>),
Dynamic(V),
Error(RenderError),
}
#[cfg(test)]
impl<V: PartialEq> PartialEq for RenderedChunk<V> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Raw(raw1), Self::Raw(raw2)) => raw1 == raw2,
(Self::Dynamic(value1), Self::Dynamic(value2)) => value1 == value2,
(Self::Error(error1), Self::Error(error2)) => {
error1.to_string() == error2.to_string()
}
_ => false,
}
}
}