use std::collections::HashMap;
use chrono::{SecondsFormat, Utc};
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::{labels, region_of_interest::RegionOfInterest},
error::Result,
hashed_uri::HashedUri,
utils::cbor_types::DateT,
};
const ASSERTION_CREATION_VERSION: usize = 1;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct AssertionMetadata {
#[serde(rename = "reviewRatings", skip_serializing_if = "Option::is_none")]
reviews: Option<Vec<ReviewRating>>,
#[serde(rename = "dateTime", skip_serializing_if = "Option::is_none")]
date_time: Option<DateT>,
#[serde(skip_serializing_if = "Option::is_none")]
reference: Option<HashedUri>,
#[serde(rename = "dataSource", skip_serializing_if = "Option::is_none")]
data_source: Option<DataSource>,
#[serde(skip_serializing_if = "Option::is_none")]
localizations: Option<Vec<HashMap<String, HashMap<String, String>>>>, #[serde(rename = "regionOfInterest", skip_serializing_if = "Option::is_none")]
region_of_interest: Option<RegionOfInterest>,
#[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
additional_fields: HashMap<String, Value>,
}
impl AssertionMetadata {
pub const LABEL: &'static str = labels::ASSERTION_METADATA;
pub fn new() -> Self {
let date_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
Self {
reviews: None,
date_time: Some(DateT(date_time)),
reference: None,
data_source: None,
localizations: None,
region_of_interest: None,
additional_fields: HashMap::new(),
}
}
pub fn reviews(&self) -> Option<&[ReviewRating]> {
self.reviews.as_deref()
}
pub fn date_time(&self) -> Option<&str> {
self.date_time.as_deref()
}
pub fn localizations(&self) -> Option<&Vec<HashMap<String, HashMap<String, String>>>> {
self.localizations.as_ref()
}
pub fn data_source(&self) -> Option<&DataSource> {
self.data_source.as_ref()
}
pub fn region_of_interest(&self) -> Option<&RegionOfInterest> {
self.region_of_interest.as_ref()
}
pub fn add_review(mut self, review: ReviewRating) -> Self {
match &mut self.reviews {
None => self.reviews = Some(vec![review]),
Some(reviews) => reviews.push(review),
}
self
}
pub fn set_reviews(mut self, reviews: Vec<ReviewRating>) -> Self {
self.reviews = Some(reviews);
self
}
pub fn set_date_time(mut self, date_time: String) -> Self {
self.date_time = Some(DateT(date_time));
self
}
#[cfg(test)] pub(crate) fn set_reference(mut self, reference: HashedUri) -> Self {
self.reference = Some(reference);
self
}
pub fn set_data_source(mut self, data_source: DataSource) -> Self {
self.data_source = Some(data_source);
self
}
pub fn set_region_of_interest(mut self, region_of_interest: RegionOfInterest) -> Self {
self.region_of_interest = Some(region_of_interest);
self
}
pub fn set_localizations(
mut self,
localizations: Vec<HashMap<String, HashMap<String, String>>>,
) -> Self {
self.localizations = Some(localizations);
self
}
pub fn set_field<S: Into<String>>(mut self, key: S, value: Value) -> Self {
self.additional_fields.insert(key.into(), value);
self
}
pub fn get_field(&self, key: &str) -> Option<&Value> {
self.additional_fields.get(key)
}
pub fn additional_fields(&self) -> &HashMap<String, Value> {
&self.additional_fields
}
pub fn set_additional_fields(mut self, fields: HashMap<String, Value>) -> Self {
self.additional_fields = fields;
self
}
}
impl Default for AssertionMetadata {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for AssertionMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&serde_json::to_string_pretty(self).unwrap_or_default())
}
}
impl AssertionCbor for AssertionMetadata {}
impl AssertionBase for AssertionMetadata {
const LABEL: &'static str = Self::LABEL;
const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);
fn to_assertion(&self) -> Result<Assertion> {
Self::to_cbor_assertion(self)
}
fn from_assertion(assertion: &Assertion) -> Result<Self> {
Self::from_cbor_assertion(assertion)
}
}
pub mod c2pa_source {
pub const SIGNER: &str = "signer";
pub const GENERATOR_REE: &str = "claimGenerator.REE";
pub const GENERATOR_TEE: &str = "claimGenerator.TEE";
pub const LOCAL_REE: &str = "localProvider.REE";
pub const LOCAL_TEE: &str = "localProvider.TEE";
pub const REMOTE_REE: &str = "remoteProvider.1stParty";
pub const REMOTE_TEE: &str = "remoteProvider.3rdParty";
pub const HUMAN_ANONYMOUS: &str = "humanEntry.anonymous";
pub const HUMAN_IDENTIFIED: &str = "humanEntry.identified";
}
#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[non_exhaustive]
pub struct DataSource {
#[serde(rename = "type")]
pub source_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actors: Option<Vec<Actor>>,
}
impl DataSource {
pub fn new(source_type: &str) -> Self {
Self {
source_type: source_type.to_owned(),
details: None,
actors: None,
}
}
pub fn set_details(mut self, details: String) -> Self {
self.details = Some(details);
self
}
pub fn set_actors(mut self, actors: Option<Vec<Actor>>) -> Self {
self.actors = actors;
self
}
}
#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[non_exhaustive]
pub struct Actor {
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<Vec<HashedUri>>,
}
impl Actor {
pub fn new(identifier: Option<&str>, credentials: Option<&Vec<HashedUri>>) -> Self {
Self {
identifier: identifier.map(|id| id.to_owned()),
credentials: credentials.cloned(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ReviewCode {
#[serde(rename(serialize = "actions.unknownActionsPerformed"))]
ActionsUnknown,
#[serde(rename(serialize = "actions.missing"))]
ActionsMissing,
#[serde(rename(serialize = "actions.possiblyMissing"))]
ActionsPossiblyMissing,
#[serde(rename(serialize = "depthMap.sceneMismatch"))]
DepthMapSceneMismatch,
#[serde(rename(serialize = "ingredient.modified"))]
IngredientModified,
#[serde(rename(serialize = "ingredient.possiblyModified"))]
IngredientPossiblyModified,
#[serde(rename(serialize = "thumbnail.primaryMismatch"))]
ThumbnailPrimaryMismatch,
#[serde(rename(serialize = "stds.iptc.location.inaccurate"))]
IptcLocationInaccurate,
#[serde(rename(serialize = "stds.schema-org.CreativeWork.misattributed"))]
CreativeWorkMisAttributed,
#[serde(rename(serialize = "stds.schema-org.CreativeWork.missingAttribution"))]
CreativeWorkMissingAttribution,
Other(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct ReviewRating {
pub explanation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub value: u8,
}
impl ReviewRating {
pub fn new(explanation: &str, code: Option<String>, value: u8) -> Self {
Self {
explanation: explanation.to_owned(),
value, code,
}
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct AssetType {
#[serde(rename = "type")]
pub asset_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
impl AssetType {
pub fn new<S: Into<String>>(asset_type: S, version: Option<String>) -> Self {
AssetType {
asset_type: asset_type.into(),
version,
}
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct DataBox {
#[serde(rename = "dc:format")]
pub format: String,
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_types: Option<Vec<AssetType>>,
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::assertions::region_of_interest::{Range, RangeType, Time, TimeType};
#[test]
fn assertion_metadata() {
let review = ReviewRating::new("foo", Some("bar".to_owned()), 3);
let mut translations = HashMap::new();
translations.insert("en-US".to_owned(), "Kevin's Five Cats".to_owned());
translations.insert("es-MX".to_owned(), "Los Cinco Gatos de Kevin".to_owned());
let mut localizations = HashMap::new();
localizations.insert("dc:title".to_owned(), translations);
let original = AssertionMetadata::new()
.add_review(review)
.set_localizations(vec![localizations])
.set_region_of_interest(RegionOfInterest {
region: vec![Range {
range_type: RangeType::Temporal,
time: Some(Time {
time_type: TimeType::Npt,
start: None,
end: None,
}),
..Default::default()
}],
..Default::default()
});
println!("{:}", &original);
let assertion = original.to_assertion().expect("build_assertion");
assert_eq!(assertion.mime_type(), "application/cbor");
assert_eq!(assertion.label(), AssertionMetadata::LABEL);
let result = AssertionMetadata::from_assertion(&assertion).expect("extract_assertion");
println!("{:?}", serde_json::to_string(&result));
assert_eq!(original.date_time, result.date_time);
assert_eq!(original.reviews, result.reviews);
let localizations = result.localizations.as_ref().unwrap();
assert_eq!(
localizations[0]
.get("dc:title")
.unwrap()
.get("en-US")
.unwrap(),
"Kevin's Five Cats"
);
assert_eq!(
original.region_of_interest.as_ref(),
result.region_of_interest()
);
let assertion = original.to_assertion().expect("build_assertion");
let result = AssertionMetadata::from_assertion(&assertion).expect("extract_assertion");
assert_eq!(original.localizations, result.localizations);
assert_eq!(original.reviews.unwrap().len(), 1);
}
#[test]
fn test_arbitrary_key_value_pairs() {
use serde_json::json;
let original = AssertionMetadata::new()
.set_field("customString", json!("test value"))
.set_field("customNumber", json!(42))
.set_field("customBool", json!(true))
.set_field("customObject", json!({"nested": "value", "count": 123}))
.set_field("customArray", json!(["item1", "item2", "item3"]));
assert_eq!(
original.get_field("customString"),
Some(&json!("test value"))
);
assert_eq!(original.get_field("customNumber"), Some(&json!(42)));
assert_eq!(original.get_field("customBool"), Some(&json!(true)));
assert_eq!(
original.get_field("customObject"),
Some(&json!({"nested": "value", "count": 123}))
);
assert_eq!(original.get_field("nonexistent"), None);
assert_eq!(original.additional_fields().len(), 5);
assert!(original.additional_fields().contains_key("customString"));
let assertion = original.to_assertion().expect("build_assertion");
let result = AssertionMetadata::from_assertion(&assertion).expect("extract_assertion");
assert_eq!(result.get_field("customString"), Some(&json!("test value")));
assert_eq!(result.get_field("customNumber"), Some(&json!(42)));
assert_eq!(result.get_field("customBool"), Some(&json!(true)));
assert_eq!(
result.get_field("customObject"),
Some(&json!({"nested": "value", "count": 123}))
);
assert_eq!(
result.get_field("customArray"),
Some(&json!(["item1", "item2", "item3"]))
);
assert_eq!(result.additional_fields().len(), 5);
}
#[test]
fn test_set_additional_fields() {
use serde_json::json;
let mut fields = HashMap::new();
fields.insert("field1".to_owned(), json!("value1"));
fields.insert("field2".to_owned(), json!(100));
fields.insert("field3".to_owned(), json!({"key": "val"}));
let metadata = AssertionMetadata::new().set_additional_fields(fields.clone());
assert_eq!(metadata.additional_fields(), &fields);
assert_eq!(metadata.get_field("field1"), Some(&json!("value1")));
assert_eq!(metadata.get_field("field2"), Some(&json!(100)));
}
#[test]
fn test_arbitrary_fields_with_standard_fields() {
use serde_json::json;
let review = ReviewRating::new("test review", Some("test.code".to_owned()), 3);
let original = AssertionMetadata::new()
.add_review(review)
.set_date_time("2021-06-28T16:49:32.874Z".to_owned())
.set_field("customField1", json!("custom value 1"))
.set_field("customField2", json!({"nested": true}));
let assertion = original.to_assertion().expect("build_assertion");
let result = AssertionMetadata::from_assertion(&assertion).expect("extract_assertion");
assert!(result.reviews().is_some());
assert_eq!(result.reviews().unwrap().len(), 1);
assert_eq!(result.date_time(), Some("2021-06-28T16:49:32.874Z"));
assert_eq!(
result.get_field("customField1"),
Some(&json!("custom value 1"))
);
assert_eq!(
result.get_field("customField2"),
Some(&json!({"nested": true}))
);
}
#[test]
fn test_empty_additional_fields() {
let original = AssertionMetadata::new();
assert_eq!(original.additional_fields().len(), 0);
assert_eq!(original.get_field("anything"), None);
let assertion = original.to_assertion().expect("build_assertion");
let result = AssertionMetadata::from_assertion(&assertion).expect("extract_assertion");
assert_eq!(result.additional_fields().len(), 0);
}
#[test]
fn test_cbor_serialization_with_arbitrary_fields() {
use serde_json::json;
let original = AssertionMetadata::new()
.set_field("stringField", json!("test"))
.set_field("numberField", json!(42.5))
.set_field("boolField", json!(false))
.set_field("nullField", json!(null))
.set_field("arrayField", json!([1, 2, 3]))
.set_field("objectField", json!({"a": 1, "b": "two"}));
let cbor_bytes = c2pa_cbor::to_vec(&original).expect("serialize to CBOR");
let result: AssertionMetadata =
c2pa_cbor::from_slice(&cbor_bytes).expect("deserialize from CBOR");
assert_eq!(result.get_field("stringField"), Some(&json!("test")));
assert_eq!(result.get_field("numberField"), Some(&json!(42.5)));
assert_eq!(result.get_field("boolField"), Some(&json!(false)));
assert_eq!(result.get_field("nullField"), Some(&json!(null)));
assert_eq!(result.get_field("arrayField"), Some(&json!([1, 2, 3])));
assert_eq!(
result.get_field("objectField"),
Some(&json!({"a": 1, "b": "two"}))
);
}
}