1use std::collections::TryReserveError;
2use std::error::Error;
3use std::fmt;
4
5use serde::Serialize;
6
7use crate::diagnostic_registry::DiagnosticKind;
8use crate::{
9 DiagnosticDetails, ErrorCode, ErrorDescriptor, OutputFormat, ReferenceProfileIdentity,
10};
11
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
14#[serde(transparent)]
15pub struct DiagnosticCode(&'static str);
16
17impl DiagnosticCode {
18 pub const NORMALIZATION_MISSING_ELEMENT_ID: Self = Self("normalization.missing-element-id");
20 pub const NORMALIZATION_DUPLICATE_ELEMENT_ID: Self = Self("normalization.duplicate-element-id");
22 pub const NORMALIZATION_MISSING_TIMESTAMP: Self = Self("normalization.missing-timestamp");
24 pub const NORMALIZATION_ZERO_ROUGH_SEED: Self = Self("normalization.zero-rough-seed");
26 pub const NORMALIZATION_RELATIONSHIP_CYCLE: Self = Self("normalization.relationship-cycle");
28 pub const COMPATIBILITY_UNSUPPORTED_ELEMENT_TYPE: Self =
30 Self("compatibility.unsupported-element-type");
31 pub const COMPATIBILITY_MALFORMED_ELEMENT: Self = Self("compatibility.malformed-element");
33 pub const COMPATIBILITY_UNSUPPORTED_PROPERTY: Self = Self("compatibility.unsupported-property");
35 pub const COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE: Self =
37 Self("compatibility.unsupported-property-value");
38 pub const COMPATIBILITY_KNOWN_ELEMENT_REPLACED: Self =
40 Self("compatibility.known-element-replaced");
41 pub const ASSET_SUPPLEMENT_CONFLICT: Self = Self("asset.supplement-conflict");
43 pub const ASSET_MEDIA_TYPE_MISMATCH: Self = Self("asset.media-type-mismatch");
45 pub const ASSET_MALFORMED_FILE_RECORD: Self = Self("asset.malformed-file-record");
47 pub const IMAGE_MISSING_FILE: Self = Self("image.missing-file");
49 pub const IMAGE_FORBIDDEN_RESOURCE: Self = Self("image.forbidden-resource");
51 pub const IMAGE_ANIMATION_FLATTENED: Self = Self("image.animation-flattened");
53 pub const IMAGE_UNSUPPORTED_FORMAT: Self = Self("image.unsupported-format");
55 pub const IMAGE_DECODE_FAILED: Self = Self("image.decode-failed");
57 pub const IMAGE_UNSAFE_SVG_CONTENT_REMOVED: Self = Self("image.unsafe-svg-content-removed");
59 pub const LINK_UNSAFE_OMITTED: Self = Self("link.unsafe-omitted");
61 pub const COLOR_INVALID: Self = Self("color.invalid");
63 pub const FONT_LIBERATION_SANS_SUBSTITUTED: Self = Self("font.liberation-sans-substituted");
65 pub const FONT_NOTO_EMOJI_SUBSTITUTED: Self = Self("font.noto-emoji-substituted");
67 pub const FONT_MISSING_GLYPH: Self = Self("font.missing-glyph");
69 pub const RESOURCE_ELEMENT_COMPLEXITY_REPLACED: Self =
71 Self("resource.element-complexity-replaced");
72 pub const RESOURCE_ELEMENT_WORK_EXCEEDED: Self = Self("resource.element-work-exceeded");
74 pub const RESOURCE_ASSET_BYTES_EXCEEDED: Self = Self("resource.asset-bytes-exceeded");
76 pub const RESOURCE_ASSET_WORK_EXCEEDED: Self = Self("resource.asset-work-exceeded");
78 pub const RESOURCE_IMAGE_DIMENSIONS_EXCEEDED: Self = Self("resource.image-dimensions-exceeded");
80 pub const RESOURCE_IMAGE_PIXELS_EXCEEDED: Self = Self("resource.image-pixels-exceeded");
82 pub const RESOURCE_NESTED_SVG_COMPLEXITY_EXCEEDED: Self =
84 Self("resource.nested-svg-complexity-exceeded");
85 pub const RESOURCE_NESTED_ASSET_DEPTH_EXCEEDED: Self =
87 Self("resource.nested-asset-depth-exceeded");
88 pub const RESOURCE_COORDINATE_RANGE_EXCEEDED: Self = Self("resource.coordinate-range-exceeded");
90 pub const RESOURCE_OPTIONAL_METADATA_OMITTED: Self = Self("resource.optional-metadata-omitted");
92 pub const METADATA_PROVENANCE_SERIALIZATION_FAILED: Self =
94 Self("metadata.provenance-serialization-failed");
95 pub const METADATA_DIAGNOSTIC_SUMMARY_SERIALIZATION_FAILED: Self =
97 Self("metadata.diagnostic-summary-serialization-failed");
98 pub const METADATA_SCENE_SERIALIZATION_FAILED: Self =
100 Self("metadata.scene-serialization-failed");
101 pub const METADATA_SCENE_COMPRESSION_FAILED: Self = Self("metadata.scene-compression-failed");
103 pub const METADATA_SCENE_EMBEDDING_FAILED: Self = Self("metadata.scene-embedding-failed");
105 pub const METADATA_MANIFEST_SERIALIZATION_FAILED: Self =
107 Self("metadata.manifest-serialization-failed");
108 pub const METADATA_MANIFEST_EMBEDDING_FAILED: Self = Self("metadata.manifest-embedding-failed");
110 pub const PDF_RASTER_FALLBACK: Self = Self("pdf.raster-fallback");
112
113 pub(crate) const ALL: [Self; 42] = [
117 Self::NORMALIZATION_MISSING_ELEMENT_ID,
118 Self::NORMALIZATION_DUPLICATE_ELEMENT_ID,
119 Self::NORMALIZATION_MISSING_TIMESTAMP,
120 Self::NORMALIZATION_ZERO_ROUGH_SEED,
121 Self::NORMALIZATION_RELATIONSHIP_CYCLE,
122 Self::COMPATIBILITY_UNSUPPORTED_ELEMENT_TYPE,
123 Self::COMPATIBILITY_MALFORMED_ELEMENT,
124 Self::COMPATIBILITY_UNSUPPORTED_PROPERTY,
125 Self::COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE,
126 Self::COMPATIBILITY_KNOWN_ELEMENT_REPLACED,
127 Self::ASSET_SUPPLEMENT_CONFLICT,
128 Self::ASSET_MEDIA_TYPE_MISMATCH,
129 Self::ASSET_MALFORMED_FILE_RECORD,
130 Self::IMAGE_MISSING_FILE,
131 Self::IMAGE_FORBIDDEN_RESOURCE,
132 Self::IMAGE_ANIMATION_FLATTENED,
133 Self::IMAGE_UNSUPPORTED_FORMAT,
134 Self::IMAGE_DECODE_FAILED,
135 Self::IMAGE_UNSAFE_SVG_CONTENT_REMOVED,
136 Self::LINK_UNSAFE_OMITTED,
137 Self::COLOR_INVALID,
138 Self::FONT_LIBERATION_SANS_SUBSTITUTED,
139 Self::FONT_NOTO_EMOJI_SUBSTITUTED,
140 Self::FONT_MISSING_GLYPH,
141 Self::RESOURCE_ELEMENT_COMPLEXITY_REPLACED,
142 Self::RESOURCE_ELEMENT_WORK_EXCEEDED,
143 Self::RESOURCE_ASSET_BYTES_EXCEEDED,
144 Self::RESOURCE_ASSET_WORK_EXCEEDED,
145 Self::RESOURCE_IMAGE_DIMENSIONS_EXCEEDED,
146 Self::RESOURCE_IMAGE_PIXELS_EXCEEDED,
147 Self::RESOURCE_NESTED_SVG_COMPLEXITY_EXCEEDED,
148 Self::RESOURCE_NESTED_ASSET_DEPTH_EXCEEDED,
149 Self::RESOURCE_COORDINATE_RANGE_EXCEEDED,
150 Self::RESOURCE_OPTIONAL_METADATA_OMITTED,
151 Self::METADATA_PROVENANCE_SERIALIZATION_FAILED,
152 Self::METADATA_DIAGNOSTIC_SUMMARY_SERIALIZATION_FAILED,
153 Self::METADATA_SCENE_SERIALIZATION_FAILED,
154 Self::METADATA_SCENE_COMPRESSION_FAILED,
155 Self::METADATA_SCENE_EMBEDDING_FAILED,
156 Self::METADATA_MANIFEST_SERIALIZATION_FAILED,
157 Self::METADATA_MANIFEST_EMBEDDING_FAILED,
158 Self::PDF_RASTER_FALLBACK,
159 ];
160
161 pub(crate) fn from_stable_str(code: &str) -> Option<Self> {
166 Self::ALL.into_iter().find(|known| known.as_str() == code)
167 }
168
169 pub const fn as_str(self) -> &'static str {
171 self.0
172 }
173}
174
175impl fmt::Display for DiagnosticCode {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 formatter.write_str(self.as_str())
178 }
179}
180
181#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
185#[non_exhaustive]
186#[serde(rename_all = "snake_case")]
187pub enum DiagnosticSeverity {
188 Info,
190 Warning,
192}
193
194impl DiagnosticSeverity {
195 pub const fn as_str(self) -> &'static str {
197 match self {
198 Self::Info => "info",
199 Self::Warning => "warning",
200 }
201 }
202}
203
204impl fmt::Display for DiagnosticSeverity {
205 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206 formatter.write_str(self.as_str())
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
212#[non_exhaustive]
213#[serde(rename_all = "snake_case")]
214pub enum DiagnosticEffect {
215 InputRepair,
217 Visual,
219 Semantic,
221 Metadata,
223}
224
225impl DiagnosticEffect {
226 pub const fn as_str(self) -> &'static str {
228 match self {
229 Self::InputRepair => "input_repair",
230 Self::Visual => "visual",
231 Self::Semantic => "semantic",
232 Self::Metadata => "metadata",
233 }
234 }
235
236 pub(crate) fn from_manifest(effect: &str) -> Option<Self> {
238 match effect {
239 "input_repair" => Some(Self::InputRepair),
240 "visual" => Some(Self::Visual),
241 "semantic" => Some(Self::Semantic),
242 "metadata" => Some(Self::Metadata),
243 _ => None,
244 }
245 }
246}
247
248impl fmt::Display for DiagnosticEffect {
249 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
250 formatter.write_str(self.as_str())
251 }
252}
253
254#[derive(Clone, Debug, PartialEq)]
261pub(crate) struct PendingDiagnostic {
262 code: DiagnosticCode,
263 severity: DiagnosticSeverity,
264 format: OutputFormat,
265 effects: Vec<DiagnosticEffect>,
266 element_id: Option<String>,
267 json_pointer: String,
268 message: String,
269 details: Option<Box<DiagnosticDetails>>,
270}
271
272fn set_identity_effects(
273 effects: &mut Vec<DiagnosticEffect>,
274 format: OutputFormat,
275 visual: bool,
276) -> Result<(), TryReserveError> {
277 effects.try_reserve(3)?;
278 effects.clear();
279 effects.push(DiagnosticEffect::InputRepair);
280 if visual {
281 effects.push(DiagnosticEffect::Visual);
282 if format != OutputFormat::Png {
283 effects.push(DiagnosticEffect::Semantic);
284 }
285 }
286 Ok(())
287}
288
289impl PendingDiagnostic {
290 pub(crate) fn omit_unbounded_known_element_fallback(&mut self) -> Result<(), TryReserveError> {
291 if self.code != DiagnosticCode::COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE {
292 return Ok(());
293 }
294 if let Some(details) = self.details.as_deref_mut() {
295 if matches!(details, DiagnosticDetails::Compatibility { recovery:crate::Recovery::Placeholder, recipe_id:Some(recipe) }
296 if recipe==crate::diagnostic_registry::FallbackRecipe::KnownElementUnavailable.id())
297 {
298 *details = DiagnosticKind::UnsupportedPropertyValue {
299 recovery: crate::Recovery::Omitted,
300 recipe: None,
301 }
302 .into_details()?;
303 }
304 }
305 Ok(())
306 }
307 pub(crate) fn set_identity_effects(&mut self, visual: bool) -> Result<(), TryReserveError> {
308 set_identity_effects(&mut self.effects, self.format, visual)
309 }
310 pub(crate) fn new(
314 kind: DiagnosticKind<'_>,
315 format: OutputFormat,
316 effects: &[DiagnosticEffect],
317 element_id: Option<&str>,
318 json_pointer: &str,
319 message: &str,
320 ) -> Result<Self, TryReserveError> {
321 let mut owned_effects = Vec::new();
322 owned_effects.try_reserve_exact(effects.len())?;
323 owned_effects.extend_from_slice(effects);
324 Ok(Self {
325 code: kind.code(),
326 severity: kind.severity(),
327 format,
328 effects: owned_effects,
329 element_id: element_id.map(try_owned).transpose()?,
330 json_pointer: try_owned(json_pointer)?,
331 message: try_owned(message)?,
332 details: Some(Box::new(kind.into_details()?)),
333 })
334 }
335
336 pub(crate) const fn code(&self) -> DiagnosticCode {
337 self.code
338 }
339
340 pub(crate) fn element_id(&self) -> Option<&str> {
341 self.element_id.as_deref()
342 }
343
344 pub(crate) fn json_pointer(&self) -> &str {
345 &self.json_pointer
346 }
347
348 pub(crate) fn scope_malformed_image_recovery(
351 &mut self,
352 error_status: impl FnOnce(&str) -> bool,
353 ) -> Result<(), TryReserveError> {
354 if self.code != DiagnosticCode::ASSET_MALFORMED_FILE_RECORD {
355 return Ok(());
356 }
357 if let Some(DiagnosticDetails::Asset { asset_id, .. }) = self.details.as_deref() {
358 let kind = DiagnosticKind::ImageUnavailable {
359 failure: crate::diagnostic_registry::ImageFailure::MalformedFileRecord,
360 asset_id,
361 recovery: crate::diagnostic_registry::ImageRecovery::for_format(
362 self.format,
363 error_status(asset_id),
364 ),
365 };
366 self.details = Some(Box::new(kind.into_details()?));
367 }
368 Ok(())
369 }
370
371 pub(crate) fn scope_forbidden_resource_recovery(
372 &mut self,
373 has_viewport: impl FnOnce(&str) -> bool,
374 ) -> Result<(), TryReserveError> {
375 if self.code != DiagnosticCode::IMAGE_FORBIDDEN_RESOURCE {
376 return Ok(());
377 }
378 if let Some(details) = self.details.as_deref_mut() {
379 if let DiagnosticDetails::Asset { asset_id, .. } = details {
380 let kind = DiagnosticKind::ForbiddenResource {
381 asset_id,
382 recovery: crate::diagnostic_registry::LocalRecovery::placeholder_if(
383 has_viewport(asset_id),
384 ),
385 };
386 *details = kind.into_details()?;
387 }
388 }
389 Ok(())
390 }
391
392 #[cfg(test)]
393 pub(crate) fn effects(&self) -> &[DiagnosticEffect] {
394 &self.effects
395 }
396
397 #[cfg(test)]
398 pub(crate) fn details(&self) -> Option<&DiagnosticDetails> {
399 self.details.as_deref()
400 }
401
402 pub(crate) fn into_location(self) -> (Option<String>, String) {
405 (self.element_id, self.json_pointer)
406 }
407
408 pub(crate) fn retained(self, order: DiagnosticOrder) -> CompatibilityDiagnostic {
410 CompatibilityDiagnostic {
411 code: self.code,
412 severity: self.severity,
413 format: self.format,
414 effects: self.effects,
415 element_id: self.element_id,
416 json_pointer: self.json_pointer,
417 message: self.message,
418 details: self.details,
419 order,
420 }
421 }
422}
423
424fn try_owned(value: &str) -> Result<String, TryReserveError> {
425 let mut owned = String::new();
426 owned.try_reserve_exact(value.len())?;
427 owned.push_str(value);
428 Ok(owned)
429}
430
431#[derive(Clone, Debug, PartialEq, Serialize)]
433pub struct CompatibilityDiagnostic {
434 code: DiagnosticCode,
435 severity: DiagnosticSeverity,
436 format: OutputFormat,
437 effects: Vec<DiagnosticEffect>,
438 element_id: Option<String>,
439 json_pointer: String,
440 message: String,
441 details: Option<Box<DiagnosticDetails>>,
442 #[serde(skip)]
443 order: DiagnosticOrder,
444}
445
446#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
452pub(crate) struct DiagnosticOrder {
453 pub(crate) owner_index: u64,
454 pub(crate) source_position: u64,
455 pub(crate) retention_ordinal: u64,
456 pub(crate) attribution: DiagnosticAttribution,
457}
458
459#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
460pub(crate) enum DiagnosticAttribution {
461 SceneGlobal,
462 Target,
463}
464
465impl DiagnosticOrder {
466 pub(crate) fn retained(
472 owner_index: Option<u64>,
473 source_position: Option<u64>,
474 retention_ordinal: u64,
475 attribution: DiagnosticAttribution,
476 ) -> Self {
477 Self {
478 owner_index: owner_index.unwrap_or(u64::MAX),
479 source_position: source_position.unwrap_or(u64::MAX),
480 retention_ordinal,
481 attribution,
482 }
483 }
484}
485
486impl CompatibilityDiagnostic {
487 pub(crate) fn omit_surface_fallback(&mut self, has_link: bool) -> Result<(), TryReserveError> {
489 if let Some(DiagnosticDetails::Compatibility {
490 recovery,
491 recipe_id,
492 }) = self.details.as_deref_mut()
493 {
494 if *recovery == crate::Recovery::Placeholder {
495 if self.code == DiagnosticCode::COMPATIBILITY_MALFORMED_ELEMENT
496 && has_link
497 && self.format != OutputFormat::Png
498 && !self.effects.contains(&DiagnosticEffect::Semantic)
499 {
500 self.effects.try_reserve(1)?;
501 self.effects.push(DiagnosticEffect::Semantic);
502 }
503 *recovery = crate::Recovery::Omitted;
504 *recipe_id = None;
505 }
506 }
507 Ok(())
508 }
509 pub(crate) fn set_forbidden_resource_recovery(
510 &mut self,
511 paints: bool,
512 ) -> Result<(), TryReserveError> {
513 if self.code == DiagnosticCode::IMAGE_FORBIDDEN_RESOURCE {
514 if let Some(details) = self.details.as_deref_mut() {
515 if let DiagnosticDetails::Asset { asset_id, .. } = details {
516 *details = DiagnosticKind::ForbiddenResource {
517 asset_id,
518 recovery: crate::diagnostic_registry::LocalRecovery::placeholder_if(paints),
519 }
520 .into_details()?;
521 }
522 }
523 }
524 Ok(())
525 }
526 pub(crate) fn set_identity_effects(&mut self, visual: bool) -> Result<(), TryReserveError> {
527 set_identity_effects(&mut self.effects, self.format, visual)
528 }
529 pub const fn code(&self) -> DiagnosticCode {
531 self.code
532 }
533
534 pub const fn severity(&self) -> DiagnosticSeverity {
536 self.severity
537 }
538
539 pub const fn format(&self) -> OutputFormat {
541 self.format
542 }
543
544 pub fn effects(&self) -> &[DiagnosticEffect] {
546 &self.effects
547 }
548
549 pub fn element_id(&self) -> Option<&str> {
551 self.element_id.as_deref()
552 }
553
554 pub fn json_pointer(&self) -> &str {
556 &self.json_pointer
557 }
558
559 pub fn message(&self) -> &str {
561 &self.message
562 }
563
564 pub fn details(&self) -> Option<&DiagnosticDetails> {
566 self.details.as_deref()
567 }
568
569 pub(crate) fn details_mut(&mut self) -> Option<&mut DiagnosticDetails> {
573 self.details.as_deref_mut()
574 }
575
576 pub(crate) const fn order(&self) -> DiagnosticOrder {
578 self.order
579 }
580
581 pub(crate) fn attribute_to_target(&mut self) {
584 self.order.attribution = DiagnosticAttribution::Target;
585 }
586
587 pub(crate) fn replaces_occurrence(&self) -> bool {
590 self.effects.contains(&DiagnosticEffect::Visual)
591 && matches!(
592 self.details().and_then(DiagnosticDetails::recovery),
593 Some(crate::Recovery::Placeholder | crate::Recovery::Omitted)
594 )
595 }
596}
597
598impl fmt::Display for CompatibilityDiagnostic {
599 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
602 write!(
603 formatter,
604 "{}: {} [{}]",
605 self.severity, self.message, self.code
606 )?;
607 if let Some(element_id) = &self.element_id {
608 write!(formatter, " (element {element_id})")?;
609 }
610 Ok(())
611 }
612}
613
614#[derive(Clone, Debug, PartialEq, Serialize)]
616pub struct CompatibilityReport {
617 pub(crate) schema_version: u32,
618 pub(crate) diagnostics: Vec<CompatibilityDiagnostic>,
619}
620
621impl CompatibilityReport {
622 pub const fn empty() -> Self {
624 Self {
625 schema_version: 1,
626 diagnostics: Vec::new(),
627 }
628 }
629
630 pub const fn schema_version(&self) -> u32 {
632 self.schema_version
633 }
634
635 pub fn diagnostics(&self) -> &[CompatibilityDiagnostic] {
637 &self.diagnostics
638 }
639
640 pub fn is_empty(&self) -> bool {
642 self.diagnostics.is_empty()
643 }
644
645 pub fn len(&self) -> usize {
647 self.diagnostics.len()
648 }
649}
650
651impl Default for CompatibilityReport {
652 fn default() -> Self {
653 Self::empty()
654 }
655}
656
657#[derive(Clone, Debug, Eq, PartialEq)]
659pub struct Artifact {
660 pub(crate) format: OutputFormat,
661 pub(crate) bytes: Vec<u8>,
662}
663
664impl Artifact {
665 pub const fn format(&self) -> OutputFormat {
667 self.format
668 }
669
670 pub const fn media_type(&self) -> &'static str {
672 self.format.media_type()
673 }
674
675 pub fn bytes(&self) -> &[u8] {
677 &self.bytes
678 }
679
680 pub fn into_bytes(self) -> Vec<u8> {
682 self.bytes
683 }
684}
685
686#[derive(Clone, Debug)]
688pub struct RenderOutcome {
689 pub(crate) artifact: Artifact,
690 pub(crate) report: CompatibilityReport,
691 pub(crate) profile: &'static ReferenceProfileIdentity,
692}
693
694impl RenderOutcome {
695 pub const fn artifact(&self) -> &Artifact {
697 &self.artifact
698 }
699
700 pub const fn report(&self) -> &CompatibilityReport {
702 &self.report
703 }
704
705 pub fn into_parts(self) -> (Artifact, CompatibilityReport) {
707 (self.artifact, self.report)
708 }
709
710 #[doc(hidden)]
713 pub const fn profile_identity(&self) -> &'static ReferenceProfileIdentity {
714 self.profile
715 }
716}
717
718#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
720#[non_exhaustive]
721#[serde(rename_all = "snake_case")]
722pub enum RenderErrorCategory {
723 Input,
725 Capability,
727 ResourceLimit,
729 NoMeaningfulArtifact,
731 Encoding,
733 Internal,
735}
736
737impl RenderErrorCategory {
738 pub const fn as_str(self) -> &'static str {
740 match self {
741 Self::Input => "input",
742 Self::Capability => "capability",
743 Self::ResourceLimit => "resource_limit",
744 Self::NoMeaningfulArtifact => "no_meaningful_artifact",
745 Self::Encoding => "encoding",
746 Self::Internal => "internal",
747 }
748 }
749}
750
751impl fmt::Display for RenderErrorCategory {
752 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
753 formatter.write_str(self.as_str())
754 }
755}
756
757#[derive(Clone, Debug)]
762pub struct RenderError {
763 pub(crate) category: RenderErrorCategory,
764 pub(crate) code: ErrorCode,
765 pub(crate) message: Box<str>,
766 pub(crate) format: OutputFormat,
767 pub(crate) element_id: Option<String>,
768 pub(crate) json_pointer: Box<str>,
769 pub(crate) details: Option<Box<DiagnosticDetails>>,
770 pub(crate) report: CompatibilityReport,
771}
772
773impl RenderError {
774 pub const fn category(&self) -> RenderErrorCategory {
776 self.category
777 }
778
779 pub const fn code(&self) -> ErrorCode {
781 self.code
782 }
783
784 pub const fn format(&self) -> OutputFormat {
786 self.format
787 }
788
789 pub fn element_id(&self) -> Option<&str> {
791 self.element_id.as_deref()
792 }
793
794 pub fn json_pointer(&self) -> &str {
796 &self.json_pointer
797 }
798
799 pub fn details(&self) -> Option<&DiagnosticDetails> {
801 self.details.as_deref()
802 }
803
804 pub const fn report(&self) -> &CompatibilityReport {
806 &self.report
807 }
808
809 pub fn descriptor(&self) -> ErrorDescriptor {
811 ErrorDescriptor::render(
812 self.category,
813 self.code,
814 self.to_string(),
815 self.format,
816 self.element_id.clone(),
817 self.json_pointer.to_string(),
818 self.details.as_deref().cloned(),
819 )
820 }
821}
822
823impl fmt::Display for RenderError {
824 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
827 formatter.write_str(&self.message)?;
828 if let Some(crossing) = self.details().and_then(DiagnosticDetails::limit_crossing) {
829 write!(formatter, ": {crossing}")?;
830 }
831 Ok(())
832 }
833}
834
835impl Error for RenderError {}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840 use crate::diagnostic_registry::{ImageFailure, ImageRecovery, LocalRecovery};
841 use crate::{Renderer, ResourceMeasurement, ResourceValue, ValueKind};
842
843 fn retained_first(pending: PendingDiagnostic) -> CompatibilityDiagnostic {
845 pending.retained(DiagnosticOrder::retained(
846 None,
847 None,
848 0,
849 DiagnosticAttribution::SceneGlobal,
850 ))
851 }
852
853 #[test]
854 fn owned_outcome_and_render_failure_expose_only_complete_contract_values() {
855 let diagnostic = retained_first(
856 PendingDiagnostic::new(
857 DiagnosticKind::MalformedElement(LocalRecovery::Placeholder),
858 OutputFormat::Svg,
859 &[DiagnosticEffect::Visual],
860 Some("element-1"),
861 "/elements/0",
862 "malformed element replaced",
863 )
864 .expect("the diagnostic allocates"),
865 );
866 let report = CompatibilityReport {
867 schema_version: 1,
868 diagnostics: vec![diagnostic],
869 };
870 let profile = Renderer::new().profile_identity();
871 let outcome = RenderOutcome {
872 artifact: Artifact {
873 format: OutputFormat::Svg,
874 bytes: b"<svg/>".to_vec(),
875 },
876 report: report.clone(),
877 profile,
878 };
879
880 assert_eq!(outcome.artifact().bytes(), b"<svg/>");
881 assert_eq!(outcome.artifact().media_type(), "image/svg+xml");
882 assert_eq!(outcome.report(), &report);
883 assert!(std::ptr::eq(outcome.profile_identity(), profile));
884 assert_eq!(
885 serde_json::to_string(outcome.report()).expect("serialize diagnosed report"),
886 concat!(
887 "{\"schema_version\":1,\"diagnostics\":[{",
888 "\"code\":\"compatibility.malformed-element\",",
889 "\"severity\":\"warning\",",
890 "\"format\":\"svg\",",
891 "\"effects\":[\"visual\"],",
892 "\"element_id\":\"element-1\",",
893 "\"json_pointer\":\"/elements/0\",",
894 "\"message\":\"malformed element replaced\",",
895 "\"details\":{\"kind\":\"compatibility\",",
896 "\"recovery\":\"placeholder\",",
897 "\"recipe_id\":\"renderer.malformed-element.v1\"}}]}"
898 )
899 );
900 let (artifact, returned_report) = outcome.into_parts();
901 assert_eq!(artifact.into_bytes(), b"<svg/>");
902 assert_eq!(returned_report, report);
903
904 let error = RenderError {
905 category: RenderErrorCategory::ResourceLimit,
906 code: ErrorCode::RESOURCE_OUTPUT_BYTES_EXCEEDED,
907 message: "output byte limit exceeded".into(),
908 format: OutputFormat::Svg,
909 element_id: None,
910 json_pointer: Box::from(""),
911 details: Some(Box::new(DiagnosticDetails::ResourceLimit {
912 field: "max_output_bytes",
913 configured: ResourceValue::Integer(134_217_728),
914 value_kind: ValueKind::Bytes,
915 measurement: ResourceMeasurement::Attempted {
916 value: ResourceValue::Integer(134_217_729),
917 },
918 recovery: None,
919 })),
920 report: returned_report,
921 };
922 assert_eq!(error.report().len(), 1);
923 assert_eq!(error.category(), RenderErrorCategory::ResourceLimit);
924 assert_eq!(
925 serde_json::to_string(&error.descriptor()).expect("serialize render descriptor"),
926 concat!(
927 "{\"family\":\"render\",",
928 "\"category\":\"resource_limit\",",
929 "\"code\":\"resource.output-bytes-exceeded\",",
930 "\"message\":\"output byte limit exceeded: needs 134217729 bytes, ",
931 "but max_output_bytes is 134217728\",",
932 "\"format\":\"svg\",",
933 "\"element_id\":null,",
934 "\"json_pointer\":\"\",",
935 "\"details\":{\"kind\":\"resource_limit\",",
936 "\"field\":\"max_output_bytes\",",
937 "\"configured\":134217728,",
938 "\"value_kind\":\"bytes\",",
939 "\"measurement\":{\"kind\":\"attempted\",\"value\":134217729},",
940 "\"recovery\":null}}"
941 )
942 );
943 }
944
945 #[test]
946 fn report_preserves_every_diagnostic_in_supplied_canonical_order() {
947 let diagnostic = |kind, pointer: &str| {
948 retained_first(
949 PendingDiagnostic::new(
950 kind,
951 OutputFormat::Png,
952 &[DiagnosticEffect::Visual],
953 None,
954 pointer,
955 "diagnosed",
956 )
957 .expect("the diagnostic allocates"),
958 )
959 };
960 let report = CompatibilityReport {
961 schema_version: 1,
962 diagnostics: vec![
963 diagnostic(DiagnosticKind::InvalidColor, "/elements/0/strokeColor"),
964 diagnostic(
965 DiagnosticKind::ImageUnavailable {
966 failure: ImageFailure::DecodeFailed {
967 media_type: "image/png",
968 },
969 asset_id: "image",
970 recovery: ImageRecovery::for_format(OutputFormat::Png, false),
971 },
972 "/files/image/dataURL",
973 ),
974 ],
975 };
976
977 assert_eq!(report.len(), 2);
978 assert_eq!(
979 report.diagnostics()[0].code(),
980 DiagnosticCode::COLOR_INVALID
981 );
982 assert_eq!(
983 report.diagnostics()[1].code(),
984 DiagnosticCode::IMAGE_DECODE_FAILED
985 );
986 }
987}