use std::fmt;
use crate::error::{Error, Result};
pub const MAX_IDEMPOTENCY_KEY_LEN: usize = 255;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Format {
#[default]
Pdf,
Png,
}
impl Format {
pub fn as_str(self) -> &'static str {
match self {
Format::Pdf => "pdf",
Format::Png => "png",
}
}
pub fn content_type(self) -> &'static str {
match self {
Format::Pdf => "application/pdf",
Format::Png => "image/png",
}
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Dpmm {
D6,
#[default]
D8,
D12,
D24,
}
impl Dpmm {
pub fn get(self) -> u32 {
match self {
Dpmm::D6 => 6,
Dpmm::D8 => 8,
Dpmm::D12 => 12,
Dpmm::D24 => 24,
}
}
}
impl TryFrom<u32> for Dpmm {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
6 => Ok(Dpmm::D6),
8 => Ok(Dpmm::D8),
12 => Ok(Dpmm::D12),
24 => Ok(Dpmm::D24),
other => Err(Error::InvalidArgument(format!(
"dpmm must be 6, 8, 12 or 24, not {other}"
))),
}
}
}
impl fmt::Display for Dpmm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.get())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderOptions {
pub format: Option<Format>,
pub page: Option<u32>,
pub dpmm: Option<Dpmm>,
pub timings: bool,
pub idempotency_key: Option<String>,
#[cfg(feature = "gzip")]
#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
pub compress: bool,
}
impl RenderOptions {
pub fn new() -> Self {
Self::default()
}
pub fn format(mut self, format: Format) -> Self {
self.format = Some(format);
self
}
pub fn page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
pub fn dpmm(mut self, dpmm: Dpmm) -> Self {
self.dpmm = Some(dpmm);
self
}
pub fn timings(mut self, timings: bool) -> Self {
self.timings = timings;
self
}
pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
self.idempotency_key = Some(key.into());
self
}
#[cfg(feature = "gzip")]
#[cfg_attr(docsrs, doc(cfg(feature = "gzip")))]
pub fn compress(mut self, compress: bool) -> Self {
self.compress = compress;
self
}
pub(crate) fn validate(&self) -> Result<()> {
if self.page == Some(0) {
return Err(Error::InvalidArgument("page must be 1 or more".into()));
}
if let Some(key) = &self.idempotency_key {
if key.is_empty() || key.len() > MAX_IDEMPOTENCY_KEY_LEN {
return Err(Error::InvalidArgument(format!(
"idempotency key must be 1 to {MAX_IDEMPOTENCY_KEY_LEN} characters"
)));
}
if !key.bytes().all(|b| (0x20..0x7f).contains(&b)) {
return Err(Error::InvalidArgument(
"idempotency key must be printable ASCII".into(),
));
}
}
Ok(())
}
pub(crate) fn query(&self) -> String {
let mut params = Vec::new();
if let Some(format) = self.format {
params.push(format!("format={format}"));
}
if let Some(page) = self.page {
params.push(format!("page={page}"));
}
if let Some(dpmm) = self.dpmm {
params.push(format!("dpmm={dpmm}"));
}
if self.timings {
params.push("timings=1".to_owned());
}
if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
}
}
#[cfg(feature = "gzip")]
pub(crate) fn compressed(&self) -> bool {
self.compress
}
#[cfg(not(feature = "gzip"))]
pub(crate) fn compressed(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_omits_unset_values() {
assert_eq!(RenderOptions::new().query(), "");
let all = RenderOptions::new()
.format(Format::Png)
.page(3)
.dpmm(Dpmm::D24)
.timings(true);
assert_eq!(all.query(), "?format=png&page=3&dpmm=24&timings=1");
}
#[test]
fn validates_page_and_key() {
assert!(RenderOptions::new().page(0).validate().is_err());
assert!(RenderOptions::new().page(1).validate().is_ok());
assert!(RenderOptions::new()
.idempotency_key("a".repeat(255))
.validate()
.is_ok());
assert!(RenderOptions::new()
.idempotency_key("a".repeat(256))
.validate()
.is_err());
assert!(RenderOptions::new()
.idempotency_key("line\nbreak")
.validate()
.is_err());
}
#[test]
fn dpmm_from_number() {
assert_eq!(Dpmm::try_from(12).unwrap(), Dpmm::D12);
assert!(matches!(Dpmm::try_from(10), Err(Error::InvalidArgument(_))));
}
}