use serde::{Deserialize, Serialize};
use std::{fmt, ops::Deref};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Uri(String);
impl Uri {
pub fn new<S: Into<String>>(uri: S) -> Result<Self, UriError> {
let uri_string = uri.into();
if !uri_string.contains(':') {
return Err(UriError::MissingScheme(uri_string));
}
if let Some(scheme_end) = uri_string.find(':') {
let scheme = &uri_string[..scheme_end];
if scheme.is_empty() {
return Err(UriError::EmptyScheme(uri_string));
}
if !scheme
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
{
return Err(UriError::InvalidScheme(uri_string));
}
if !scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-'))
{
return Err(UriError::InvalidScheme(uri_string));
}
}
Ok(Self(uri_string))
}
#[must_use]
pub fn new_unchecked<S: Into<String>>(uri: S) -> Self {
Self(uri.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn scheme(&self) -> Option<&str> {
self.0.split(':').next()
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Uri {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Deref for Uri {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for Uri {
fn from(uri: String) -> Self {
Self::new_unchecked(uri)
}
}
impl From<&str> for Uri {
fn from(uri: &str) -> Self {
Self::new_unchecked(uri)
}
}
impl PartialEq<&str> for Uri {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<Uri> for &str {
fn eq(&self, other: &Uri) -> bool {
*self == other.as_str()
}
}
impl From<Uri> for String {
fn from(uri: Uri) -> Self {
uri.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MimeType(String);
impl MimeType {
pub fn new<S: Into<String>>(mime: S) -> Result<Self, MimeTypeError> {
let mime_string = mime.into();
if !mime_string.contains('/') {
return Err(MimeTypeError::InvalidFormat(mime_string));
}
let main_part = mime_string.split(';').next().unwrap_or(&mime_string);
let parts: Vec<&str> = main_part.split('/').collect();
if parts.len() != 2 {
return Err(MimeTypeError::InvalidFormat(mime_string));
}
let type_part = parts[0].trim();
let subtype = parts[1].trim();
if type_part.is_empty() {
return Err(MimeTypeError::EmptyType(mime_string));
}
if subtype.is_empty() {
return Err(MimeTypeError::EmptySubtype(mime_string));
}
Ok(Self(mime_string))
}
#[must_use]
pub fn new_unchecked<S: Into<String>>(mime: S) -> Self {
Self(mime.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn type_part(&self) -> Option<&str> {
self.0
.split('/')
.next()
.map(|s| s.split(';').next().unwrap_or(s).trim())
}
#[must_use]
pub fn subtype(&self) -> Option<&str> {
self.0
.split('/')
.nth(1)
.map(|s| s.split(';').next().unwrap_or(s).trim())
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for MimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for MimeType {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Deref for MimeType {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for MimeType {
fn from(mime: String) -> Self {
Self::new_unchecked(mime)
}
}
impl From<&str> for MimeType {
fn from(mime: &str) -> Self {
Self::new_unchecked(mime)
}
}
impl PartialEq<&str> for MimeType {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<MimeType> for &str {
fn eq(&self, other: &MimeType) -> bool {
*self == other.as_str()
}
}
impl From<MimeType> for String {
fn from(mime: MimeType) -> Self {
mime.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Base64String(String);
impl Base64String {
pub fn new<S: Into<String>>(data: S) -> Result<Self, Base64Error> {
let data_string = data.into();
if !data_string
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
{
return Err(Base64Error::InvalidCharacters(data_string));
}
if let Some(first_pad) = data_string.find('=')
&& !data_string[first_pad..].chars().all(|c| c == '=')
{
return Err(Base64Error::InvalidPadding(data_string));
}
Ok(Self(data_string))
}
#[must_use]
pub fn new_unchecked<S: Into<String>>(data: S) -> Self {
Self(data.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for Base64String {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for Base64String {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Deref for Base64String {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for Base64String {
fn from(data: String) -> Self {
Self::new_unchecked(data)
}
}
impl From<&str> for Base64String {
fn from(data: &str) -> Self {
Self::new_unchecked(data)
}
}
impl PartialEq<&str> for Base64String {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<Base64String> for &str {
fn eq(&self, other: &Base64String) -> bool {
*self == other.as_str()
}
}
impl From<Base64String> for String {
fn from(b64: Base64String) -> Self {
b64.0
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum UriError {
#[error("URI missing scheme separator: {0}")]
MissingScheme(String),
#[error("URI has empty scheme: {0}")]
EmptyScheme(String),
#[error(
"URI has invalid scheme (must start with letter and contain only alphanumeric, +, ., -): {0}"
)]
InvalidScheme(String),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum MimeTypeError {
#[error("Invalid MIME type format (must be type/subtype): {0}")]
InvalidFormat(String),
#[error("MIME type has empty type part: {0}")]
EmptyType(String),
#[error("MIME type has empty subtype part: {0}")]
EmptySubtype(String),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum Base64Error {
#[error("Base64 string contains invalid characters: {0}")]
InvalidCharacters(String),
#[error("Base64 string has invalid padding (= must only appear at end): {0}")]
InvalidPadding(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uri_validation() {
assert!(Uri::new("file:///path/to/file").is_ok());
assert!(Uri::new("https://example.com").is_ok());
assert!(Uri::new("resource://test").is_ok());
assert!(Uri::new("custom+scheme://data").is_ok());
assert!(Uri::new("not-a-uri").is_err());
assert!(Uri::new(":no-scheme").is_err());
assert!(Uri::new("123://invalid-start").is_err());
}
#[test]
fn test_uri_scheme_extraction() {
let uri = Uri::new("https://example.com/path").unwrap();
assert_eq!(uri.scheme(), Some("https"));
let file_uri = Uri::new("file:///local/path").unwrap();
assert_eq!(file_uri.scheme(), Some("file"));
}
#[test]
fn test_mime_type_validation() {
assert!(MimeType::new("text/plain").is_ok());
assert!(MimeType::new("application/json").is_ok());
assert!(MimeType::new("text/html; charset=utf-8").is_ok());
assert!(MimeType::new("image/png").is_ok());
assert!(MimeType::new("invalid").is_err());
assert!(MimeType::new("/no-type").is_err());
assert!(MimeType::new("no-subtype/").is_err());
}
#[test]
fn test_mime_type_parts() {
let mime = MimeType::new("text/html; charset=utf-8").unwrap();
assert_eq!(mime.type_part(), Some("text"));
assert_eq!(mime.subtype(), Some("html"));
}
#[test]
fn test_base64_validation() {
assert!(Base64String::new("SGVsbG8gV29ybGQh").is_ok());
assert!(Base64String::new("YWJjMTIz").is_ok());
assert!(Base64String::new("dGVzdA==").is_ok());
assert!(Base64String::new("").is_ok());
assert!(Base64String::new("invalid!@#").is_err());
assert!(Base64String::new("test=data").is_err()); }
#[test]
fn test_domain_type_conversions() {
let uri = Uri::new("https://example.com").unwrap();
assert_eq!(uri.as_str(), "https://example.com");
assert_eq!(uri.to_string(), "https://example.com");
let mime = MimeType::new("text/plain").unwrap();
assert_eq!(mime.as_str(), "text/plain");
let b64 = Base64String::new("dGVzdA==").unwrap();
assert_eq!(b64.as_str(), "dGVzdA==");
}
}