use super::accept::AcceptHeader;
use super::media_type::MediaType;
pub trait Renderer {
fn media_type(&self) -> &MediaType;
fn format(&self) -> &str;
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum NegotiationError {
NoSuitableRenderer,
}
impl std::fmt::Display for NegotiationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NegotiationError::NoSuitableRenderer => write!(f, "No suitable renderer found"),
}
}
}
impl std::error::Error for NegotiationError {}
pub trait BaseContentNegotiation {
fn select_parser(
&self,
_request: Option<&str>,
_parsers: &[MediaType],
) -> Result<MediaType, NegotiationError> {
Err(NegotiationError::NoSuitableRenderer)
}
fn select_renderer(
&self,
_request: Option<&str>,
_renderers: &[MediaType],
) -> Result<(MediaType, String), NegotiationError> {
Err(NegotiationError::NoSuitableRenderer)
}
}
#[derive(Debug, Clone)]
pub struct ContentNegotiator {
default_media_type: MediaType,
}
impl ContentNegotiator {
pub fn new() -> Self {
Self {
default_media_type: MediaType::new("application", "json"),
}
}
pub fn with_default(mut self, media_type: MediaType) -> Self {
self.default_media_type = media_type;
self
}
pub fn negotiate(&self, accept_header: &str, available: &[MediaType]) -> MediaType {
let accept = AcceptHeader::parse(accept_header);
accept
.find_best_match(available)
.unwrap_or_else(|| self.default_media_type.clone())
}
pub fn select_renderer(
&self,
accept_header: Option<&str>,
renderers: &[MediaType],
) -> Result<(MediaType, String), NegotiationError> {
if renderers.is_empty() {
return Err(NegotiationError::NoSuitableRenderer);
}
let accept_str = accept_header.unwrap_or("");
if accept_str.is_empty() || accept_str == "*/*" {
let renderer = renderers[0].clone();
let media_type_str = renderer.to_string();
return Ok((renderer, media_type_str));
}
let accept = AcceptHeader::parse(accept_str);
for accepted in &accept.media_types {
for renderer in renderers {
if accepted.matches(renderer) {
let result_str = if !accepted.parameters.is_empty() {
accepted.full_string()
} else {
renderer.to_string()
};
return Ok((renderer.clone(), result_str));
}
}
}
Err(NegotiationError::NoSuitableRenderer)
}
pub fn filter_renderers(
&self,
renderers: &[RendererInfo],
format: &str,
) -> Result<Vec<RendererInfo>, NegotiationError> {
let filtered: Vec<_> = renderers
.iter()
.filter(|r| r.format == format)
.cloned()
.collect();
if filtered.is_empty() {
Err(NegotiationError::NoSuitableRenderer)
} else {
Ok(filtered)
}
}
pub fn select_by_format(&self, format: &str, available: &[MediaType]) -> Option<MediaType> {
let format_lower = format.to_lowercase();
available
.iter()
.find(|mt| {
mt.subtype.to_lowercase() == format_lower
|| mt.to_string().to_lowercase().contains(&format_lower)
})
.cloned()
}
}
#[derive(Debug, Clone)]
pub struct RendererInfo {
pub media_type: MediaType,
pub format: String,
}
impl Default for ContentNegotiator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BaseNegotiator;
impl BaseContentNegotiation for BaseNegotiator {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_negotiate() {
let negotiator = ContentNegotiator::new();
let available = vec![
MediaType::new("application", "json"),
MediaType::new("text", "html"),
];
let result = negotiator.negotiate("text/html, application/json", &available);
assert_eq!(result.subtype, "html");
}
#[test]
fn test_select_by_format() {
let negotiator = ContentNegotiator::new();
let available = vec![
MediaType::new("application", "json"),
MediaType::new("text", "html"),
];
let result = negotiator.select_by_format("json", &available);
assert!(result.is_some());
assert_eq!(result.unwrap().subtype, "json");
}
}