use std::collections::BTreeMap;
use std::sync::Arc;
use crate::{ErrorCategory, ErrorCode, PixelFlowError, Result};
const CORE_KEYS: [(&str, MetadataKind); 10] = [
("core:matrix", MetadataKind::String),
("core:transfer", MetadataKind::String),
("core:primaries", MetadataKind::String),
("core:range", MetadataKind::String),
("core:chroma_siting", MetadataKind::String),
("core:field_order", MetadataKind::String),
("core:frame_number", MetadataKind::Int),
("core:duration", MetadataKind::Rational),
("core:timecode", MetadataKind::String),
("core:source_path", MetadataKind::String),
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Rational {
pub numerator: i64,
pub denominator: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum MetadataValue {
None,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Array(Vec<MetadataValue>),
Rational(Rational),
Blob(Arc<[u8]>),
}
impl MetadataValue {
#[must_use]
pub const fn kind(&self) -> Option<MetadataKind> {
match self {
Self::None => None,
Self::Bool(_) => Some(MetadataKind::Bool),
Self::Int(_) => Some(MetadataKind::Int),
Self::Float(_) => Some(MetadataKind::Float),
Self::String(_) => Some(MetadataKind::String),
Self::Array(_) => Some(MetadataKind::Array),
Self::Rational(_) => Some(MetadataKind::Rational),
Self::Blob(_) => Some(MetadataKind::Blob),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MetadataKind {
Bool,
Int,
Float,
String,
Array,
Rational,
Blob,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataSchema {
core: BTreeMap<String, MetadataKind>,
plugin: BTreeMap<String, MetadataKind>,
}
impl MetadataSchema {
#[must_use]
pub fn core() -> Self {
let core = CORE_KEYS
.into_iter()
.map(|(key, kind)| (key.to_owned(), kind))
.collect();
Self {
core,
plugin: BTreeMap::new(),
}
}
pub fn register_plugin_key(&mut self, key: &str, kind: MetadataKind) -> Result<()> {
if !is_plugin_key(key) {
return Err(PixelFlowError::new(
ErrorCategory::Plugin,
ErrorCode::new("metadata.invalid_plugin_key"),
format!("invalid plugin metadata key '{key}'"),
));
}
self.plugin.insert(key.to_owned(), kind);
Ok(())
}
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
self.kind_for(key).is_some()
}
#[must_use]
pub fn kind(&self, key: &str) -> Option<MetadataKind> {
self.kind_for(key).map(|(kind, _is_core)| kind)
}
#[must_use]
pub fn is_core_key(&self, key: &str) -> bool {
self.core.contains_key(key)
}
pub fn validate_value(&self, key: &str, value: &MetadataValue) -> Result<()> {
let Some((expected_kind, is_core)) = self.kind_for(key) else {
return Err(PixelFlowError::new(
ErrorCategory::Plugin,
ErrorCode::new("metadata.unregistered_key"),
format!("metadata key '{key}' is not registered"),
));
};
if let Some(actual_kind) = value.kind()
&& actual_kind != expected_kind
{
let category = if is_core {
ErrorCategory::Core
} else {
ErrorCategory::Plugin
};
return Err(PixelFlowError::new(
category,
ErrorCode::new("metadata.type_mismatch"),
format!(
"metadata key '{key}' expects {:?}, got {:?}",
expected_kind, actual_kind
),
));
}
Ok(())
}
pub(crate) fn kind_for(&self, key: &str) -> Option<(MetadataKind, bool)> {
if let Some(kind) = self.core.get(key).copied() {
return Some((kind, true));
}
self.plugin.get(key).copied().map(|kind| (kind, false))
}
pub(crate) fn core_keys(&self) -> impl Iterator<Item = &str> {
self.core.keys().map(String::as_str)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Metadata {
values: BTreeMap<String, MetadataValue>,
}
impl Metadata {
#[must_use]
pub fn new(schema: &MetadataSchema) -> Self {
let values = schema
.core_keys()
.map(|key| (key.to_owned(), MetadataValue::None))
.collect();
Self { values }
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&MetadataValue> {
self.values.get(key)
}
pub fn clear(&mut self, schema: &MetadataSchema, key: &str) -> Result<()> {
self.set(schema, key, MetadataValue::None)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> + '_ {
self.values.iter().map(|(key, value)| (key.as_str(), value))
}
pub fn set(&mut self, schema: &MetadataSchema, key: &str, value: MetadataValue) -> Result<()> {
schema.validate_value(key, &value)?;
self.values.insert(key.to_owned(), value);
Ok(())
}
}
fn is_plugin_key(key: &str) -> bool {
let Some((namespace, field)) = key.split_once(':') else {
return false;
};
let Some((publisher, plugin)) = namespace.split_once('/') else {
return false;
};
is_key_component(publisher) && is_key_component(plugin) && is_key_component(field)
}
fn is_key_component(component: &str) -> bool {
!component.is_empty()
&& component
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}
#[cfg(test)]
mod tests {
use crate::{ErrorCategory, ErrorCode};
use super::{Metadata, MetadataKind, MetadataSchema, MetadataValue, Rational};
#[test]
fn core_metadata_keys_are_always_present_as_none() {
let schema = MetadataSchema::core();
let metadata = Metadata::new(&schema);
for key in [
"core:matrix",
"core:transfer",
"core:primaries",
"core:range",
"core:chroma_siting",
"core:field_order",
"core:frame_number",
"core:duration",
"core:timecode",
"core:source_path",
] {
assert_eq!(metadata.get(key), Some(&MetadataValue::None));
}
}
#[test]
fn plugin_metadata_write_requires_registered_key() {
let schema = MetadataSchema::core();
let mut metadata = Metadata::new(&schema);
let error = metadata
.set(&schema, "acme/filter:strength", MetadataValue::Float(0.5))
.expect_err("unregistered plugin key should fail");
assert_eq!(error.category(), ErrorCategory::Plugin);
assert_eq!(error.code(), ErrorCode::new("metadata.unregistered_key"));
}
#[test]
fn plugin_metadata_write_accepts_registered_key_and_type() {
let mut schema = MetadataSchema::core();
schema
.register_plugin_key("acme/filter:strength", MetadataKind::Float)
.expect("plugin key should register");
let mut metadata = Metadata::new(&schema);
metadata
.set(&schema, "acme/filter:strength", MetadataValue::Float(0.5))
.expect("registered key should accept matching value");
assert_eq!(
metadata.get("acme/filter:strength"),
Some(&MetadataValue::Float(0.5))
);
}
#[test]
fn mismatched_metadata_type_returns_structured_error() {
let schema = MetadataSchema::core();
let mut metadata = Metadata::new(&schema);
let error = metadata
.set(
&schema,
"core:frame_number",
MetadataValue::String("zero".to_owned()),
)
.expect_err("wrong type should fail");
assert_eq!(error.category(), ErrorCategory::Core);
assert_eq!(error.code(), ErrorCode::new("metadata.type_mismatch"));
}
#[test]
fn metadata_supports_rational_array_and_blob_values() {
let mut schema = MetadataSchema::core();
schema
.register_plugin_key("acme/filter:ratios", MetadataKind::Array)
.expect("array key should register");
schema
.register_plugin_key("acme/filter:payload", MetadataKind::Blob)
.expect("blob key should register");
let mut metadata = Metadata::new(&schema);
metadata
.set(
&schema,
"acme/filter:ratios",
MetadataValue::Array(vec![MetadataValue::Rational(Rational {
numerator: 1,
denominator: 2,
})]),
)
.expect("array value should be accepted");
metadata
.set(
&schema,
"acme/filter:payload",
MetadataValue::Blob(vec![1_u8, 2, 3].into()),
)
.expect("blob value should be accepted");
assert!(matches!(
metadata.get("acme/filter:ratios"),
Some(MetadataValue::Array(_))
));
assert!(matches!(
metadata.get("acme/filter:payload"),
Some(MetadataValue::Blob(_))
));
}
#[test]
fn metadata_schema_exposes_registered_kind_and_namespace() {
let mut schema = MetadataSchema::core();
schema
.register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
.expect("plugin key should register");
assert_eq!(schema.kind("core:frame_number"), Some(MetadataKind::Int));
assert_eq!(schema.kind("acme/filter:enabled"), Some(MetadataKind::Bool));
assert!(schema.is_core_key("core:frame_number"));
assert!(!schema.is_core_key("acme/filter:enabled"));
assert_eq!(schema.kind("missing"), None);
}
#[test]
fn metadata_clear_sets_registered_key_to_none() {
let mut schema = MetadataSchema::core();
schema
.register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
.expect("plugin key should register");
let mut metadata = Metadata::new(&schema);
metadata
.set(&schema, "acme/filter:enabled", MetadataValue::Bool(true))
.expect("registered key should set");
metadata
.clear(&schema, "acme/filter:enabled")
.expect("registered key should clear");
assert_eq!(
metadata.get("acme/filter:enabled"),
Some(&MetadataValue::None)
);
}
#[test]
fn metadata_iter_is_deterministic_and_sorted() {
let mut schema = MetadataSchema::core();
schema
.register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
.expect("plugin key should register");
let mut metadata = Metadata::new(&schema);
metadata
.set(&schema, "acme/filter:enabled", MetadataValue::Bool(true))
.expect("registered key should set");
let keys = metadata.iter().map(|(key, _value)| key).collect::<Vec<_>>();
assert_eq!(keys.first().copied(), Some("acme/filter:enabled"));
assert!(
keys.windows(2)
.all(|pair| matches!(pair, [left, right] if left < right))
);
}
}