1use prism::pipeline::{ShapeViolation, ViolationKind};
38
39use crate::json::{JsonValue, JsonValueRef};
40
41const PHOTO_SCHEMA_VIOLATION: ShapeViolation = ShapeViolation {
44 shape_iri: "https://schema.org/Photograph",
45 constraint_iri: "https://schema.org/Photograph/schemaOrgConformance",
46 property_iri: "https://schema.org/Photograph",
47 expected_range: "https://schema.org/Photograph",
48 min_count: 0,
49 max_count: 1,
50 kind: ViolationKind::ValueCheck,
51};
52
53pub const SCHEMA_ORG_CONTEXTS: &[&[u8]] = &[b"https://schema.org", b"http://schema.org"];
55
56pub const PHOTOGRAPH_TYPE: &[u8] = b"Photograph";
58
59pub const REQUIRED_PROPERTIES: &[&[u8]] = &[b"@context", b"@type", b"contentUrl", b"creator"];
61
62#[derive(Clone)]
65pub struct PhotoValue {
66 inner: JsonValue,
67}
68
69impl core::fmt::Debug for PhotoValue {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 f.debug_struct("PhotoValue").finish_non_exhaustive()
72 }
73}
74
75impl PhotoValue {
76 pub fn parse(raw: &[u8]) -> Result<Self, ShapeViolation> {
79 let inner = JsonValue::parse(raw).map_err(|_| PHOTO_SCHEMA_VIOLATION)?;
80 let root = JsonValueRef::root(&inner);
81 if !root.is_object() {
82 return Err(PHOTO_SCHEMA_VIOLATION);
83 }
84
85 let context = root
87 .get(b"@context")
88 .and_then(|v| v.as_str())
89 .ok_or(PHOTO_SCHEMA_VIOLATION)?;
90 if !SCHEMA_ORG_CONTEXTS.contains(&context) {
91 return Err(PHOTO_SCHEMA_VIOLATION);
92 }
93
94 let ty = root
96 .get(b"@type")
97 .and_then(|v| v.as_str())
98 .ok_or(PHOTO_SCHEMA_VIOLATION)?;
99 if ty != PHOTOGRAPH_TYPE {
100 return Err(PHOTO_SCHEMA_VIOLATION);
101 }
102
103 let _ = root
105 .get(b"contentUrl")
106 .and_then(|v| v.as_str())
107 .ok_or(PHOTO_SCHEMA_VIOLATION)?;
108
109 let creator = root.get(b"creator").ok_or(PHOTO_SCHEMA_VIOLATION)?;
111 if creator.as_str().is_some() {
112 } else if creator.is_object() {
114 let ct = creator
115 .get(b"@type")
116 .and_then(|v| v.as_str())
117 .ok_or(PHOTO_SCHEMA_VIOLATION)?;
118 if ct != b"Person" && ct != b"Organization" {
119 return Err(PHOTO_SCHEMA_VIOLATION);
120 }
121 if creator.get(b"name").and_then(|v| v.as_str()).is_none() {
122 return Err(PHOTO_SCHEMA_VIOLATION);
123 }
124 } else {
125 return Err(PHOTO_SCHEMA_VIOLATION);
126 }
127
128 Ok(Self { inner })
129 }
130
131 #[must_use]
133 pub fn tagged_bytes(&self) -> &[u8] {
134 self.inner.tagged_bytes()
135 }
136}
137
138pub fn address(raw: &[u8]) -> Result<crate::AddressOutcome<71>, AddressFailure> {
143 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
144 crate::json::address(raw).map_err(|e| match e {
145 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
146 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
147 })
148}
149
150pub fn address_blake3(raw: &[u8]) -> Result<crate::AddressOutcome<71>, AddressFailure> {
157 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
158 crate::json::address_blake3(raw).map_err(|e| match e {
159 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
160 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
161 })
162}
163
164pub fn address_sha3_256(raw: &[u8]) -> Result<crate::AddressOutcome<73>, AddressFailure> {
171 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
172 crate::json::address_sha3_256(raw).map_err(|e| match e {
173 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
174 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
175 })
176}
177
178pub fn address_keccak256(raw: &[u8]) -> Result<crate::AddressOutcome<74>, AddressFailure> {
185 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
186 crate::json::address_keccak256(raw).map_err(|e| match e {
187 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
188 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
189 })
190}
191
192pub fn address_sha512(raw: &[u8]) -> Result<crate::AddressOutcome<135, 64>, AddressFailure> {
198 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
199 crate::json::address_sha512(raw).map_err(|e| match e {
200 crate::json::AddressFailure::InvalidJson => AddressFailure::SchemaViolation,
201 crate::json::AddressFailure::PipelineFailure => AddressFailure::PipelineFailure,
202 })
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum AddressFailure {
208 SchemaViolation,
210 PipelineFailure,
212}
213
214#[cfg(feature = "alloc")]
218pub fn canonicalize(raw: &[u8]) -> Result<alloc::vec::Vec<u8>, AddressFailure> {
219 extern crate alloc;
220 PhotoValue::parse(raw).map_err(|_| AddressFailure::SchemaViolation)?;
221 crate::json::canonicalize(raw).map_err(|_| AddressFailure::PipelineFailure)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 const VALID_PHOTO: &[u8] = br#"{
229 "@context": "https://schema.org",
230 "@type": "Photograph",
231 "contentUrl": "https://example.org/photo.jpg",
232 "creator": {"@type": "Person", "name": "Ada Lovelace"}
233 }"#;
234
235 #[test]
236 fn admits_valid_schema_org_photograph() {
237 let p = PhotoValue::parse(VALID_PHOTO).expect("valid");
238 assert!(!p.tagged_bytes().is_empty());
239 }
240
241 #[test]
242 fn admits_string_creator() {
243 let raw = br#"{
244 "@context": "https://schema.org",
245 "@type": "Photograph",
246 "contentUrl": "https://example.org/photo.jpg",
247 "creator": "Ada Lovelace"
248 }"#;
249 let p = PhotoValue::parse(raw).expect("valid");
250 assert!(!p.tagged_bytes().is_empty());
251 }
252
253 #[test]
254 fn admits_http_context() {
255 let raw = br#"{
256 "@context": "http://schema.org",
257 "@type": "Photograph",
258 "contentUrl": "https://example.org/photo.jpg",
259 "creator": "Ada Lovelace"
260 }"#;
261 PhotoValue::parse(raw).expect("valid");
262 }
263
264 #[test]
265 fn rejects_wrong_context() {
266 let raw = br#"{
267 "@context": "https://example.org/custom",
268 "@type": "Photograph",
269 "contentUrl": "https://example.org/photo.jpg",
270 "creator": "Ada Lovelace"
271 }"#;
272 let err = PhotoValue::parse(raw).expect_err("not schema.org");
273 assert_eq!(err.constraint_iri, PHOTO_SCHEMA_VIOLATION.constraint_iri);
274 }
275
276 #[test]
277 fn rejects_wrong_type() {
278 let raw = br#"{
279 "@context": "https://schema.org",
280 "@type": "Article",
281 "contentUrl": "https://example.org/photo.jpg",
282 "creator": "Ada Lovelace"
283 }"#;
284 let err = PhotoValue::parse(raw).expect_err("not Photograph");
285 assert_eq!(err.constraint_iri, PHOTO_SCHEMA_VIOLATION.constraint_iri);
286 }
287
288 #[test]
289 fn rejects_missing_content_url() {
290 let raw = br#"{
291 "@context": "https://schema.org",
292 "@type": "Photograph",
293 "creator": "Ada Lovelace"
294 }"#;
295 let err = PhotoValue::parse(raw).expect_err("missing contentUrl");
296 assert_eq!(err.constraint_iri, PHOTO_SCHEMA_VIOLATION.constraint_iri);
297 }
298
299 #[test]
300 fn rejects_creator_with_unsupported_type() {
301 let raw = br#"{
302 "@context": "https://schema.org",
303 "@type": "Photograph",
304 "contentUrl": "https://example.org/photo.jpg",
305 "creator": {"@type": "Robot", "name": "A.L.I.C.E."}
306 }"#;
307 let err = PhotoValue::parse(raw).expect_err("unsupported creator @type");
308 assert_eq!(err.constraint_iri, PHOTO_SCHEMA_VIOLATION.constraint_iri);
309 }
310
311 #[test]
312 fn address_matches_json_realization_for_admitted_input() {
313 let from_photo = address(VALID_PHOTO).expect("κ-label").address;
314 let from_json = crate::json::address(VALID_PHOTO).expect("κ-label").address;
315 assert_eq!(from_photo, from_json);
316 }
317}