use std::fmt;
use std::str::FromStr;
use tagged_urn::{TaggedUrn, TaggedUrnBuilder, TaggedUrnError};
pub const MEDIA_VOID: &str = "media:void";
pub const MEDIA_STRING: &str = "media:textable;form=scalar";
pub const MEDIA_INTEGER: &str = "media:integer;textable;numeric;form=scalar";
pub const MEDIA_NUMBER: &str = "media:textable;numeric;form=scalar";
pub const MEDIA_BOOLEAN: &str = "media:bool;textable;form=scalar";
pub const MEDIA_OBJECT: &str = "media:form=map;textable";
pub const MEDIA_BINARY: &str = "media:bytes";
pub const MEDIA_STRING_ARRAY: &str = "media:textable;form=list";
pub const MEDIA_INTEGER_ARRAY: &str = "media:integer;textable;numeric;form=list";
pub const MEDIA_NUMBER_ARRAY: &str = "media:textable;numeric;form=list";
pub const MEDIA_BOOLEAN_ARRAY: &str = "media:bool;textable;form=list";
pub const MEDIA_OBJECT_ARRAY: &str = "media:form=list;textable";
pub const MEDIA_PNG: &str = "media:image;png;bytes";
pub const MEDIA_AUDIO: &str = "media:wav;audio;bytes;";
pub const MEDIA_VIDEO: &str = "media:video;bytes";
pub const MEDIA_AUDIO_SPEECH: &str = "media:audio;wav;bytes;speech";
pub const MEDIA_IMAGE_THUMBNAIL: &str = "media:image;png;bytes;thumbnail";
pub const MEDIA_PDF: &str = "media:pdf;bytes";
pub const MEDIA_EPUB: &str = "media:epub;bytes";
pub const MEDIA_MD: &str = "media:md;textable";
pub const MEDIA_TXT: &str = "media:txt;textable";
pub const MEDIA_RST: &str = "media:rst;textable";
pub const MEDIA_LOG: &str = "media:log;textable";
pub const MEDIA_HTML: &str = "media:html;textable";
pub const MEDIA_XML: &str = "media:xml;textable";
pub const MEDIA_JSON: &str = "media:json;textable;form=map";
pub const MEDIA_JSON_SCHEMA: &str = "media:json;json-schema;textable;form=map";
pub const MEDIA_YAML: &str = "media:yaml;textable;form=map";
pub const MEDIA_FILE_PATH: &str = "media:file-path;textable;form=scalar";
pub const MEDIA_FILE_PATH_ARRAY: &str = "media:file-path;textable;form=list";
pub const MEDIA_FRONTMATTER_TEXT: &str = "media:frontmatter;textable;form=scalar";
pub const MEDIA_MODEL_SPEC: &str = "media:model-spec;textable;form=scalar";
pub const MEDIA_MLX_MODEL_PATH: &str = "media:mlx-model-path;textable;form=scalar";
pub const MEDIA_MODEL_REPO: &str = "media:model-repo;textable;form=map";
pub fn binary_media_urn_for_ext(ext: &str) -> String {
format!("media:binary;ext={}", ext)
}
pub fn text_media_urn_for_ext(ext: &str) -> String {
format!("media:ext={};textable", ext)
}
pub fn image_media_urn_for_ext(ext: &str) -> String {
format!("media:image;ext={};bytes", ext)
}
pub fn audio_media_urn_for_ext(ext: &str) -> String {
format!("media:audio;ext={};bytes", ext)
}
pub const MEDIA_MODEL_DIM: &str = "media:model-dim;integer;textable;numeric;form=scalar";
pub const MEDIA_DOWNLOAD_OUTPUT: &str = "media:download-result;textable;form=map";
pub const MEDIA_LIST_OUTPUT: &str = "media:model-list;textable;form=map";
pub const MEDIA_STATUS_OUTPUT: &str = "media:model-status;textable;form=map";
pub const MEDIA_CONTENTS_OUTPUT: &str = "media:model-contents;textable;form=map";
pub const MEDIA_EMBEDDING_VECTOR: &str = "media:embedding-vector;textable;form=map";
pub const MEDIA_LLM_INFERENCE_OUTPUT: &str = "media:generated-text;textable;form=map";
pub const MEDIA_FILE_METADATA: &str = "media:file-metadata;textable;form=map";
pub const MEDIA_DOCUMENT_OUTLINE: &str = "media:document-outline;textable;form=map";
pub const MEDIA_DISBOUND_PAGES: &str = "media:disbound-pages;textable;form=list";
pub const MEDIA_CAPTION_OUTPUT: &str = "media:image-caption;textable;form=map";
pub const MEDIA_TRANSCRIPTION_OUTPUT: &str = "media:transcription;textable;form=map";
pub const MEDIA_VISION_INFERENCE_OUTPUT: &str = "media:vision-inference-output;textable;form=map";
pub const MEDIA_DECISION: &str = "media:decision;bool;textable;form=scalar";
pub const MEDIA_DECISION_ARRAY: &str = "media:decision;bool;textable;form=list";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MediaUrn(TaggedUrn);
impl MediaUrn {
pub const PREFIX: &'static str = "media";
pub fn new(urn: TaggedUrn) -> Result<Self, MediaUrnError> {
if urn.prefix != Self::PREFIX {
return Err(MediaUrnError::InvalidPrefix {
expected: Self::PREFIX.to_string(),
actual: urn.prefix.clone(),
});
}
Ok(Self(urn))
}
pub fn from_string(s: &str) -> Result<Self, MediaUrnError> {
let urn = TaggedUrn::from_string(s).map_err(MediaUrnError::Parse)?;
Self::new(urn)
}
pub fn simple(type_name: &str, version: u32) -> Self {
let urn = TaggedUrnBuilder::new(Self::PREFIX)
.solo_tag(type_name)
.tag("v", &version.to_string())
.expect("version is non-empty")
.build()
.expect("valid media URN");
Self(urn)
}
pub fn with_subtype(type_name: &str, subtype: &str, version: Option<u32>) -> Self {
let mut builder = TaggedUrnBuilder::new(Self::PREFIX)
.solo_tag(type_name)
.tag("subtype", subtype)
.expect("subtype is non-empty");
if let Some(v) = version {
builder = builder.tag("v", &v.to_string()).expect("version is non-empty");
}
let urn = builder.build().expect("valid media URN");
Self(urn)
}
pub fn inner(&self) -> &TaggedUrn {
&self.0
}
pub fn type_name(&self) -> Option<&str> {
self.0.get_tag("type").map(|s| s.as_str())
}
pub fn subtype(&self) -> Option<&str> {
self.0.get_tag("subtype").map(|s| s.as_str())
}
pub fn version(&self) -> Option<u32> {
self.0.get_tag("v").and_then(|v| v.parse().ok())
}
pub fn profile(&self) -> Option<&str> {
self.0.get_tag("profile").map(|s| s.as_str())
}
pub fn get_tag(&self, key: &str) -> Option<&str> {
self.0.get_tag(key).map(|s| s.as_str())
}
pub fn has_tag(&self, key: &str, value: &str) -> bool {
self.0.has_tag(key, value)
}
pub fn with_tag(&self, key: &str, value: &str) -> Result<Self, tagged_urn::TaggedUrnError> {
Ok(Self(self.0.clone().with_tag(key.to_string(), value.to_string())?))
}
pub fn without_tag(&self, key: &str) -> Self {
Self(self.0.clone().without_tag(key))
}
pub fn to_string(&self) -> String {
self.0.to_string()
}
pub fn matches(&self, request: &MediaUrn) -> Result<bool, MediaUrnError> {
self.0.matches(&request.0).map_err(MediaUrnError::Match)
}
pub fn specificity(&self) -> usize {
self.0.specificity()
}
pub fn extension(&self) -> Option<&str> {
self.get_tag("ext")
}
pub fn is_binary(&self) -> bool {
self.get_tag("bytes").is_some()
}
pub fn is_map(&self) -> bool {
self.has_tag("form", "map")
}
pub fn is_scalar(&self) -> bool {
self.has_tag("form", "scalar")
}
pub fn is_list(&self) -> bool {
self.has_tag("form", "list")
}
pub fn is_structured(&self) -> bool {
self.is_map() || self.is_list()
}
pub fn is_json(&self) -> bool {
self.get_tag("json").is_some()
}
pub fn is_text(&self) -> bool {
self.get_tag("textable").is_some()
}
pub fn is_void(&self) -> bool {
self.0.tags.contains_key("void") || self.type_name() == Some("void")
}
pub fn is_file_path(&self) -> bool {
self.0.tags.get("file-path").map_or(false, |v| v == "*")
}
pub fn is_file_path_array(&self) -> bool {
self.0.tags.get("file-path-array").map_or(false, |v| v == "*")
}
pub fn is_any_file_path(&self) -> bool {
self.is_file_path() || self.is_file_path_array()
}
pub fn can_provide_input_for(&self, requirement: &MediaUrn) -> bool {
for (key, value) in &requirement.0.tags {
if value == "*" {
match self.0.tags.get(key) {
Some(self_value) if self_value == "*" => {
continue;
}
_ => {
return false;
}
}
} else {
match self.0.tags.get(key) {
Some(self_value) => {
if self_value != value && self_value != "*" {
return false;
}
}
None => {
continue;
}
}
}
}
true
}
}
impl fmt::Display for MediaUrn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for MediaUrn {
type Err = MediaUrnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_string(s)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MediaUrnError {
InvalidPrefix { expected: String, actual: String },
Parse(TaggedUrnError),
Match(TaggedUrnError),
}
impl fmt::Display for MediaUrnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MediaUrnError::InvalidPrefix { expected, actual } => {
write!(
f,
"invalid media URN prefix: expected '{}', got '{}'",
expected, actual
)
}
MediaUrnError::Parse(e) => write!(f, "failed to parse media URN: {}", e),
MediaUrnError::Match(e) => write!(f, "media URN match error: {}", e),
}
}
}
impl std::error::Error for MediaUrnError {}
impl From<TaggedUrnError> for MediaUrnError {
fn from(e: TaggedUrnError) -> Self {
MediaUrnError::Parse(e)
}
}
impl serde::Serialize for MediaUrn {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for MediaUrn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
MediaUrn::from_string(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let urn = MediaUrn::from_string("media:string").unwrap();
assert!(urn.to_string().contains("string"));
assert!(urn.version().is_none());
assert!(urn.subtype().is_none());
assert!(urn.profile().is_none());
}
#[test]
fn test_parse_with_subtype() {
let urn = MediaUrn::from_string("media:application;subtype=json").unwrap();
assert_eq!(urn.subtype(), Some("json"));
assert!(urn.to_string().contains("application"));
}
#[test]
fn test_parse_with_profile() {
let urn = MediaUrn::from_string(
r#"media:object;profile="https://example.com/schema.json""#,
)
.unwrap();
assert_eq!(urn.profile(), Some("https://example.com/schema.json"));
assert!(urn.to_string().contains("object"));
}
#[test]
fn test_wrong_prefix_fails() {
let result = MediaUrn::from_string("cap:string");
assert!(result.is_err());
if let Err(MediaUrnError::InvalidPrefix { expected, actual }) = result {
assert_eq!(expected, "media");
assert_eq!(actual, "cap");
} else {
panic!("expected InvalidPrefix error");
}
}
#[test]
fn test_is_binary() {
assert!(MediaUrn::from_string("media:bytes").unwrap().is_binary());
assert!(MediaUrn::from_string(MEDIA_PNG).unwrap().is_binary()); assert!(MediaUrn::from_string(MEDIA_PDF).unwrap().is_binary()); assert!(MediaUrn::from_string(MEDIA_BINARY).unwrap().is_binary()); assert!(!MediaUrn::from_string("media:textable").unwrap().is_binary());
assert!(!MediaUrn::from_string("media:object;textable;form=map").unwrap().is_binary());
}
#[test]
fn test_is_map() {
assert!(MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_map()); assert!(MediaUrn::from_string("media:custom;form=map").unwrap().is_map());
assert!(!MediaUrn::from_string("media:textable").unwrap().is_map());
assert!(!MediaUrn::from_string(MEDIA_STRING).unwrap().is_map()); assert!(!MediaUrn::from_string(MEDIA_STRING_ARRAY).unwrap().is_map()); }
#[test]
fn test_is_scalar() {
assert!(MediaUrn::from_string(MEDIA_STRING).unwrap().is_scalar()); assert!(MediaUrn::from_string(MEDIA_INTEGER).unwrap().is_scalar()); assert!(MediaUrn::from_string(MEDIA_NUMBER).unwrap().is_scalar()); assert!(MediaUrn::from_string(MEDIA_BOOLEAN).unwrap().is_scalar()); assert!(!MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_scalar()); assert!(!MediaUrn::from_string(MEDIA_STRING_ARRAY).unwrap().is_scalar()); }
#[test]
fn test_is_list() {
assert!(MediaUrn::from_string(MEDIA_STRING_ARRAY).unwrap().is_list()); assert!(MediaUrn::from_string(MEDIA_INTEGER_ARRAY).unwrap().is_list()); assert!(MediaUrn::from_string(MEDIA_OBJECT_ARRAY).unwrap().is_list()); assert!(!MediaUrn::from_string(MEDIA_STRING).unwrap().is_list()); assert!(!MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_list()); }
#[test]
fn test_is_structured() {
assert!(MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_structured()); assert!(MediaUrn::from_string(MEDIA_STRING_ARRAY).unwrap().is_structured()); assert!(MediaUrn::from_string(MEDIA_JSON).unwrap().is_structured()); assert!(!MediaUrn::from_string(MEDIA_STRING).unwrap().is_structured()); assert!(!MediaUrn::from_string(MEDIA_INTEGER).unwrap().is_structured()); assert!(!MediaUrn::from_string("media:textable").unwrap().is_structured());
}
#[test]
fn test_is_json() {
assert!(MediaUrn::from_string(MEDIA_JSON).unwrap().is_json()); assert!(MediaUrn::from_string("media:custom;json").unwrap().is_json());
assert!(!MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_json()); assert!(!MediaUrn::from_string("media:textable").unwrap().is_json());
}
#[test]
fn test_is_text() {
assert!(MediaUrn::from_string(MEDIA_STRING).unwrap().is_text()); assert!(MediaUrn::from_string(MEDIA_INTEGER).unwrap().is_text()); assert!(MediaUrn::from_string(MEDIA_OBJECT).unwrap().is_text()); assert!(!MediaUrn::from_string(MEDIA_BINARY).unwrap().is_text()); assert!(!MediaUrn::from_string(MEDIA_PNG).unwrap().is_text()); }
#[test]
fn test_is_void() {
assert!(MediaUrn::from_string("media:void").unwrap().is_void());
assert!(!MediaUrn::from_string("media:string").unwrap().is_void());
}
#[test]
fn test_simple_constructor() {
let urn = MediaUrn::simple("string", 1);
assert!(urn.to_string().contains("string"));
assert_eq!(urn.version(), Some(1));
}
#[test]
fn test_with_subtype_constructor() {
let urn = MediaUrn::with_subtype("application", "json", Some(1));
assert!(urn.to_string().contains("application"));
assert_eq!(urn.subtype(), Some("json"));
assert_eq!(urn.version(), Some(1));
}
#[test]
fn test_to_string_roundtrip() {
let original = "media:string";
let urn = MediaUrn::from_string(original).unwrap();
let s = urn.to_string();
let urn2 = MediaUrn::from_string(&s).unwrap();
assert_eq!(urn, urn2);
}
#[test]
fn test_constants_parse() {
assert!(MediaUrn::from_string(MEDIA_VOID).is_ok());
assert!(MediaUrn::from_string(MEDIA_STRING).is_ok());
assert!(MediaUrn::from_string(MEDIA_INTEGER).is_ok());
assert!(MediaUrn::from_string(MEDIA_NUMBER).is_ok());
assert!(MediaUrn::from_string(MEDIA_BOOLEAN).is_ok());
assert!(MediaUrn::from_string(MEDIA_OBJECT).is_ok());
assert!(MediaUrn::from_string(MEDIA_BINARY).is_ok());
assert!(MediaUrn::from_string(MEDIA_STRING_ARRAY).is_ok());
assert!(MediaUrn::from_string(MEDIA_INTEGER_ARRAY).is_ok());
assert!(MediaUrn::from_string(MEDIA_NUMBER_ARRAY).is_ok());
assert!(MediaUrn::from_string(MEDIA_BOOLEAN_ARRAY).is_ok());
assert!(MediaUrn::from_string(MEDIA_OBJECT_ARRAY).is_ok());
assert!(MediaUrn::from_string(MEDIA_PNG).is_ok());
assert!(MediaUrn::from_string(MEDIA_AUDIO).is_ok());
assert!(MediaUrn::from_string(MEDIA_VIDEO).is_ok());
assert!(MediaUrn::from_string(MEDIA_PDF).is_ok());
assert!(MediaUrn::from_string(MEDIA_EPUB).is_ok());
assert!(MediaUrn::from_string(MEDIA_MD).is_ok());
assert!(MediaUrn::from_string(MEDIA_TXT).is_ok());
assert!(MediaUrn::from_string(MEDIA_RST).is_ok());
assert!(MediaUrn::from_string(MEDIA_LOG).is_ok());
assert!(MediaUrn::from_string(MEDIA_HTML).is_ok());
assert!(MediaUrn::from_string(MEDIA_XML).is_ok());
assert!(MediaUrn::from_string(MEDIA_JSON).is_ok());
assert!(MediaUrn::from_string(MEDIA_YAML).is_ok());
}
#[test]
fn test_extension_helpers() {
let pdf_urn = binary_media_urn_for_ext("pdf");
assert!(pdf_urn.contains("ext=pdf"));
let parsed = MediaUrn::from_string(&pdf_urn).unwrap();
assert_eq!(parsed.extension(), Some("pdf"));
let md_urn = text_media_urn_for_ext("md");
assert!(md_urn.contains("ext=md"));
let parsed = MediaUrn::from_string(&md_urn).unwrap();
assert_eq!(parsed.extension(), Some("md"));
}
#[test]
fn test_media_urn_matching() {
let pdf_listing = MediaUrn::from_string(MEDIA_PDF).unwrap(); let pdf_requirement = MediaUrn::from_string("media:pdf").unwrap();
assert!(pdf_listing.matches(&pdf_requirement).expect("MediaUrn prefix mismatch impossible"));
let md_listing = MediaUrn::from_string(MEDIA_MD).unwrap(); let md_requirement = MediaUrn::from_string("media:md").unwrap();
assert!(md_listing.matches(&md_requirement).expect("MediaUrn prefix mismatch impossible"));
let string_urn = MediaUrn::from_string(MEDIA_STRING).unwrap();
let string_req = MediaUrn::from_string(MEDIA_STRING).unwrap();
assert!(string_urn.matches(&string_req).expect("MediaUrn prefix mismatch impossible"));
}
#[test]
fn test_matching() {
let handler = MediaUrn::from_string("media:string").unwrap();
let request = MediaUrn::from_string("media:string").unwrap();
assert!(handler.matches(&request).unwrap());
let general_handler = MediaUrn::from_string("media:string").unwrap();
assert!(general_handler.matches(&request).unwrap());
let same = MediaUrn::from_string("media:string").unwrap();
assert!(handler.matches(&same).unwrap());
}
#[test]
fn test_specificity() {
let urn1 = MediaUrn::from_string("media:string").unwrap();
let urn2 = MediaUrn::from_string("media:textable").unwrap();
let urn3 = MediaUrn::from_string("media:textable;form=scalar").unwrap();
let s1 = urn1.specificity();
let s2 = urn2.specificity();
let s3 = urn3.specificity();
assert!(s2 >= s1, "urn2 ({}) should have >= specificity than urn1 ({})", s2, s1);
assert!(s3 >= s2, "urn3 ({}) should have >= specificity than urn2 ({})", s3, s2);
}
#[test]
fn test_serde_roundtrip() {
let urn = MediaUrn::from_string("media:string").unwrap();
let json = serde_json::to_string(&urn).unwrap();
assert_eq!(json, "\"media:string\"");
let urn2: MediaUrn = serde_json::from_str(&json).unwrap();
assert_eq!(urn, urn2);
}
}
#[cfg(test)]
mod debug_tests {
use super::*;
use crate::standard::media::{MEDIA_BINARY, MEDIA_STRING, MEDIA_OBJECT};
#[test]
fn debug_matching_behavior() {
println!("MEDIA_BINARY = {}", MEDIA_BINARY);
println!("MEDIA_STRING = {}", MEDIA_STRING);
println!("MEDIA_OBJECT = {}", MEDIA_OBJECT);
let str_urn = MediaUrn::from_string(MEDIA_STRING).unwrap();
let obj_urn = MediaUrn::from_string(MEDIA_OBJECT).unwrap();
let _bin_urn = MediaUrn::from_string(MEDIA_BINARY).unwrap();
println!("string.matches(string) = {:?}", str_urn.matches(&str_urn));
println!("object.matches(string) = {:?}", obj_urn.matches(&str_urn));
println!("object.matches(object) = {:?}", obj_urn.matches(&obj_urn));
println!("string.matches(object) = {:?}", str_urn.matches(&obj_urn));
assert!(
!obj_urn.matches(&str_urn).expect("MediaUrn prefix mismatch impossible"),
"MEDIA_OBJECT should NOT match MEDIA_STRING"
);
}
}