vorma 0.86.0-pre.5

Rust-first full-stack web framework: typed views and resources, generated TypeScript contracts, and a Vite-integrated frontend.
Documentation
//! Media types that Vorma accepts or keeps.

use http::HeaderValue;

/// Whether a `Content-Type` value is `application/json` or a media type with
/// a `+json` suffix, such as `application/merge-patch+json`. Parameters such as
/// `charset` are allowed, and the comparison ignores case.
pub(crate) fn is_json_media_type(value: &HeaderValue) -> bool {
	read_essence(value).is_some_and(|essence| {
		essence == "application/json"
			|| essence
				.split_once('/')
				.and_then(|(_, subtype)| subtype.strip_suffix("+json"))
				.is_some_and(|name| !name.is_empty())
	})
}

/// The media type of an HTML form, read from a `Content-Type` value.
#[doc(hidden)]
#[derive(Debug)]
pub enum FormMediaType {
	/// `application/x-www-form-urlencoded`.
	UrlEncoded,
	/// `multipart/form-data`.
	Multipart {
		/// The text that separates the parts of the body.
		boundary: String,
	},
}

/// Why a `Content-Type` value names no form that Vorma reads.
pub(crate) enum FormMediaTypeError {
	/// The value is neither of the two media types of an HTML form.
	NotAForm,
	/// The value is `multipart/form-data` without a boundary that separates its parts.
	NoBoundary,
}

impl FormMediaType {
	/// Reads a `Content-Type` value. Parameters such as `charset` are allowed, and the comparison
	/// of the type ignores case.
	pub(crate) fn parse(value: &HeaderValue) -> Result<Self, FormMediaTypeError> {
		match read_essence(value).as_deref() {
			Some("application/x-www-form-urlencoded") => Ok(Self::UrlEncoded),
			Some("multipart/form-data") => value
				.to_str()
				.ok()
				.and_then(|text| multer::parse_boundary(text).ok())
				.map(|boundary| Self::Multipart { boundary })
				.ok_or(FormMediaTypeError::NoBoundary),
			_ => Err(FormMediaTypeError::NotAForm),
		}
	}
}

/// The lowercase `type/subtype` of a `Content-Type` value, without its
/// parameters, or `None` when the value is not a media type.
fn read_essence(value: &HeaderValue) -> Option<String> {
	let text = value.to_str().ok()?;
	let essence = text
		.split(';')
		.next()
		.unwrap_or_default()
		.trim()
		.to_ascii_lowercase();
	let (kind, subtype) = essence.split_once('/')?;
	(is_token(kind) && is_token(subtype)).then_some(essence)
}

/// Whether `text` is an HTTP token: one or more of the characters that RFC 9110
/// allows in a media type's type and subtype.
fn is_token(text: &str) -> bool {
	!text.is_empty()
		&& text
			.bytes()
			.all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
}