use crate::media_urn::{
MEDIA_VOID, MEDIA_STRING, MEDIA_INTEGER, MEDIA_NUMBER, MEDIA_BOOLEAN, MEDIA_OBJECT,
MEDIA_STRING_ARRAY, MEDIA_INTEGER_ARRAY, MEDIA_NUMBER_ARRAY, MEDIA_BOOLEAN_ARRAY, MEDIA_OBJECT_ARRAY,
MEDIA_BINARY,
MEDIA_PDF, MEDIA_EPUB,
MEDIA_MD, MEDIA_TXT, MEDIA_RST, MEDIA_LOG, MEDIA_HTML, MEDIA_XML, MEDIA_JSON, MEDIA_YAML,
MEDIA_PNG, MEDIA_AUDIO, MEDIA_VIDEO, MEDIA_TEXT,
MEDIA_DOWNLOAD_OUTPUT, MEDIA_LOAD_OUTPUT, MEDIA_UNLOAD_OUTPUT,
MEDIA_LIST_OUTPUT, MEDIA_STATUS_OUTPUT, MEDIA_CONTENTS_OUTPUT,
MEDIA_GENERATE_OUTPUT, MEDIA_STRUCTURED_QUERY_OUTPUT, MEDIA_QUESTIONS_ARRAY,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
pub const SCHEMA_BASE: &str = "https://capns.org/schema";
pub fn get_schema_base() -> String {
if let Ok(schema_url) = std::env::var("CAPNS_SCHEMA_BASE_URL") {
return schema_url;
}
if let Ok(registry_url) = std::env::var("CAPNS_REGISTRY_URL") {
return format!("{}/schema", registry_url);
}
SCHEMA_BASE.to_string()
}
pub fn get_profile_url(profile_name: &str) -> String {
format!("{}/{}", get_schema_base(), profile_name)
}
pub const PROFILE_STR: &str = "https://capns.org/schema/str";
pub const PROFILE_INT: &str = "https://capns.org/schema/int";
pub const PROFILE_NUM: &str = "https://capns.org/schema/num";
pub const PROFILE_BOOL: &str = "https://capns.org/schema/bool";
pub const PROFILE_OBJ: &str = "https://capns.org/schema/obj";
pub const PROFILE_STR_ARRAY: &str = "https://capns.org/schema/str-array";
pub const PROFILE_INT_ARRAY: &str = "https://capns.org/schema/int-array";
pub const PROFILE_NUM_ARRAY: &str = "https://capns.org/schema/num-array";
pub const PROFILE_BOOL_ARRAY: &str = "https://capns.org/schema/bool-array";
pub const PROFILE_OBJ_ARRAY: &str = "https://capns.org/schema/obj-array";
pub const PROFILE_VOID: &str = "https://capns.org/schema/void";
pub const PROFILE_IMAGE: &str = "https://capns.org/schema/image";
pub const PROFILE_AUDIO: &str = "https://capns.org/schema/audio";
pub const PROFILE_VIDEO: &str = "https://capns.org/schema/video";
pub const PROFILE_TEXT: &str = "https://capns.org/schema/text";
pub const PROFILE_PDF: &str = "https://capns.org/schema/pdf";
pub const PROFILE_EPUB: &str = "https://capns.org/schema/epub";
pub const PROFILE_MD: &str = "https://capns.org/schema/md";
pub const PROFILE_TXT: &str = "https://capns.org/schema/txt";
pub const PROFILE_RST: &str = "https://capns.org/schema/rst";
pub const PROFILE_LOG: &str = "https://capns.org/schema/log";
pub const PROFILE_HTML: &str = "https://capns.org/schema/html";
pub const PROFILE_XML: &str = "https://capns.org/schema/xml";
pub const PROFILE_JSON: &str = "https://capns.org/schema/json";
pub const PROFILE_YAML: &str = "https://capns.org/schema/yaml";
pub const PROFILE_CAPNS_DOWNLOAD_OUTPUT: &str = "https://capns.org/schema/download-output";
pub const PROFILE_CAPNS_LOAD_OUTPUT: &str = "https://capns.org/schema/load-output";
pub const PROFILE_CAPNS_UNLOAD_OUTPUT: &str = "https://capns.org/schema/unload-output";
pub const PROFILE_CAPNS_LIST_OUTPUT: &str = "https://capns.org/schema/list-output";
pub const PROFILE_CAPNS_STATUS_OUTPUT: &str = "https://capns.org/schema/status-output";
pub const PROFILE_CAPNS_CONTENTS_OUTPUT: &str = "https://capns.org/schema/contents-output";
pub const PROFILE_CAPNS_GENERATE_OUTPUT: &str = "https://capns.org/schema/embeddings";
pub const PROFILE_CAPNS_STRUCTURED_QUERY_OUTPUT: &str = "https://capns.org/schema/structured-query-output";
pub const PROFILE_CAPNS_QUESTIONS_ARRAY: &str = "https://capns.org/schema/questions-array";
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct ArgumentValidation {
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_values: Option<Vec<String>>,
}
impl ArgumentValidation {
pub fn is_empty(&self) -> bool {
self.min.is_none() &&
self.max.is_none() &&
self.min_length.is_none() &&
self.max_length.is_none() &&
self.pattern.is_none() &&
self.allowed_values.is_none()
}
pub fn numeric_range(min: Option<f64>, max: Option<f64>) -> Self {
Self {
min,
max,
min_length: None,
max_length: None,
pattern: None,
allowed_values: None,
}
}
pub fn string_length(min_length: Option<usize>, max_length: Option<usize>) -> Self {
Self {
min: None,
max: None,
min_length,
max_length,
pattern: None,
allowed_values: None,
}
}
pub fn with_pattern(pattern: String) -> Self {
Self {
min: None,
max: None,
min_length: None,
max_length: None,
pattern: Some(pattern),
allowed_values: None,
}
}
pub fn with_allowed_values(values: Vec<String>) -> Self {
Self {
min: None,
max: None,
min_length: None,
max_length: None,
pattern: None,
allowed_values: Some(values),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MediaSpecDef {
String(String),
Object(MediaSpecDefObject),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MediaSpecDefObject {
pub media_type: String,
pub profile_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<ArgumentValidation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl MediaSpecDef {
pub fn string(media_spec: impl Into<String>) -> Self {
MediaSpecDef::String(media_spec.into())
}
pub fn object(media_type: impl Into<String>, profile_uri: impl Into<String>) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
})
}
pub fn object_with_schema(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
schema: serde_json::Value,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
schema: Some(schema),
title: None,
description: None,
validation: None,
metadata: None,
})
}
pub fn object_with_title(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
title: impl Into<String>,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
schema: None,
title: Some(title.into()),
description: None,
validation: None,
metadata: None,
})
}
pub fn object_full(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
schema: Option<serde_json::Value>,
title: Option<String>,
description: Option<String>,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
schema,
title,
description,
validation: None,
metadata: None,
})
}
pub fn object_with_validation(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
validation: ArgumentValidation,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
schema: None,
title: None,
description: None,
validation: Some(validation),
metadata: None,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedMediaSpec {
pub media_urn: String,
pub media_type: String,
pub profile_uri: Option<String>,
pub schema: Option<serde_json::Value>,
pub title: Option<String>,
pub description: Option<String>,
pub validation: Option<ArgumentValidation>,
}
impl ResolvedMediaSpec {
pub fn is_binary(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_binary())
.unwrap_or(false)
}
pub fn is_json(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_json())
.unwrap_or(false)
}
pub fn is_text(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_text())
.unwrap_or(false)
}
}
pub fn resolve_media_urn(
media_urn: &str,
media_specs: &HashMap<String, MediaSpecDef>,
) -> Result<ResolvedMediaSpec, MediaSpecError> {
if let Some(def) = media_specs.get(media_urn) {
return resolve_def(media_urn, def);
}
if let Some(resolved) = resolve_builtin(media_urn) {
return Ok(resolved);
}
Err(MediaSpecError::UnresolvableMediaUrn(media_urn.to_string()))
}
pub async fn resolve_media_urn_with_registry(
media_urn: &str,
media_specs: Option<&HashMap<String, MediaSpecDef>>,
registry: Option<&crate::media_registry::MediaUrnRegistry>,
) -> Result<ResolvedMediaSpec, MediaSpecError> {
if let Some(specs) = media_specs {
if let Some(def) = specs.get(media_urn) {
return resolve_def(media_urn, def);
}
}
if let Some(resolved) = resolve_builtin(media_urn) {
return Ok(resolved);
}
if let Some(registry) = registry {
match registry.get_media_spec(media_urn).await {
Ok(stored_spec) => {
return Ok(ResolvedMediaSpec {
media_urn: media_urn.to_string(),
media_type: stored_spec.media_type,
profile_uri: stored_spec.profile_uri,
schema: stored_spec.schema,
title: Some(stored_spec.title),
description: stored_spec.description,
validation: stored_spec.validation,
});
}
Err(_) => {
}
}
}
Err(MediaSpecError::UnresolvableMediaUrn(media_urn.to_string()))
}
fn resolve_def(media_urn: &str, def: &MediaSpecDef) -> Result<ResolvedMediaSpec, MediaSpecError> {
match def {
MediaSpecDef::String(s) => {
let parsed = MediaSpec::parse(s)?;
Ok(ResolvedMediaSpec {
media_urn: media_urn.to_string(),
media_type: parsed.media_type,
profile_uri: parsed.profile,
schema: None,
title: None,
description: None,
validation: None, })
}
MediaSpecDef::Object(obj) => Ok(ResolvedMediaSpec {
media_urn: media_urn.to_string(),
media_type: obj.media_type.clone(),
profile_uri: Some(obj.profile_uri.clone()),
schema: obj.schema.clone(),
title: obj.title.clone(),
description: obj.description.clone(),
validation: obj.validation.clone(), }),
}
}
fn resolve_builtin(media_urn: &str) -> Option<ResolvedMediaSpec> {
let (media_type, profile_uri) = match media_urn {
MEDIA_STRING => ("text/plain", Some(PROFILE_STR)),
MEDIA_INTEGER => ("text/plain", Some(PROFILE_INT)),
MEDIA_NUMBER => ("text/plain", Some(PROFILE_NUM)),
MEDIA_BOOLEAN => ("text/plain", Some(PROFILE_BOOL)),
MEDIA_OBJECT => ("application/json", Some(PROFILE_OBJ)),
MEDIA_STRING_ARRAY => ("application/json", Some(PROFILE_STR_ARRAY)),
MEDIA_INTEGER_ARRAY => ("application/json", Some(PROFILE_INT_ARRAY)),
MEDIA_NUMBER_ARRAY => ("application/json", Some(PROFILE_NUM_ARRAY)),
MEDIA_BOOLEAN_ARRAY => ("application/json", Some(PROFILE_BOOL_ARRAY)),
MEDIA_OBJECT_ARRAY => ("application/json", Some(PROFILE_OBJ_ARRAY)),
MEDIA_BINARY => ("application/octet-stream", None),
MEDIA_VOID => ("application/x-void", Some(PROFILE_VOID)),
MEDIA_PNG => ("image/png", Some(PROFILE_IMAGE)),
MEDIA_AUDIO => ("audio/wav", Some(PROFILE_AUDIO)),
MEDIA_VIDEO => ("video/mp4", Some(PROFILE_VIDEO)),
MEDIA_TEXT => ("text/plain", Some(PROFILE_TEXT)),
MEDIA_PDF => ("application/pdf", Some(PROFILE_PDF)),
MEDIA_EPUB => ("application/epub+zip", Some(PROFILE_EPUB)),
MEDIA_MD => ("text/markdown", Some(PROFILE_MD)),
MEDIA_TXT => ("text/plain", Some(PROFILE_TXT)),
MEDIA_RST => ("text/x-rst", Some(PROFILE_RST)),
MEDIA_LOG => ("text/plain", Some(PROFILE_LOG)),
MEDIA_HTML => ("text/html", Some(PROFILE_HTML)),
MEDIA_XML => ("application/xml", Some(PROFILE_XML)),
MEDIA_JSON => ("application/json", Some(PROFILE_JSON)),
MEDIA_YAML => ("application/x-yaml", Some(PROFILE_YAML)),
MEDIA_DOWNLOAD_OUTPUT => ("application/json", Some(PROFILE_CAPNS_DOWNLOAD_OUTPUT)),
MEDIA_LOAD_OUTPUT => ("application/json", Some(PROFILE_CAPNS_LOAD_OUTPUT)),
MEDIA_UNLOAD_OUTPUT => ("application/json", Some(PROFILE_CAPNS_UNLOAD_OUTPUT)),
MEDIA_LIST_OUTPUT => ("application/json", Some(PROFILE_CAPNS_LIST_OUTPUT)),
MEDIA_STATUS_OUTPUT => ("application/json", Some(PROFILE_CAPNS_STATUS_OUTPUT)),
MEDIA_CONTENTS_OUTPUT => ("application/json", Some(PROFILE_CAPNS_CONTENTS_OUTPUT)),
MEDIA_GENERATE_OUTPUT => ("application/json", Some(PROFILE_CAPNS_GENERATE_OUTPUT)),
MEDIA_STRUCTURED_QUERY_OUTPUT => ("application/json", Some(PROFILE_CAPNS_STRUCTURED_QUERY_OUTPUT)),
MEDIA_QUESTIONS_ARRAY => ("application/json", Some(PROFILE_CAPNS_QUESTIONS_ARRAY)),
_ => return None,
};
Some(ResolvedMediaSpec {
media_urn: media_urn.to_string(),
media_type: media_type.to_string(),
profile_uri: profile_uri.map(String::from),
schema: None, title: None, description: None,
validation: None, })
}
pub fn is_builtin_media_urn(media_urn: &str) -> bool {
matches!(
media_urn,
MEDIA_STRING
| MEDIA_INTEGER
| MEDIA_NUMBER
| MEDIA_BOOLEAN
| MEDIA_OBJECT
| MEDIA_STRING_ARRAY
| MEDIA_INTEGER_ARRAY
| MEDIA_NUMBER_ARRAY
| MEDIA_BOOLEAN_ARRAY
| MEDIA_OBJECT_ARRAY
| MEDIA_BINARY
| MEDIA_VOID
| MEDIA_PNG
| MEDIA_AUDIO
| MEDIA_VIDEO
| MEDIA_TEXT
| MEDIA_PDF
| MEDIA_EPUB
| MEDIA_MD
| MEDIA_TXT
| MEDIA_RST
| MEDIA_LOG
| MEDIA_HTML
| MEDIA_XML
| MEDIA_JSON
| MEDIA_YAML
| MEDIA_DOWNLOAD_OUTPUT
| MEDIA_LOAD_OUTPUT
| MEDIA_UNLOAD_OUTPUT
| MEDIA_LIST_OUTPUT
| MEDIA_STATUS_OUTPUT
| MEDIA_CONTENTS_OUTPUT
| MEDIA_GENERATE_OUTPUT
| MEDIA_STRUCTURED_QUERY_OUTPUT
| MEDIA_QUESTIONS_ARRAY
)
}
#[derive(Debug, Clone, PartialEq)]
pub struct MediaSpec {
pub media_type: String,
pub profile: Option<String>,
}
impl MediaSpec {
pub fn parse(s: &str) -> Result<Self, MediaSpecError> {
let s = s.trim();
if s.is_empty() {
return Err(MediaSpecError::EmptyMediaType);
}
let parts: Vec<&str> = s.splitn(2, ';').collect();
let media_type = parts[0].trim().to_string();
if media_type.is_empty() {
return Err(MediaSpecError::EmptyMediaType);
}
if !media_type.contains('/') {
return Err(MediaSpecError::InvalidMediaType(format!(
"media type '{}' must contain '/'",
media_type
)));
}
let profile = if parts.len() > 1 {
let params = parts[1].trim();
MediaSpec::parse_profile(params)?
} else {
None
};
Ok(MediaSpec { media_type, profile })
}
fn parse_profile(params: &str) -> Result<Option<String>, MediaSpecError> {
let lower = params.to_lowercase();
if let Some(pos) = lower.find("profile=") {
let after_profile = ¶ms[pos + 8..];
let value = if after_profile.starts_with('"') {
let rest = &after_profile[1..];
if let Some(end) = rest.find('"') {
rest[..end].to_string()
} else {
return Err(MediaSpecError::UnterminatedQuote);
}
} else {
after_profile
.split(';')
.next()
.unwrap_or("")
.trim()
.to_string()
};
if value.is_empty() {
Ok(None)
} else {
Ok(Some(value))
}
} else {
Ok(None)
}
}
pub fn primary_type(&self) -> &str {
self.media_type
.split('/')
.next()
.unwrap_or(&self.media_type)
}
pub fn subtype(&self) -> Option<&str> {
self.media_type.split('/').nth(1)
}
pub fn from_resolved(resolved: &ResolvedMediaSpec) -> Self {
MediaSpec {
media_type: resolved.media_type.clone(),
profile: resolved.profile_uri.clone(),
}
}
}
impl fmt::Display for MediaSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.media_type)?;
if let Some(ref profile) = self.profile {
write!(f, "; profile={}", profile)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MediaSpecError {
EmptyMediaType,
InvalidMediaType(String),
UnterminatedQuote,
UnresolvableMediaUrn(String),
}
impl fmt::Display for MediaSpecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MediaSpecError::EmptyMediaType => {
write!(f, "media type cannot be empty")
}
MediaSpecError::InvalidMediaType(msg) => {
write!(f, "invalid media type: {}", msg)
}
MediaSpecError::UnterminatedQuote => {
write!(f, "unterminated quote in profile value")
}
MediaSpecError::UnresolvableMediaUrn(urn) => {
write!(
f,
"cannot resolve media URN '{}' - not found in media_specs and not a built-in primitive",
urn
)
}
}
}
}
impl std::error::Error for MediaSpecError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_with_profile() {
let spec =
MediaSpec::parse("application/json; profile=https://capns.org/schema/obj").unwrap();
assert_eq!(spec.media_type, "application/json");
assert_eq!(
spec.profile,
Some("https://capns.org/schema/obj".to_string())
);
}
#[test]
fn test_parse_text_with_profile() {
let spec = MediaSpec::parse("text/plain; profile=https://capns.org/schema/str").unwrap();
assert_eq!(spec.media_type, "text/plain");
assert_eq!(
spec.profile,
Some("https://capns.org/schema/str".to_string())
);
}
#[test]
fn test_parse_binary() {
let spec = MediaSpec::parse("application/octet-stream").unwrap();
assert_eq!(spec.media_type, "application/octet-stream");
assert!(spec.profile.is_none());
}
#[test]
fn test_parse_image() {
let spec = MediaSpec::parse("image/png; profile=https://example.com/thumbnail").unwrap();
assert_eq!(spec.media_type, "image/png");
}
#[test]
fn test_parse_no_profile() {
let spec = MediaSpec::parse("text/html").unwrap();
assert_eq!(spec.media_type, "text/html");
assert!(spec.profile.is_none());
}
#[test]
fn test_parse_quoted_profile() {
let spec =
MediaSpec::parse("application/json; profile=\"https://example.com/schema\"").unwrap();
assert_eq!(
spec.profile,
Some("https://example.com/schema".to_string())
);
}
#[test]
fn test_invalid_media_type() {
let result = MediaSpec::parse("invalid");
assert!(result.is_err());
if let Err(MediaSpecError::InvalidMediaType(_)) = result {
} else {
panic!("Expected InvalidMediaType error");
}
}
#[test]
fn test_display() {
let spec = MediaSpec {
media_type: "application/json".to_string(),
profile: Some("https://example.com/schema".to_string()),
};
assert_eq!(
spec.to_string(),
"application/json; profile=https://example.com/schema"
);
}
#[test]
fn test_display_no_profile() {
let spec = MediaSpec {
media_type: "text/plain".to_string(),
profile: None,
};
assert_eq!(spec.to_string(), "text/plain");
}
#[test]
fn test_resolve_builtin_str() {
let media_specs = HashMap::new();
let resolved = resolve_media_urn(MEDIA_STRING, &media_specs).unwrap();
assert_eq!(resolved.media_urn, MEDIA_STRING);
assert_eq!(resolved.media_type, "text/plain");
assert_eq!(resolved.profile_uri, Some(PROFILE_STR.to_string()));
assert!(resolved.schema.is_none());
}
#[test]
fn test_resolve_builtin_obj() {
let media_specs = HashMap::new();
let resolved = resolve_media_urn(MEDIA_OBJECT, &media_specs).unwrap();
assert_eq!(resolved.media_type, "application/json");
assert_eq!(resolved.profile_uri, Some(PROFILE_OBJ.to_string()));
}
#[test]
fn test_resolve_builtin_binary() {
let media_specs = HashMap::new();
let resolved = resolve_media_urn(MEDIA_BINARY, &media_specs).unwrap();
assert_eq!(resolved.media_type, "application/octet-stream");
assert!(resolved.profile_uri.is_none());
assert!(resolved.is_binary());
}
#[test]
fn test_resolve_custom_string_form() {
let mut media_specs = HashMap::new();
media_specs.insert(
"media:custom-spec".to_string(),
MediaSpecDef::String("application/json; profile=https://example.com/schema".to_string()),
);
let resolved = resolve_media_urn("media:custom-spec", &media_specs).unwrap();
assert_eq!(resolved.media_urn, "media:custom-spec");
assert_eq!(resolved.media_type, "application/json");
assert_eq!(
resolved.profile_uri,
Some("https://example.com/schema".to_string())
);
assert!(resolved.schema.is_none());
}
#[test]
fn test_resolve_custom_object_form() {
let mut media_specs = HashMap::new();
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
media_specs.insert(
"media:output-spec".to_string(),
MediaSpecDef::Object(MediaSpecDefObject {
media_type: "application/json".to_string(),
profile_uri: "https://example.com/schema/output".to_string(),
schema: Some(schema.clone()),
title: None,
description: None,
validation: None,
metadata: None,
}),
);
let resolved = resolve_media_urn("media:output-spec", &media_specs).unwrap();
assert_eq!(resolved.media_urn, "media:output-spec");
assert_eq!(resolved.media_type, "application/json");
assert_eq!(
resolved.profile_uri,
Some("https://example.com/schema/output".to_string())
);
assert_eq!(resolved.schema, Some(schema));
}
#[test]
fn test_resolve_unresolvable_fails_hard() {
let media_specs = HashMap::new();
let result = resolve_media_urn("media:unknown", &media_specs);
assert!(result.is_err());
if let Err(MediaSpecError::UnresolvableMediaUrn(urn)) = result {
assert_eq!(urn, "media:unknown");
} else {
panic!("Expected UnresolvableMediaUrn error");
}
}
#[test]
fn test_custom_overrides_builtin() {
let mut media_specs = HashMap::new();
media_specs.insert(
MEDIA_STRING.to_string(),
MediaSpecDef::String("application/json; profile=https://custom.example.com/str".to_string()),
);
let resolved = resolve_media_urn(MEDIA_STRING, &media_specs).unwrap();
assert_eq!(resolved.media_type, "application/json");
assert_eq!(
resolved.profile_uri,
Some("https://custom.example.com/str".to_string())
);
}
#[test]
fn test_is_builtin_media_urn() {
assert!(is_builtin_media_urn(MEDIA_STRING));
assert!(is_builtin_media_urn(MEDIA_INTEGER));
assert!(is_builtin_media_urn(MEDIA_BINARY));
assert!(!is_builtin_media_urn("media:custom-spec"));
assert!(!is_builtin_media_urn("random-string"));
}
#[test]
fn test_media_spec_def_string_serialize() {
let def = MediaSpecDef::String("text/plain; profile=https://example.com".to_string());
let json = serde_json::to_string(&def).unwrap();
assert_eq!(json, "\"text/plain; profile=https://example.com\"");
}
#[test]
fn test_media_spec_def_object_serialize() {
let def = MediaSpecDef::Object(MediaSpecDefObject {
media_type: "application/json".to_string(),
profile_uri: "https://example.com/profile".to_string(),
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
});
let json = serde_json::to_string(&def).unwrap();
assert!(json.contains("\"media_type\":\"application/json\""));
assert!(json.contains("\"profile_uri\":\"https://example.com/profile\""));
assert!(!json.contains("\"schema\":"));
assert!(!json.contains("\"title\":"));
assert!(!json.contains("\"description\":"));
assert!(!json.contains("\"validation\":"));
}
#[test]
fn test_media_spec_def_deserialize_string() {
let json = "\"text/plain; profile=https://example.com\"";
let def: MediaSpecDef = serde_json::from_str(json).unwrap();
assert!(matches!(def, MediaSpecDef::String(_)));
}
#[test]
fn test_media_spec_def_deserialize_object() {
let json = r#"{"media_type":"application/json","profile_uri":"https://example.com"}"#;
let def: MediaSpecDef = serde_json::from_str(json).unwrap();
assert!(matches!(def, MediaSpecDef::Object(_)));
}
#[test]
fn test_resolved_is_binary() {
let resolved = ResolvedMediaSpec {
media_urn: "media:raw;binary".to_string(),
media_type: "application/octet-stream".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
};
assert!(resolved.is_binary());
assert!(!resolved.is_json());
}
#[test]
fn test_resolved_is_json() {
let resolved = ResolvedMediaSpec {
media_urn: "media:object;textable;keyed".to_string(),
media_type: "application/json".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
};
assert!(resolved.is_json());
assert!(!resolved.is_binary());
}
#[test]
fn test_resolved_is_text() {
let resolved = ResolvedMediaSpec {
media_urn: "media:string;textable".to_string(),
media_type: "text/plain".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
};
assert!(resolved.is_text());
assert!(!resolved.is_binary());
assert!(!resolved.is_json());
}
}