use std::any::Any;
use std::collections::{BTreeSet, HashMap};
use std::fmt::{Debug, Display, Formatter};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc;
use thiserror::Error;
use crate::TemplateMode;
use crate::util::{ContentTypeUtils, LoggingUtils};
pub type TemplateSelectorSet = BTreeSet<Option<String>>;
pub type TemplateResolutionAttributes = HashMap<Option<String>, TemplateResolutionAttributeValue>;
trait ErasedAttributeValue: Any + Send + Sync {
fn as_any(&self) -> &dyn Any;
fn equals_erased(&self, other: &dyn ErasedAttributeValue) -> bool;
fn hash_erased(&self) -> u64;
fn fmt_java(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result;
}
struct TypedAttributeValue<T>(T);
impl<T> ErasedAttributeValue for TypedAttributeValue<T>
where
T: Any + Display + Eq + Hash + Send + Sync,
{
fn as_any(&self) -> &dyn Any {
&self.0
}
fn equals_erased(&self, other: &dyn ErasedAttributeValue) -> bool {
other.as_any().downcast_ref::<T>() == Some(&self.0)
}
fn hash_erased(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.type_id().hash(&mut hasher);
self.0.hash(&mut hasher);
hasher.finish()
}
fn fmt_java(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, formatter)
}
}
#[derive(Clone)]
pub struct TemplateResolutionAttributeValue {
inner: Option<Arc<dyn ErasedAttributeValue>>,
}
impl TemplateResolutionAttributeValue {
#[must_use]
pub fn new<T>(value: T) -> Self
where
T: Any + Display + Eq + Hash + Send + Sync,
{
Self {
inner: Some(Arc::new(TypedAttributeValue(value))),
}
}
#[must_use]
pub const fn null() -> Self {
Self { inner: None }
}
#[must_use]
pub const fn is_null(&self) -> bool {
self.inner.is_none()
}
}
impl Debug for TemplateResolutionAttributeValue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, formatter)
}
}
impl Display for TemplateResolutionAttributeValue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match &self.inner {
Some(inner) => inner.fmt_java(formatter),
None => formatter.write_str("null"),
}
}
}
impl PartialEq for TemplateResolutionAttributeValue {
fn eq(&self, other: &Self) -> bool {
match (&self.inner, &other.inner) {
(Some(left), Some(right)) => left.equals_erased(right.as_ref()),
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
}
}
}
impl Eq for TemplateResolutionAttributeValue {}
impl Hash for TemplateResolutionAttributeValue {
fn hash<H: Hasher>(&self, state: &mut H) {
match &self.inner {
Some(inner) => {
1_u8.hash(state);
inner.hash_erased().hash(state);
}
None => 0_u8.hash(state),
}
}
}
#[derive(Clone, Debug)]
pub struct TemplateSpec {
template: String,
template_selectors: Option<Vec<String>>,
template_mode: Option<TemplateMode>,
template_resolution_attributes: Option<TemplateResolutionAttributes>,
output_content_type: Option<String>,
output_sse: bool,
}
impl TemplateSpec {
pub fn with_template_mode(
template: Option<&str>,
template_mode: Option<TemplateMode>,
) -> Result<Self, TemplateSpecError> {
Self::try_new(template, None, template_mode, None, None)
}
pub fn with_output_content_type(
template: Option<&str>,
output_content_type: Option<&str>,
) -> Result<Self, TemplateSpecError> {
Self::try_new(template, None, None, output_content_type, None)
}
pub fn with_resolution_attributes(
template: Option<&str>,
template_resolution_attributes: Option<&TemplateResolutionAttributes>,
) -> Result<Self, TemplateSpecError> {
Self::try_new(template, None, None, None, template_resolution_attributes)
}
pub fn with_selectors_and_template_mode(
template: Option<&str>,
template_selectors: Option<&TemplateSelectorSet>,
template_mode: Option<TemplateMode>,
template_resolution_attributes: Option<&TemplateResolutionAttributes>,
) -> Result<Self, TemplateSpecError> {
Self::try_new(
template,
template_selectors,
template_mode,
None,
template_resolution_attributes,
)
}
pub fn with_selectors_and_output_content_type(
template: Option<&str>,
template_selectors: Option<&TemplateSelectorSet>,
output_content_type: Option<&str>,
template_resolution_attributes: Option<&TemplateResolutionAttributes>,
) -> Result<Self, TemplateSpecError> {
Self::try_new(
template,
template_selectors,
None,
output_content_type,
template_resolution_attributes,
)
}
pub(crate) fn try_new(
template: Option<&str>,
template_selectors: Option<&TemplateSelectorSet>,
template_mode: Option<TemplateMode>,
output_content_type: Option<&str>,
template_resolution_attributes: Option<&TemplateResolutionAttributes>,
) -> Result<Self, TemplateSpecError> {
let template = template.ok_or(TemplateSpecError::TemplateCannotBeNull)?;
if template_mode.is_some() && output_content_type.is_some() {
return Err(TemplateSpecError::ModeAndContentTypeConflict);
}
let template_selectors = normalize_selectors(template_selectors)?;
let template_resolution_attributes =
template_resolution_attributes.filter(|attributes| !attributes.is_empty());
let output_content_type = output_content_type.map(str::to_owned);
let computed_template_mode = ContentTypeUtils::compute_template_mode_for_content_type(
output_content_type.as_deref(),
)
.map_err(TemplateSpecError::from)?;
let output_sse =
ContentTypeUtils::is_content_type_sse(output_content_type.as_deref()) == Ok(true);
Ok(Self {
template: template.to_owned(),
template_selectors,
template_mode: computed_template_mode.or(template_mode),
template_resolution_attributes: template_resolution_attributes.cloned(),
output_content_type,
output_sse,
})
}
#[must_use]
pub fn get_template(&self) -> &str {
&self.template
}
#[must_use]
pub const fn has_template_selectors(&self) -> bool {
self.template_selectors.is_some()
}
#[must_use]
pub fn get_template_selectors(&self) -> Option<&[String]> {
self.template_selectors.as_deref()
}
#[must_use]
pub const fn has_template_mode(&self) -> bool {
self.template_mode.is_some()
}
#[must_use]
pub const fn get_template_mode(&self) -> Option<TemplateMode> {
self.template_mode
}
#[must_use]
pub const fn has_template_resolution_attributes(&self) -> bool {
self.template_resolution_attributes.is_some()
}
#[must_use]
pub const fn get_template_resolution_attributes(
&self,
) -> Option<&TemplateResolutionAttributes> {
self.template_resolution_attributes.as_ref()
}
#[must_use]
pub fn get_output_content_type(&self) -> Option<&str> {
self.output_content_type.as_deref()
}
#[must_use]
pub const fn is_output_sse(&self) -> bool {
self.output_sse
}
pub fn equals_java(&self, other: Option<&dyn Any>) -> Result<bool, TemplateSpecError> {
let Some(other) = other else {
return Ok(false);
};
let Some(that) = other.downcast_ref::<Self>() else {
return Ok(false);
};
if std::ptr::eq(self, that) {
return Ok(true);
}
if self.template != that.template
|| self.template_selectors != that.template_selectors
|| self.template_mode != that.template_mode
{
return Ok(false);
}
let Some(output_content_type) = &self.output_content_type else {
return Err(TemplateSpecError::EqualsNullOutputContentType);
};
if Some(output_content_type) != that.output_content_type.as_ref() {
return Ok(false);
}
Ok(self.template_resolution_attributes == that.template_resolution_attributes)
}
}
impl PartialEq for TemplateSpec {
fn eq(&self, other: &Self) -> bool {
self.template == other.template
&& self.template_selectors == other.template_selectors
&& self.template_mode == other.template_mode
&& self.output_content_type == other.output_content_type
&& self.template_resolution_attributes == other.template_resolution_attributes
}
}
impl Eq for TemplateSpec {}
impl Hash for TemplateSpec {
fn hash<H: Hasher>(&self, state: &mut H) {
self.template.hash(state);
self.template_selectors.hash(state);
self.template_mode.hash(state);
self.output_content_type.hash(state);
hash_attributes(&self.template_resolution_attributes, state);
}
}
impl Display for TemplateSpec {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut rendered =
LoggingUtils::loggify_str(Some(&self.template)).expect("non-null template");
if let Some(selectors) = &self.template_selectors {
rendered.push_str("::");
rendered.push_str(&format_selectors(selectors));
}
if let Some(template_mode) = self.template_mode {
rendered.push_str(" @");
rendered.push_str(&template_mode.to_string());
}
if let Some(attributes) = &self.template_resolution_attributes {
rendered.push_str(" (");
rendered.push_str(&format_attributes(attributes));
rendered.push(')');
}
if let Some(output_content_type) = &self.output_content_type {
rendered.push_str(" [");
rendered.push_str(output_content_type);
rendered.push(']');
}
formatter.write_str(&rendered)
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum TemplateSpecError {
#[error("Template cannot be null")]
TemplateCannotBeNull,
#[error("If template mode or output content type are specified, the other one cannot")]
ModeAndContentTypeConflict,
#[error("If specified, the Template Selector set cannot contain any nulls or empties")]
NullOrEmptyTemplateSelector,
#[error("Index 0 out of bounds for length 0")]
MalformedOutputContentType,
#[error("Cannot invoke \"String.equals(Object)\" because \"this.outputContentType\" is null")]
EqualsNullOutputContentType,
}
impl From<crate::util::ContentTypeError> for TemplateSpecError {
fn from(_: crate::util::ContentTypeError) -> Self {
Self::MalformedOutputContentType
}
}
fn normalize_selectors(
template_selectors: Option<&TemplateSelectorSet>,
) -> Result<Option<Vec<String>>, TemplateSpecError> {
let Some(template_selectors) = template_selectors.filter(|selectors| !selectors.is_empty())
else {
return Ok(None);
};
let mut normalized = Vec::with_capacity(template_selectors.len());
for selector in template_selectors {
let Some(selector) = selector else {
return Err(TemplateSpecError::NullOrEmptyTemplateSelector);
};
if is_java_empty_or_whitespace(selector) {
return Err(TemplateSpecError::NullOrEmptyTemplateSelector);
}
normalized.push(selector.clone());
}
normalized.sort_by(|left, right| left.encode_utf16().cmp(right.encode_utf16()));
Ok(Some(normalized))
}
fn is_java_empty_or_whitespace(value: &str) -> bool {
value.is_empty() || value.chars().all(is_java_whitespace)
}
fn is_java_whitespace(character: char) -> bool {
matches!(
character,
'\u{0009}'..='\u{000D}'
| '\u{001C}'..='\u{0020}'
| '\u{1680}'
| '\u{2000}'..='\u{2006}'
| '\u{2008}'..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{205F}'
| '\u{3000}'
)
}
fn format_selectors(selectors: &[String]) -> String {
format!("[{}]", selectors.join(", "))
}
pub(crate) fn format_attributes(attributes: &TemplateResolutionAttributes) -> String {
let mut entries = attributes
.iter()
.map(|(key, value)| format!("{}={value}", key.as_deref().unwrap_or("null")))
.collect::<Vec<_>>();
entries.sort();
format!("{{{}}}", entries.join(", "))
}
fn hash_attributes<H: Hasher>(attributes: &Option<TemplateResolutionAttributes>, state: &mut H) {
let Some(attributes) = attributes else {
0_u8.hash(state);
return;
};
1_u8.hash(state);
let mut entry_hashes = attributes
.iter()
.map(|entry| {
let mut hasher = DefaultHasher::new();
entry.hash(&mut hasher);
hasher.finish()
})
.collect::<Vec<_>>();
entry_hashes.sort_unstable();
entry_hashes.hash(state);
}
#[cfg(test)]
mod tests {
use std::any::Any;
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
use super::{
TemplateResolutionAttributeValue, TemplateResolutionAttributes, TemplateSelectorSet,
TemplateSpec, TemplateSpecError,
};
use crate::TemplateMode;
use crate::util::LoggingUtils;
fn hash_of<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn selectors(values: &[Option<&str>]) -> TemplateSelectorSet {
values
.iter()
.map(|value| value.map(str::to_owned))
.collect()
}
fn attributes() -> TemplateResolutionAttributes {
HashMap::from([
(
Some("tenant".to_owned()),
TemplateResolutionAttributeValue::new("acme".to_owned()),
),
(
Some("attempt".to_owned()),
TemplateResolutionAttributeValue::new(3_i32),
),
(None, TemplateResolutionAttributeValue::null()),
])
}
#[test]
fn constructors_validate_template_and_conflicting_fields() {
assert_eq!(
TemplateSpec::with_template_mode(None, None),
Err(TemplateSpecError::TemplateCannotBeNull)
);
assert_eq!(
TemplateSpec::try_new(
Some("index"),
None,
Some(TemplateMode::HTML),
Some("text/html"),
None
),
Err(TemplateSpecError::ModeAndContentTypeConflict)
);
assert_eq!(
TemplateSpec::with_output_content_type(Some("index"), Some(";;;")),
Err(TemplateSpecError::MalformedOutputContentType)
);
assert_eq!(
TemplateSpecError::TemplateCannotBeNull.to_string(),
"Template cannot be null"
);
assert_eq!(
TemplateSpecError::ModeAndContentTypeConflict.to_string(),
"If template mode or output content type are specified, the other one cannot"
);
assert_eq!(
TemplateSpecError::MalformedOutputContentType.to_string(),
"Index 0 out of bounds for length 0"
);
}
#[test]
fn constructors_normalize_selectors_and_copy_attributes() {
let empty_selectors = TemplateSelectorSet::new();
let empty_attributes = TemplateResolutionAttributes::new();
let plain = TemplateSpec::with_selectors_and_template_mode(
Some("index"),
Some(&empty_selectors),
None,
Some(&empty_attributes),
)
.unwrap();
assert!(!plain.has_template_selectors());
assert_eq!(plain.get_template_selectors(), None);
assert!(!plain.has_template_resolution_attributes());
assert_eq!(plain.get_template_resolution_attributes(), None);
for invalid in [
selectors(&[None]),
selectors(&[Some("")]),
selectors(&[Some(" \n")]),
selectors(&[Some("\u{2003}")]),
selectors(&[Some("\u{2009}")]),
] {
assert_eq!(
TemplateSpec::with_selectors_and_template_mode(
Some("index"),
Some(&invalid),
None,
None
),
Err(TemplateSpecError::NullOrEmptyTemplateSelector)
);
}
assert_eq!(
TemplateSpecError::NullOrEmptyTemplateSelector.to_string(),
"If specified, the Template Selector set cannot contain any nulls or empties"
);
let selected = selectors(&[
Some("footer"),
Some("article"),
Some("article"),
Some("\u{00A0}"),
]);
let mut source_attributes = attributes();
let spec = TemplateSpec::with_selectors_and_template_mode(
Some("index"),
Some(&selected),
Some(TemplateMode::XML),
Some(&source_attributes),
)
.unwrap();
source_attributes.clear();
assert_eq!(spec.get_template(), "index");
assert!(spec.has_template_selectors());
assert_eq!(
spec.get_template_selectors(),
Some(
[
"article".to_owned(),
"footer".to_owned(),
"\u{00A0}".to_owned()
]
.as_slice()
)
);
assert!(spec.has_template_mode());
assert_eq!(spec.get_template_mode(), Some(TemplateMode::XML));
assert!(spec.has_template_resolution_attributes());
assert_eq!(spec.get_template_resolution_attributes().unwrap().len(), 3);
assert_eq!(spec.get_output_content_type(), None);
assert!(!spec.is_output_sse());
}
#[test]
fn content_types_force_exact_modes_and_preserve_original_text() {
let cases = [
("text/html", Some(TemplateMode::HTML), false),
("application/xhtml+xml", Some(TemplateMode::HTML), false),
("application/xml", Some(TemplateMode::XML), false),
("text/xml", Some(TemplateMode::XML), false),
("application/rss+xml", Some(TemplateMode::XML), false),
("application/atom+xml", Some(TemplateMode::XML), false),
(
"application/javascript",
Some(TemplateMode::JAVASCRIPT),
false,
),
(
"application/x-javascript",
Some(TemplateMode::JAVASCRIPT),
false,
),
(
"application/ecmascript",
Some(TemplateMode::JAVASCRIPT),
false,
),
("text/javascript", Some(TemplateMode::JAVASCRIPT), false),
("text/ecmascript", Some(TemplateMode::JAVASCRIPT), false),
("application/json", Some(TemplateMode::JAVASCRIPT), false),
("text/css", Some(TemplateMode::CSS), false),
("text/plain", Some(TemplateMode::TEXT), false),
("text/event-stream", None, true),
("application/octet-stream", None, false),
("", None, false),
(" \t", None, false),
];
for (content_type, expected_mode, expected_sse) in cases {
let spec =
TemplateSpec::with_output_content_type(Some("index"), Some(content_type)).unwrap();
assert_eq!(spec.get_template_mode(), expected_mode, "{content_type}");
assert_eq!(spec.is_output_sse(), expected_sse, "{content_type}");
assert_eq!(spec.get_output_content_type(), Some(content_type));
}
let normalized = TemplateSpec::with_output_content_type(
Some("index"),
Some("; TEXT/HTML ;; Charset=UTF-8"),
)
.unwrap();
assert_eq!(normalized.get_template_mode(), Some(TemplateMode::HTML));
assert_eq!(
normalized.get_output_content_type(),
Some("; TEXT/HTML ;; Charset=UTF-8")
);
}
#[test]
fn all_public_constructor_shapes_preserve_fields() {
let attrs = attributes();
let by_mode =
TemplateSpec::with_template_mode(Some("index"), Some(TemplateMode::RAW)).unwrap();
assert_eq!(by_mode.get_template_mode(), Some(TemplateMode::RAW));
let by_attributes =
TemplateSpec::with_resolution_attributes(Some("index"), Some(&attrs)).unwrap();
assert!(by_attributes.has_template_resolution_attributes());
let selector_set = selectors(&[Some("main")]);
let by_content_type = TemplateSpec::with_selectors_and_output_content_type(
Some("index"),
Some(&selector_set),
Some("text/css"),
Some(&attrs),
)
.unwrap();
assert_eq!(by_content_type.get_template_mode(), Some(TemplateMode::CSS));
assert_eq!(by_content_type.get_template_selectors().unwrap().len(), 1);
}
#[test]
fn attribute_values_preserve_null_type_equality_hash_and_display() {
let null = TemplateResolutionAttributeValue::null();
let another_null = TemplateResolutionAttributeValue::null();
let i32_value = TemplateResolutionAttributeValue::new(7_i32);
let same_i32 = TemplateResolutionAttributeValue::new(7_i32);
let other_i32 = TemplateResolutionAttributeValue::new(8_i32);
let i64_value = TemplateResolutionAttributeValue::new(7_i64);
assert!(null.is_null());
assert!(!i32_value.is_null());
assert_eq!(null, another_null);
assert_ne!(null, i32_value);
assert_eq!(i32_value, same_i32);
assert_ne!(i32_value, other_i32);
assert_ne!(i32_value, i64_value);
assert_eq!(hash_of(&i32_value), hash_of(&same_i32));
assert_eq!(null.to_string(), "null");
assert_eq!(format!("{null:?}"), "null");
assert_eq!(i32_value.to_string(), "7");
}
#[test]
fn rust_equality_and_hash_are_safe_and_order_independent() {
let first_attributes = attributes();
let second_attributes = HashMap::from_iter(first_attributes.clone());
let first =
TemplateSpec::with_resolution_attributes(Some("index"), Some(&first_attributes))
.unwrap();
let second =
TemplateSpec::with_resolution_attributes(Some("index"), Some(&second_attributes))
.unwrap();
let different = TemplateSpec::with_template_mode(Some("other"), None).unwrap();
assert_eq!(first, second);
assert_eq!(hash_of(&first), hash_of(&second));
assert_ne!(first, different);
assert_ne!(hash_of(&first), hash_of(&different));
}
#[test]
fn equals_preserves_identity_order_and_null_output_bug() {
let without_content_type = TemplateSpec::with_template_mode(Some("index"), None).unwrap();
let same_fields = TemplateSpec::with_template_mode(Some("index"), None).unwrap();
assert_eq!(
without_content_type.equals_java(Some(&without_content_type)),
Ok(true)
);
assert_eq!(without_content_type.equals_java(None), Ok(false));
assert_eq!(
without_content_type.equals_java(Some(&"not a spec" as &dyn Any)),
Ok(false)
);
assert_eq!(
without_content_type.equals_java(Some(&same_fields)),
Err(TemplateSpecError::EqualsNullOutputContentType)
);
assert_eq!(
TemplateSpecError::EqualsNullOutputContentType.to_string(),
"Cannot invoke \"String.equals(Object)\" because \"this.outputContentType\" is null"
);
let different_template =
TemplateSpec::with_output_content_type(Some("other"), Some("text/html")).unwrap();
let left =
TemplateSpec::with_output_content_type(Some("index"), Some("text/html")).unwrap();
assert_eq!(left.equals_java(Some(&different_template)), Ok(false));
let selectors = selectors(&[Some("main")]);
let different_selectors = TemplateSpec::with_selectors_and_output_content_type(
Some("index"),
Some(&selectors),
Some("text/html"),
None,
)
.unwrap();
assert_eq!(left.equals_java(Some(&different_selectors)), Ok(false));
let different_mode =
TemplateSpec::with_output_content_type(Some("index"), Some("text/plain")).unwrap();
assert_eq!(left.equals_java(Some(&different_mode)), Ok(false));
let missing_content = TemplateSpec::with_template_mode(Some("index"), None).unwrap();
assert_eq!(left.equals_java(Some(&missing_content)), Ok(false));
let same =
TemplateSpec::with_output_content_type(Some("index"), Some("text/html")).unwrap();
assert_eq!(left.equals_java(Some(&same)), Ok(true));
let attrs = attributes();
let with_attrs = TemplateSpec::with_selectors_and_output_content_type(
Some("index"),
None,
Some("text/html"),
Some(&attrs),
)
.unwrap();
assert_eq!(left.equals_java(Some(&with_attrs)), Ok(false));
}
#[test]
fn display_matches_java_shape_and_loggifies_long_names() {
let selectors = selectors(&[Some("footer"), Some("article")]);
let attrs = HashMap::from([
(
Some("tenant".to_owned()),
TemplateResolutionAttributeValue::new("acme".to_owned()),
),
(None, TemplateResolutionAttributeValue::null()),
]);
let spec = TemplateSpec::with_selectors_and_output_content_type(
Some("home\npage"),
Some(&selectors),
Some("text/html;charset=UTF-8"),
Some(&attrs),
)
.unwrap();
assert_eq!(
spec.to_string(),
"home page::[article, footer] @HTML ({null=null, tenant=acme}) [text/html;charset=UTF-8]"
);
assert!(format!("{spec:?}").contains("template: \"home\\npage\""));
let short = "x".repeat(120);
assert_eq!(LoggingUtils::loggify_str(Some(&short)), Some(short.clone()));
let long = format!("{}\n{}尾", "a".repeat(34), "b".repeat(90));
let rendered = LoggingUtils::loggify_str(Some(&long)).expect("rendered");
assert!(rendered.starts_with(&format!("{} ", "a".repeat(34))));
assert!(rendered.contains("[...]"));
assert!(rendered.ends_with('尾'));
}
#[test]
fn selectors_use_java_utf16_lexicographic_order() {
let selector_set = selectors(&[Some("\u{E000}"), Some("\u{10000}")]);
let spec = TemplateSpec::with_selectors_and_template_mode(
Some("index"),
Some(&selector_set),
None,
None,
)
.unwrap();
assert_eq!(
spec.get_template_selectors(),
Some(["\u{10000}".to_owned(), "\u{E000}".to_owned()].as_slice())
);
}
}