schema_org.rs (8914B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::collections::HashMap; 15 16 use serde::{de::DeserializeOwned, Deserialize, Serialize}; 17 use serde_json::{json, Value}; 18 19 use crate::{ 20 assertion::{Assertion, AssertionBase, AssertionJson}, 21 assertions::labels, 22 error::{Error, Result}, 23 hashed_uri::HashedUri, 24 }; 25 26 const ASSERTION_CREATION_VERSION: usize = 1; 27 28 #[derive(Serialize, Deserialize, Clone, Debug)] 29 pub struct SchemaDotOrg { 30 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] 31 object_context: Option<Value>, 32 #[serde(rename = "@type", default = "default_type")] 33 object_type: String, 34 #[serde(flatten)] 35 value: HashMap<String, Value>, 36 } 37 38 // used to set the default @type if it is missing 39 fn default_type() -> String { 40 "Thing".to_string() 41 } 42 43 impl SchemaDotOrg { 44 /// constructs an empty Schema.org object of the specified @type with @context 45 pub fn new(object_type: String) -> Self { 46 Self { 47 object_context: None, 48 object_type, 49 value: HashMap::new(), 50 } 51 } 52 53 /// sets the @context field for Schema dot org. 54 pub fn set_default_context(mut self) -> Self { 55 self.object_context = Some(json!("https://schema.org")); 56 self 57 } 58 59 /// sets the @context field for Schema dot org. 60 pub fn set_context(mut self, context: Value) -> Self { 61 self.object_context = Some(context); 62 self 63 } 64 65 /// return the @type value from the object 66 pub fn object_type(&self) -> &str { 67 self.object_type.as_str() 68 } 69 70 /// get values by key as an instance of type `T`. 71 /// This return T is owned, not a reference 72 /// # Errors 73 /// 74 /// This conversion can fail if the structure of the field at key does not match the 75 /// structure expected by `T` 76 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> { 77 self.value 78 .get(key) 79 .and_then(|v| serde_json::from_value(v.clone()).ok()) 80 } 81 82 /// insert key / value pair of instance of type `T` 83 /// # Errors 84 /// 85 /// This conversion can fail if `T`'s implementation of `Serialize` decides to 86 /// fail, or if `T` contains a map with non-string keys. 87 pub fn insert<T: Serialize>(mut self, key: String, value: T) -> Result<Self> { 88 self.value.insert(key, serde_json::to_value(value)?); 89 Ok(self) 90 } 91 92 // add a value to a Vec stored at key 93 pub fn insert_push<T: Serialize + DeserializeOwned>( 94 self, 95 key: String, 96 value: T, 97 ) -> Result<Self> { 98 Ok(match self.get(&key) as Option<Vec<T>> { 99 Some(mut v) => { 100 v.push(value); 101 self 102 } 103 None => self.insert(key, Vec::from([value]))?, 104 }) 105 } 106 107 /// creates the struct from a correctly formatted JSON string 108 pub fn from_json_str(json: &str) -> Result<Self> { 109 serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError) 110 } 111 } 112 113 impl Default for SchemaDotOrg { 114 fn default() -> Self { 115 Self::new(default_type()) 116 } 117 } 118 119 impl AssertionJson for SchemaDotOrg {} 120 121 impl AssertionBase for SchemaDotOrg { 122 const LABEL: &'static str = labels::SCHEMA_ORG; 123 const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); 124 125 fn to_assertion(&self) -> Result<Assertion> { 126 Self::to_json_assertion(self) 127 } 128 129 fn from_assertion(assertion: &Assertion) -> Result<Self> { 130 Self::from_json_assertion(assertion) 131 } 132 } 133 #[derive(Serialize, Deserialize, Clone, Debug)] 134 pub struct SchemaDotOrgPerson(SchemaDotOrg); 135 136 impl SchemaDotOrgPerson { 137 pub const CREDENTIAL: &'static str = "credential"; 138 pub const IDENTIFIER: &'static str = "identifier"; 139 pub const NAME: &'static str = "name"; 140 pub const PERSON: &'static str = "Person"; 141 142 pub fn new() -> Self { 143 Self(SchemaDotOrg::new(Self::PERSON.to_owned())) 144 } 145 146 pub fn new_person<S: Into<String>>(name: S, identifier: S) -> Result<Self> { 147 Self(SchemaDotOrg::new(Self::PERSON.to_owned())) 148 .set_name(name)? 149 .set_identifier(identifier) 150 } 151 152 /// get values by key 153 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> { 154 self.0.get(key) 155 } 156 157 /// insert key / value pair 158 pub fn insert<S: Into<String>, T: Serialize>(self, key: S, value: T) -> Result<Self> { 159 self.0.insert(key.into(), value).map(Self) 160 } 161 162 // add a value to a Vec stored at key 163 pub fn insert_push<S: Into<String>, T>(self, key: S, value: T) -> Result<Self> 164 where 165 T: Serialize + DeserializeOwned, 166 { 167 self.0.insert_push(key.into(), value).map(Self) 168 } 169 170 // get name field if it exists 171 pub fn name(&self) -> Option<String> { 172 self.get(Self::NAME) 173 } 174 175 pub fn set_name<S: Into<String>>(self, author: S) -> Result<Self> { 176 self.insert(Self::NAME.to_string(), author.into()) 177 } 178 179 // get identifier field if it exists 180 pub fn identifier(&self) -> Option<String> { 181 self.get(Self::IDENTIFIER) 182 } 183 184 pub fn set_identifier<S: Into<String>>(self, identifier: S) -> Result<Self> { 185 self.insert(Self::IDENTIFIER.to_owned(), identifier.into()) 186 } 187 188 pub fn add_credential(self, credential: HashedUri) -> Result<Self> { 189 self.insert_push(Self::CREDENTIAL.to_owned(), credential) 190 } 191 } 192 193 impl Default for SchemaDotOrgPerson { 194 fn default() -> Self { 195 Self::new() 196 } 197 } 198 199 impl std::ops::Deref for SchemaDotOrgPerson { 200 type Target = SchemaDotOrg; 201 202 fn deref(&self) -> &Self::Target { 203 &self.0 204 } 205 } 206 #[cfg(test)] 207 pub mod tests { 208 #![allow(clippy::expect_used)] 209 #![allow(clippy::unwrap_used)] 210 211 use super::*; 212 213 const USER: &str = "Joe Bloggs"; 214 const USER_ID: &str = "1234567890"; 215 const IDENTITY_URI: &str = "https://some_identity/service/"; 216 217 // example review rating from 218 // https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review 219 const RATING: &str = r#"{ 220 "@context": "http://schema.org", 221 "@type": "ClaimReview", 222 "claimReviewed": "The world is flat", 223 "reviewRating": { 224 "@type": "Rating", 225 "ratingValue": "1", 226 "bestRating": "5", 227 "worstRating": "1", 228 "ratingExplanation": "The world is not flat", 229 "alternateName": "False" 230 }, 231 "itemReviewed": { 232 "@type": "CreativeWork", 233 "author": { 234 "@type": "Person", 235 "name": "A N Other" 236 }, 237 "headline": "Earth: Flat." 238 } 239 }"#; 240 241 #[test] 242 fn assertion_creative_work() { 243 let uri = HashedUri::new(USER_ID.to_string(), None, b"abcde"); 244 let original_person = SchemaDotOrgPerson::new() 245 .set_name(USER.to_owned()) 246 .unwrap() 247 .set_identifier(IDENTITY_URI.to_owned()) 248 .unwrap() 249 .add_credential(uri) 250 .unwrap(); 251 let original = SchemaDotOrg::new("CreativeWork".to_owned()) 252 .insert("author".to_owned(), original_person.clone()) 253 .expect("insert"); 254 let assertion = original.to_assertion().expect("build_assertion"); 255 assert_eq!(assertion.mime_type(), "application/json"); 256 assert_eq!(assertion.label(), SchemaDotOrg::LABEL); 257 let result = SchemaDotOrg::from_assertion(&assertion).expect("extract_assertion"); 258 assert_eq!(original.object_type(), result.object_type()); 259 let result_person = result.get::<SchemaDotOrgPerson>("author").unwrap(); 260 assert_eq!(original_person.name(), result_person.name()); 261 } 262 263 #[test] 264 fn from_rating() { 265 let original = SchemaDotOrg::from_json_str(RATING).expect("from_json"); 266 let original_claim_reviewed: String = original.get("claimReviewed").unwrap(); 267 let assertion = original.to_assertion().expect("build_assertion"); 268 assert_eq!(assertion.mime_type(), "application/json"); 269 assert_eq!(assertion.label(), SchemaDotOrg::LABEL); 270 let result = SchemaDotOrg::from_assertion(&assertion).expect("extract_assertion"); 271 assert_eq!(original.object_type(), result.object_type()); 272 let result_claim_reviewed: String = result.get("claimReviewed").unwrap(); 273 assert_eq!(original_claim_reviewed, result_claim_reviewed); 274 } 275 }