postproject_core/
metadata_registry.rs1mod catalog;
4
5use crate::{
6 Error, ErrorKind, MetadataProperty, MetadataValue, MetadataValueKind, Result, VocabularyId,
7};
8
9pub use catalog::{
10 DUBLIN_CORE_ELEMENTS_VOCABULARY, EBUCORE_VOCABULARY, IPTC_VMH_JSON_VOCABULARY,
11 METADATA_VOCABULARIES, POSTPROJECT_METADATA_VOCABULARY, XMP_BASIC_VOCABULARY,
12};
13
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
16#[non_exhaustive]
17pub enum MetadataCardinality {
18 Single,
20 Repeatable,
22}
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct MetadataPropertyAlias {
27 profile: &'static str,
28 property: &'static str,
29}
30
31impl MetadataPropertyAlias {
32 #[must_use]
34 pub const fn profile(self) -> &'static str {
35 self.profile
36 }
37
38 #[must_use]
40 pub const fn property(self) -> &'static str {
41 self.property
42 }
43}
44
45#[derive(Clone, Copy, Debug)]
47pub struct MetadataPropertyDefinition {
48 property: &'static str,
49 label: &'static str,
50 description: &'static str,
51 accepted_kinds: &'static [MetadataValueKind],
52 cardinality: MetadataCardinality,
53 aliases: &'static [MetadataPropertyAlias],
54 validator: Option<fn(&MetadataValue) -> bool>,
55}
56
57impl MetadataPropertyDefinition {
58 #[must_use]
60 pub const fn property(self) -> &'static str {
61 self.property
62 }
63
64 #[must_use]
66 pub const fn label(self) -> &'static str {
67 self.label
68 }
69
70 #[must_use]
72 pub const fn description(self) -> &'static str {
73 self.description
74 }
75
76 #[must_use]
78 pub const fn accepted_kinds(self) -> &'static [MetadataValueKind] {
79 self.accepted_kinds
80 }
81
82 #[must_use]
84 pub const fn cardinality(self) -> MetadataCardinality {
85 self.cardinality
86 }
87
88 #[must_use]
90 pub const fn aliases(self) -> &'static [MetadataPropertyAlias] {
91 self.aliases
92 }
93
94 pub fn validate_values(self, values: &[MetadataValue]) -> Result<()> {
101 if self.cardinality == MetadataCardinality::Single && values.len() > 1 {
102 return Err(Error::new(
103 ErrorKind::InvalidArgument,
104 format!(
105 "metadata property {} accepts at most one value",
106 self.property
107 ),
108 ));
109 }
110 if let Some(value) = values.iter().find(|value| {
111 !self.accepted_kinds.contains(&value.kind())
112 || self.validator.is_some_and(|validator| !validator(value))
113 }) {
114 return Err(Error::new(
115 ErrorKind::InvalidArgument,
116 format!(
117 "metadata property {} does not accept value kind {:?}",
118 self.property,
119 value.kind()
120 ),
121 ));
122 }
123 Ok(())
124 }
125}
126
127#[derive(Clone, Copy, Debug)]
129pub struct MetadataVocabularyDefinition {
130 vocabulary: &'static str,
131 label: &'static str,
132 reference: &'static str,
133 properties: &'static [MetadataPropertyDefinition],
134}
135
136impl MetadataVocabularyDefinition {
137 #[must_use]
139 pub const fn vocabulary(self) -> &'static str {
140 self.vocabulary
141 }
142
143 #[must_use]
145 pub const fn label(self) -> &'static str {
146 self.label
147 }
148
149 #[must_use]
151 pub const fn reference(self) -> &'static str {
152 self.reference
153 }
154
155 #[must_use]
157 pub const fn properties(self) -> &'static [MetadataPropertyDefinition] {
158 self.properties
159 }
160}
161
162#[must_use]
164pub fn metadata_vocabulary_definition(
165 vocabulary: &VocabularyId,
166) -> Option<&'static MetadataVocabularyDefinition> {
167 METADATA_VOCABULARIES
168 .iter()
169 .find(|definition| definition.vocabulary == vocabulary.as_str())
170}
171
172#[must_use]
174pub fn metadata_property_definition(
175 property: &MetadataProperty,
176) -> Option<&'static MetadataPropertyDefinition> {
177 metadata_vocabulary_definition(property.vocabulary())?
178 .properties
179 .iter()
180 .find(|definition| definition.property == property.property().as_str())
181}
182
183#[cfg(test)]
184mod tests {
185 use crate::{MetadataProperty, MetadataValue, PropertyId, VocabularyId};
186
187 use super::*;
188
189 fn property(vocabulary: &str, property: &str) -> MetadataProperty {
190 MetadataProperty::new(
191 VocabularyId::new(vocabulary).expect("valid vocabulary"),
192 PropertyId::new(property).expect("valid property"),
193 )
194 }
195
196 #[test]
197 fn vocabulary_lookup_is_exact_and_descriptive() {
198 let vocabulary = VocabularyId::new(IPTC_VMH_JSON_VOCABULARY).unwrap();
199 let definition = metadata_vocabulary_definition(&vocabulary).expect("known vocabulary");
200 assert_eq!(definition.vocabulary(), IPTC_VMH_JSON_VOCABULARY);
201 assert_eq!(definition.label(), "IPTC Video Metadata Hub 1.7 JSON");
202 assert_eq!(definition.reference(), IPTC_VMH_JSON_VOCABULARY);
203 assert_eq!(definition.properties().len(), 2);
204
205 let unknown = VocabularyId::new("https://example.com/metadata").unwrap();
206 assert!(metadata_vocabulary_definition(&unknown).is_none());
207 }
208
209 #[test]
210 fn property_hints_expose_types_cardinality_and_mappings() {
211 let title = property(IPTC_VMH_JSON_VOCABULARY, "title");
212 let definition = metadata_property_definition(&title).expect("known property");
213 assert_eq!(definition.label(), "Title");
214 assert!(!definition.description().is_empty());
215 assert_eq!(definition.cardinality(), MetadataCardinality::Single);
216 assert_eq!(
217 definition.accepted_kinds(),
218 &[MetadataValueKind::String, MetadataValueKind::LangString]
219 );
220 assert!(
221 definition
222 .aliases()
223 .iter()
224 .any(|alias| alias.profile() == "XMP" && alias.property() == "dc:title")
225 );
226
227 assert!(
228 metadata_property_definition(&property(IPTC_VMH_JSON_VOCABULARY, "unknown")).is_none()
229 );
230 }
231
232 #[test]
233 fn validation_is_opt_in_and_checks_the_complete_value_set() {
234 let title = metadata_property_definition(&property(IPTC_VMH_JSON_VOCABULARY, "title"))
235 .expect("known title");
236 let plain = MetadataValue::string("Interview").unwrap();
237 let localized = MetadataValue::language_string("Interview", "en-US").unwrap();
238 assert!(title.validate_values(std::slice::from_ref(&plain)).is_ok());
239 assert!(
240 title
241 .validate_values(std::slice::from_ref(&localized))
242 .is_ok()
243 );
244 assert!(title.validate_values(&[plain, localized]).is_err());
245 assert!(title.validate_values(&[MetadataValue::u64(42)]).is_err());
246
247 let keywords =
248 metadata_property_definition(&property(IPTC_VMH_JSON_VOCABULARY, "keywords"))
249 .expect("known keywords");
250 assert!(
251 keywords
252 .validate_values(&[
253 MetadataValue::string("interview").unwrap(),
254 MetadataValue::string("studio").unwrap(),
255 ])
256 .is_ok()
257 );
258 }
259
260 #[test]
261 fn built_in_validation_callbacks_can_add_property_rules() {
262 let definition = MetadataPropertyDefinition {
263 property: "example",
264 label: "Example",
265 description: "Test-only callback coverage.",
266 accepted_kinds: &[MetadataValueKind::String],
267 cardinality: MetadataCardinality::Single,
268 aliases: &[],
269 validator: Some(|value| value.as_string() == Some("accepted")),
270 };
271 assert!(
272 definition
273 .validate_values(&[MetadataValue::string("accepted").unwrap()])
274 .is_ok()
275 );
276 assert!(
277 definition
278 .validate_values(&[MetadataValue::string("rejected").unwrap()])
279 .is_err()
280 );
281 }
282}