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_DOWNLOAD_OUTPUT,
MEDIA_LIST_OUTPUT, MEDIA_STATUS_OUTPUT, MEDIA_CONTENTS_OUTPUT,
MEDIA_EMBEDDING_VECTOR,
};
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/model-list";
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 MediaValidation {
#[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 MediaValidation {
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,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<MediaValidation>,
#[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>,
title: impl Into<String>,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
title: title.into(),
schema: None,
description: None,
validation: None,
metadata: None,
})
}
pub fn object_with_schema(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
title: impl Into<String>,
schema: serde_json::Value,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
title: title.into(),
schema: Some(schema),
description: None,
validation: None,
metadata: None,
})
}
pub fn object_full(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
title: impl Into<String>,
schema: Option<serde_json::Value>,
description: Option<String>,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
title: title.into(),
schema,
description,
validation: None,
metadata: None,
})
}
pub fn object_with_validation(
media_type: impl Into<String>,
profile_uri: impl Into<String>,
title: impl Into<String>,
validation: MediaValidation,
) -> Self {
MediaSpecDef::Object(MediaSpecDefObject {
media_type: media_type.into(),
profile_uri: profile_uri.into(),
title: title.into(),
schema: 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<MediaValidation>,
pub metadata: Option<serde_json::Value>,
}
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_map(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_map())
.unwrap_or(false)
}
pub fn is_scalar(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_scalar())
.unwrap_or(false)
}
pub fn is_list(&self) -> bool {
crate::MediaUrn::from_string(&self.media_urn)
.map(|urn| urn.is_list())
.unwrap_or(false)
}
pub fn is_structured(&self) -> bool {
self.is_map() || self.is_list()
}
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 async fn resolve_media_urn(
media_urn: &str,
media_specs: Option<&HashMap<String, MediaSpecDef>>,
registry: &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);
}
}
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,
metadata: stored_spec.metadata,
});
}
Err(e) => {
eprintln!(
"[WARN] Media URN '{}' not found in registry: {} - \
ensure it's defined in capns_dot_org/standard/media/",
media_urn, e
);
}
}
Err(MediaSpecError::UnresolvableMediaUrn(format!(
"cannot resolve media URN '{}' - not found in cap's media_specs or registry",
media_urn
)))
}
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, metadata: 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: Some(obj.title.clone()),
description: obj.description.clone(),
validation: obj.validation.clone(), metadata: obj.metadata.clone(), }),
}
}
#[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");
}
async fn test_registry() -> crate::media_registry::MediaUrnRegistry {
crate::media_registry::MediaUrnRegistry::new().await.expect("Failed to create test registry")
}
#[tokio::test]
async fn test_resolve_from_registry_str() {
let registry = test_registry().await;
let resolved = resolve_media_urn(MEDIA_STRING, None, ®istry).await.unwrap();
assert_eq!(resolved.media_urn, MEDIA_STRING);
assert_eq!(resolved.media_type, "text/plain");
assert!(resolved.profile_uri.is_some());
}
#[tokio::test]
async fn test_resolve_from_registry_obj() {
let registry = test_registry().await;
let resolved = resolve_media_urn(MEDIA_OBJECT, None, ®istry).await.unwrap();
assert_eq!(resolved.media_type, "application/json");
}
#[tokio::test]
async fn test_resolve_from_registry_binary() {
let registry = test_registry().await;
let resolved = resolve_media_urn(MEDIA_BINARY, None, ®istry).await.unwrap();
assert_eq!(resolved.media_type, "application/octet-stream");
assert!(resolved.is_binary());
}
#[tokio::test]
async fn test_resolve_custom_string_form() {
let registry = test_registry().await;
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", Some(&media_specs), ®istry).await.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());
}
#[tokio::test]
async fn test_resolve_custom_object_form() {
let registry = test_registry().await;
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(),
title: "Output Spec".to_string(),
schema: Some(schema.clone()),
description: None,
validation: None,
metadata: None,
}),
);
let resolved = resolve_media_urn("media:output-spec", Some(&media_specs), ®istry).await.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));
}
#[tokio::test]
async fn test_resolve_unresolvable_fails_hard() {
let registry = test_registry().await;
let result = resolve_media_urn("media:completely-unknown-urn-not-in-registry", None, ®istry).await;
assert!(result.is_err());
if let Err(MediaSpecError::UnresolvableMediaUrn(msg)) = result {
assert!(msg.contains("media:completely-unknown-urn-not-in-registry"));
} else {
panic!("Expected UnresolvableMediaUrn error");
}
}
#[tokio::test]
async fn test_local_overrides_registry() {
let registry = test_registry().await;
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, Some(&media_specs), ®istry).await.unwrap();
assert_eq!(resolved.media_type, "application/json");
assert_eq!(
resolved.profile_uri,
Some("https://custom.example.com/str".to_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(),
title: "Test Media".to_string(),
schema: 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("\"title\":\"Test Media\""));
assert!(!json.contains("\"schema\":"));
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","title":"Test"}"#;
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:bytes".to_string(),
media_type: "application/octet-stream".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_binary());
assert!(!resolved.is_map());
assert!(!resolved.is_json());
}
#[test]
fn test_resolved_is_map() {
let resolved = ResolvedMediaSpec {
media_urn: "media:textable;form=map".to_string(),
media_type: "application/json".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_map());
assert!(!resolved.is_binary());
assert!(!resolved.is_scalar());
assert!(!resolved.is_list());
}
#[test]
fn test_resolved_is_scalar() {
let resolved = ResolvedMediaSpec {
media_urn: "media:textable;form=scalar".to_string(),
media_type: "text/plain".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_scalar());
assert!(!resolved.is_map());
assert!(!resolved.is_list());
}
#[test]
fn test_resolved_is_list() {
let resolved = ResolvedMediaSpec {
media_urn: "media:textable;form=list".to_string(),
media_type: "application/json".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_list());
assert!(!resolved.is_map());
assert!(!resolved.is_scalar());
}
#[test]
fn test_resolved_is_json() {
let resolved = ResolvedMediaSpec {
media_urn: "media:json;textable;form=map".to_string(),
media_type: "application/json".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_json());
assert!(resolved.is_map()); assert!(!resolved.is_binary());
}
#[test]
fn test_resolved_is_text() {
let resolved = ResolvedMediaSpec {
media_urn: "media:textable".to_string(),
media_type: "text/plain".to_string(),
profile_uri: None,
schema: None,
title: None,
description: None,
validation: None,
metadata: None,
};
assert!(resolved.is_text());
assert!(!resolved.is_binary());
assert!(!resolved.is_json());
}
#[tokio::test]
async fn test_metadata_propagation_from_object_def() {
let registry = test_registry().await;
let mut media_specs = std::collections::HashMap::new();
media_specs.insert(
"media:custom-setting;setting".to_string(),
MediaSpecDef::Object(MediaSpecDefObject {
media_type: "text/plain".to_string(),
profile_uri: "https://example.com/schema".to_string(),
title: "Custom Setting".to_string(),
schema: None,
description: Some("A custom setting".to_string()),
validation: None,
metadata: Some(serde_json::json!({
"category_key": "interface",
"ui_type": "SETTING_UI_TYPE_CHECKBOX",
"subcategory_key": "appearance",
"display_index": 5
})),
}),
);
let resolved = resolve_media_urn("media:custom-setting;setting", Some(&media_specs), ®istry).await.unwrap();
assert!(resolved.metadata.is_some());
let metadata = resolved.metadata.unwrap();
assert_eq!(metadata.get("category_key").unwrap(), "interface");
assert_eq!(metadata.get("ui_type").unwrap(), "SETTING_UI_TYPE_CHECKBOX");
assert_eq!(metadata.get("subcategory_key").unwrap(), "appearance");
assert_eq!(metadata.get("display_index").unwrap(), 5);
}
#[tokio::test]
async fn test_metadata_none_for_string_def() {
let registry = test_registry().await;
let mut media_specs = std::collections::HashMap::new();
media_specs.insert(
"media:simple;textable".to_string(),
MediaSpecDef::String("text/plain; profile=https://example.com".to_string()),
);
let resolved = resolve_media_urn("media:simple;textable", Some(&media_specs), ®istry).await.unwrap();
assert!(resolved.metadata.is_none());
}
#[tokio::test]
async fn test_metadata_with_validation() {
let registry = test_registry().await;
let mut media_specs = std::collections::HashMap::new();
media_specs.insert(
"media:bounded-number;numeric;setting".to_string(),
MediaSpecDef::Object(MediaSpecDefObject {
media_type: "text/plain".to_string(),
profile_uri: "https://example.com/schema".to_string(),
title: "Bounded Number".to_string(),
schema: None,
description: None,
validation: Some(MediaValidation {
min: Some(0.0),
max: Some(100.0),
min_length: None,
max_length: None,
pattern: None,
allowed_values: None,
}),
metadata: Some(serde_json::json!({
"category_key": "inference",
"ui_type": "SETTING_UI_TYPE_SLIDER"
})),
}),
);
let resolved = resolve_media_urn("media:bounded-number;numeric;setting", Some(&media_specs), ®istry).await.unwrap();
assert!(resolved.validation.is_some());
let validation = resolved.validation.unwrap();
assert_eq!(validation.min, Some(0.0));
assert_eq!(validation.max, Some(100.0));
assert!(resolved.metadata.is_some());
let metadata = resolved.metadata.unwrap();
assert_eq!(metadata.get("category_key").unwrap(), "inference");
assert_eq!(metadata.get("ui_type").unwrap(), "SETTING_UI_TYPE_SLIDER");
}
}