1#![deny(missing_docs)]
11#![forbid(unsafe_code)]
12
13use boxology_schema::{
14 Diagnostic, Diagnostics, ExposureLevel, Idempotency, SchemaCapability, SchemaDataField,
15 SchemaDataShape, SchemaDataType, SchemaDataVariant, SchemaDocument, SchemaField, SchemaPayload,
16 SchemaType, SchemaVariant, TypeExpression,
17};
18use std::collections::BTreeMap;
19
20#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
22pub enum Class {
23 Unchanged,
25 Documentation,
27 Deprecation,
29 Additive,
31 CompatibleWithConditions,
33 Incompatible,
35}
36
37impl Class {
38 pub fn canonical_name(self) -> &'static str {
40 match self {
41 Self::Unchanged => "unchanged",
42 Self::Documentation => "documentation",
43 Self::Deprecation => "deprecation",
44 Self::Additive => "additive",
45 Self::CompatibleWithConditions => "compatible_with_conditions",
46 Self::Incompatible => "incompatible",
47 }
48 }
49}
50
51#[derive(Debug, Eq, PartialEq)]
53pub struct Finding {
54 code: &'static str,
55 path: String,
56 kind: &'static str,
57 class: Class,
58 base_excerpt: Option<String>,
59 submitted_excerpt: Option<String>,
60 condition: Option<&'static str>,
61}
62
63impl Finding {
64 pub fn code(&self) -> &'static str {
66 self.code
67 }
68
69 pub fn path(&self) -> &str {
71 &self.path
72 }
73
74 pub fn kind(&self) -> &'static str {
76 self.kind
77 }
78
79 pub fn class(&self) -> Class {
81 self.class
82 }
83
84 pub fn base_excerpt(&self) -> Option<&str> {
86 self.base_excerpt.as_deref()
87 }
88
89 pub fn submitted_excerpt(&self) -> Option<&str> {
91 self.submitted_excerpt.as_deref()
92 }
93
94 pub fn condition(&self) -> Option<&'static str> {
96 self.condition
97 }
98}
99
100#[derive(Debug, Eq, PartialEq)]
102pub struct ClassificationReport {
103 findings: Vec<Finding>,
104 verdict: Class,
105}
106
107impl ClassificationReport {
108 pub fn findings(&self) -> &[Finding] {
110 &self.findings
111 }
112
113 pub fn verdict(&self) -> Class {
115 self.verdict
116 }
117}
118
119pub fn classify(
121 base: Option<&SchemaDocument>,
122 submitted: Option<&SchemaDocument>,
123) -> Result<ClassificationReport, Diagnostics> {
124 match (base, submitted) {
125 (None, None) => {
126 Err(
127 Diagnostics::new(Vec::from([Diagnostic::classification_requires_document()]))
128 .expect("one classification diagnostic"),
129 )
130 }
131 (None, Some(document)) => Ok(report(Vec::from([Finding {
132 code: "BXC0026",
133 path: document.box_id.as_str().to_owned(),
134 kind: KIND_CONTRACT_INTRODUCED,
135 class: Class::Additive,
136 base_excerpt: None,
137 submitted_excerpt: Some(document.box_id.as_str().to_owned()),
138 condition: None,
139 }]))),
140 (Some(document), None) => Ok(report(Vec::from([Finding {
141 code: "BXC0027",
142 path: document.box_id.as_str().to_owned(),
143 kind: KIND_CONTRACT_REMOVED,
144 class: Class::Incompatible,
145 base_excerpt: Some(document.box_id.as_str().to_owned()),
146 submitted_excerpt: None,
147 condition: None,
148 }]))),
149 (Some(base), Some(submitted)) if base.box_id != submitted.box_id => {
150 Err(Diagnostics::new(Vec::from([Diagnostic::box_id_mismatch()]))
151 .expect("one classification diagnostic"))
152 }
153 (Some(base), Some(submitted)) if equal_modulo_provenance(base, submitted) => {
154 Ok(report(Vec::new()))
155 }
156 (Some(base), Some(submitted)) if base.revision == submitted.revision => {
161 Err(Diagnostics::new(Vec::from([
162 Diagnostic::integrity_findings_under_equal_revisions(),
163 ]))
164 .expect("one integrity diagnostic"))
165 }
166 (Some(base), Some(submitted)) => {
167 let findings = classify_paired_documents(base, submitted);
168 if findings.is_empty() {
174 Err(Diagnostics::new(Vec::from([
175 Diagnostic::integrity_silence_under_differing_revisions(),
176 ]))
177 .expect("one integrity diagnostic"))
178 } else {
179 Ok(report(findings))
180 }
181 }
182 }
183}
184
185const CODE_FAIL_CLOSED: &str = "BXC0028";
187
188const CODE_TYPE_ADDED: &str = "BXC0031";
190
191const CODE_TYPE_REMOVED: &str = "BXC0032";
193
194const CODE_DOCS_CHANGED: &str = "BXC0033";
196
197const CODE_DEPRECATION_CHANGED: &str = "BXC0034";
199
200const CODE_VARIANT_REMOVED: &str = "BXC0035";
202
203const CODE_VARIANT_ADDED: &str = "BXC0036";
205
206const CODE_CAPABILITY_ADDED: &str = "BXC0039";
208
209const CODE_CAPABILITY_REMOVED: &str = "BXC0040";
211
212const CODE_INPUT_NAME_CHANGED: &str = "BXC0041";
214
215const CODE_INPUT_LEAF_CHANGED: &str = "BXC0042";
217
218const CODE_OUTPUT_LEAF_CHANGED: &str = "BXC0043";
220
221const CODE_ERROR_CHANGED: &str = "BXC0044";
223
224const CODE_EXPOSURE_RAISED: &str = "BXC0045";
226
227const CODE_EXPOSURE_LOWERED: &str = "BXC0046";
229
230const CODE_IDEMPOTENCY_STRENGTHENED: &str = "BXC0047";
232
233const CODE_IDEMPOTENCY_WEAKENED: &str = "BXC0048";
235
236const CODE_FIELD_ADDED: &str = "BXC0049";
238
239const CODE_FIELD_REMOVED: &str = "BXC0050";
241
242const CODE_FIELD_TYPE_CHANGED: &str = "BXC0051";
244
245const CODE_PAYLOAD_CHANGED: &str = "BXC0052";
247
248const CODE_TYPE_KIND_CHANGED: &str = "BXC0063";
250const CODE_OUTPUT_FIELD_ADDED: &str = "BXC0064";
252const CODE_OUTPUT_FIELD_REMOVED: &str = "BXC0065";
254const CODE_OUTPUT_FIELD_TYPE_CHANGED: &str = "BXC0066";
256
257const CODE_INPUT_VARIANT_ADDED: &str = "BXC0067";
259
260const CODE_OUTPUT_VARIANT_ADDED: &str = "BXC0068";
262
263const CODE_OUTPUT_VARIANT_REMOVED: &str = "BXC0069";
265
266const KIND_CONTRACT_INTRODUCED: &str = "contract introduced";
267const KIND_CONTRACT_REMOVED: &str = "contract removed";
268const KIND_UNCLASSIFIED: &str = "unclassified change";
269const KIND_TYPE_ADDED: &str = "type added";
270const KIND_TYPE_REMOVED: &str = "type removed";
271const KIND_DOCS_CHANGED: &str = "documentation changed";
272const KIND_DEPRECATION_CHANGED: &str = "deprecation changed";
273const KIND_VARIANT_REMOVED: &str = "variant removed";
274const KIND_VARIANT_ADDED: &str = "error variant added";
275const KIND_CAPABILITY_ADDED: &str = "capability added";
276const KIND_CAPABILITY_REMOVED: &str = "capability removed";
277const KIND_INPUT_NAME_CHANGED: &str = "capability input parameter name changed";
278const KIND_INPUT_LEAF_CHANGED: &str = "capability input type changed";
279const KIND_OUTPUT_LEAF_CHANGED: &str = "capability output type changed";
280const KIND_ERROR_CHANGED: &str = "capability declared error changed";
281const KIND_EXPOSURE_RAISED: &str = "max exposure raised";
282const KIND_EXPOSURE_LOWERED: &str = "max exposure lowered";
283const KIND_IDEMPOTENCY_STRENGTHENED: &str = "idempotency strengthened";
284const KIND_IDEMPOTENCY_WEAKENED: &str = "idempotency weakened";
285const KIND_FIELD_ADDED: &str = "field added";
286const KIND_FIELD_REMOVED: &str = "field removed";
287const KIND_FIELD_TYPE_CHANGED: &str = "field type changed";
288const KIND_PAYLOAD_CHANGED: &str = "error payload changed";
289const KIND_TYPE_KIND_CHANGED: &str = "type kind changed";
290const KIND_ENUM_VARIANT_ADDED: &str = "enum variant added";
291
292const CONDITION_UNKNOWN_VARIANT: &str = "unknown-variant tolerance";
294const CONDITION_PROVIDER_FIRST: &str = "provider-first deployment order";
296
297fn fail_closed_finding(base: &SchemaDocument) -> Finding {
298 Finding {
299 code: CODE_FAIL_CLOSED,
300 path: base.box_id.as_str().to_owned(),
301 kind: KIND_UNCLASSIFIED,
302 class: Class::Incompatible,
303 base_excerpt: None,
304 submitted_excerpt: None,
305 condition: None,
306 }
307}
308
309fn data_finding(
310 code: &'static str,
311 path: String,
312 kind: &'static str,
313 class: Class,
314 base_excerpt: Option<String>,
315 submitted_excerpt: Option<String>,
316 condition: Option<&'static str>,
317) -> Finding {
318 Finding {
319 code,
320 path,
321 kind,
322 class,
323 base_excerpt,
324 submitted_excerpt,
325 condition,
326 }
327}
328
329fn docs_excerpt(docs: &[String]) -> String {
330 docs.join("\n")
331}
332
333fn exposure_excerpt(level: ExposureLevel) -> &'static str {
334 match level {
335 ExposureLevel::CodeOnly => "code_only",
336 ExposureLevel::Internal => "internal",
337 ExposureLevel::External => "external",
338 }
339}
340
341fn idempotency_excerpt(value: Idempotency) -> &'static str {
342 match value {
343 Idempotency::None => "none",
344 Idempotency::Inherent => "inherent",
345 }
346}
347
348fn payload_kind_excerpt(payload: &SchemaPayload) -> &'static str {
349 match payload {
350 SchemaPayload::Unit => "unit",
351 SchemaPayload::Value { .. } => "value",
352 SchemaPayload::Named(_) => "named",
353 }
354}
355
356fn capability_named<'a>(document: &'a SchemaDocument, name: &str) -> Option<&'a SchemaCapability> {
357 document
358 .capabilities
359 .iter()
360 .find(|c| c.name.as_str() == name)
361}
362
363fn type_named<'a>(document: &'a SchemaDocument, name: &str) -> Option<&'a SchemaType> {
364 document.types.iter().find(|t| t.name == name)
365}
366
367fn variant_named<'a>(schema_type: &'a SchemaType, name: &str) -> Option<&'a SchemaVariant> {
368 schema_type.variants.iter().find(|v| v.name == name)
369}
370
371fn field_named<'a>(variant: &'a SchemaVariant, name: &str) -> Option<&'a SchemaField> {
372 match &variant.payload {
373 SchemaPayload::Named(fields) => fields.iter().find(|f| f.name == name),
374 _ => None,
375 }
376}
377
378fn data_type_named<'a>(document: &'a SchemaDocument, name: &str) -> Option<&'a SchemaDataType> {
379 document.data_types.iter().find(|item| item.name == name)
380}
381
382fn data_field_named<'a>(data_type: &'a SchemaDataType, name: &str) -> Option<&'a SchemaDataField> {
383 match &data_type.shape {
384 SchemaDataShape::Struct(fields) => fields.iter().find(|item| item.name == name),
385 SchemaDataShape::Enum(_) => None,
386 }
387}
388
389fn data_variant_named<'a>(
390 data_type: &'a SchemaDataType,
391 name: &str,
392) -> Option<&'a SchemaDataVariant> {
393 match &data_type.shape {
394 SchemaDataShape::Enum(variants) => variants.iter().find(|item| item.name == name),
395 SchemaDataShape::Struct(_) => None,
396 }
397}
398
399fn data_shape_excerpt(shape: &SchemaDataShape) -> &'static str {
400 match shape {
401 SchemaDataShape::Struct(_) => "struct",
402 SchemaDataShape::Enum(_) => "enum",
403 }
404}
405
406fn declaration<'a>(
407 document: &'a SchemaDocument,
408 name: &str,
409) -> Option<(&'static str, &'a [String], Option<&'a str>)> {
410 if let Some(item) = type_named(document, name) {
411 return Some(("error", &item.docs, item.deprecation.as_deref()));
412 }
413 data_type_named(document, name).map(|item| {
414 (
415 data_shape_excerpt(&item.shape),
416 item.docs.as_slice(),
417 item.deprecation.as_deref(),
418 )
419 })
420}
421
422fn is_top_level_optional(ty: &TypeExpression) -> bool {
423 let TypeExpression::Option(_) = ty else {
424 return false;
425 };
426 true
427}
428
429fn classify_paired_documents(base: &SchemaDocument, submitted: &SchemaDocument) -> Vec<Finding> {
442 let roles = reachability(base, submitted);
443 let changes = type_changes(base, submitted, &roles);
444 let mut findings = Vec::new();
445 let mut unclassified = append_data_findings(base, submitted, &roles, &mut findings);
446 for change in &changes {
447 match change {
448 TypeChange::TypeAdded { name, roles } if roles.output => {
449 findings.push(Finding {
450 code: CODE_TYPE_ADDED,
451 path: type_path(base, name),
452 kind: KIND_TYPE_ADDED,
453 class: Class::Additive,
454 base_excerpt: None,
455 submitted_excerpt: Some(name.clone()),
456 condition: None,
457 });
458 }
459 TypeChange::TypeRemoved { name, .. } => {
460 findings.push(Finding {
461 code: CODE_TYPE_REMOVED,
462 path: type_path(base, name),
463 kind: KIND_TYPE_REMOVED,
464 class: Class::Incompatible,
465 base_excerpt: Some(name.clone()),
466 submitted_excerpt: None,
467 condition: None,
468 });
469 }
470 TypeChange::TypeDocsChanged { name } => {
471 findings.push(Finding {
472 code: CODE_DOCS_CHANGED,
473 path: type_path(base, name),
474 kind: KIND_DOCS_CHANGED,
475 class: Class::Documentation,
476 base_excerpt: type_named(base, name).map(|ty| docs_excerpt(&ty.docs)),
477 submitted_excerpt: type_named(submitted, name).map(|ty| docs_excerpt(&ty.docs)),
478 condition: None,
479 });
480 }
481 TypeChange::TypeDeprecationChanged { name } => {
482 findings.push(Finding {
483 code: CODE_DEPRECATION_CHANGED,
484 path: type_path(base, name),
485 kind: KIND_DEPRECATION_CHANGED,
486 class: Class::Deprecation,
487 base_excerpt: type_named(base, name).and_then(|ty| ty.deprecation.clone()),
488 submitted_excerpt: type_named(submitted, name)
489 .and_then(|ty| ty.deprecation.clone()),
490 condition: None,
491 });
492 }
493 TypeChange::VariantAdded {
494 type_name,
495 variant_name,
496 roles,
497 } if roles.output => {
498 findings.push(Finding {
499 code: CODE_VARIANT_ADDED,
500 path: variant_path(base, type_name, variant_name),
501 kind: KIND_VARIANT_ADDED,
502 class: Class::CompatibleWithConditions,
503 base_excerpt: None,
504 submitted_excerpt: Some(variant_name.clone()),
505 condition: Some(CONDITION_UNKNOWN_VARIANT),
506 });
507 }
508 TypeChange::VariantRemoved {
509 type_name,
510 variant_name,
511 ..
512 } => {
513 findings.push(Finding {
514 code: CODE_VARIANT_REMOVED,
515 path: variant_path(base, type_name, variant_name),
516 kind: KIND_VARIANT_REMOVED,
517 class: Class::Incompatible,
518 base_excerpt: Some(variant_name.clone()),
519 submitted_excerpt: None,
520 condition: None,
521 });
522 }
523 TypeChange::VariantDocsChanged {
524 type_name,
525 variant_name,
526 } => {
527 findings.push(Finding {
528 code: CODE_DOCS_CHANGED,
529 path: variant_path(base, type_name, variant_name),
530 kind: KIND_DOCS_CHANGED,
531 class: Class::Documentation,
532 base_excerpt: type_named(base, type_name)
533 .and_then(|ty| variant_named(ty, variant_name))
534 .map(|v| docs_excerpt(&v.docs)),
535 submitted_excerpt: type_named(submitted, type_name)
536 .and_then(|ty| variant_named(ty, variant_name))
537 .map(|v| docs_excerpt(&v.docs)),
538 condition: None,
539 });
540 }
541 TypeChange::VariantDeprecationChanged {
542 type_name,
543 variant_name,
544 } => {
545 findings.push(Finding {
546 code: CODE_DEPRECATION_CHANGED,
547 path: variant_path(base, type_name, variant_name),
548 kind: KIND_DEPRECATION_CHANGED,
549 class: Class::Deprecation,
550 base_excerpt: type_named(base, type_name)
551 .and_then(|ty| variant_named(ty, variant_name))
552 .and_then(|v| v.deprecation.clone()),
553 submitted_excerpt: type_named(submitted, type_name)
554 .and_then(|ty| variant_named(ty, variant_name))
555 .and_then(|v| v.deprecation.clone()),
556 condition: None,
557 });
558 }
559 TypeChange::PayloadDocsChanged {
560 type_name,
561 variant_name,
562 }
563 | TypeChange::FieldDocsChanged {
564 type_name,
565 variant_name,
566 field_name: _,
567 } => {
568 let (path, base_excerpt, submitted_excerpt) = match change {
569 TypeChange::FieldDocsChanged { field_name, .. } => (
570 field_path(base, type_name, variant_name, field_name),
571 type_named(base, type_name)
572 .and_then(|ty| variant_named(ty, variant_name))
573 .and_then(|v| field_named(v, field_name))
574 .map(|f| docs_excerpt(&f.docs)),
575 type_named(submitted, type_name)
576 .and_then(|ty| variant_named(ty, variant_name))
577 .and_then(|v| field_named(v, field_name))
578 .map(|f| docs_excerpt(&f.docs)),
579 ),
580 _ => (
581 variant_path(base, type_name, variant_name),
582 type_named(base, type_name)
583 .and_then(|ty| variant_named(ty, variant_name))
584 .map(|v| match &v.payload {
585 SchemaPayload::Value { docs, .. } => docs_excerpt(docs),
586 _ => docs_excerpt(&v.docs),
587 }),
588 type_named(submitted, type_name)
589 .and_then(|ty| variant_named(ty, variant_name))
590 .map(|v| match &v.payload {
591 SchemaPayload::Value { docs, .. } => docs_excerpt(docs),
592 _ => docs_excerpt(&v.docs),
593 }),
594 ),
595 };
596 findings.push(Finding {
597 code: CODE_DOCS_CHANGED,
598 path,
599 kind: KIND_DOCS_CHANGED,
600 class: Class::Documentation,
601 base_excerpt,
602 submitted_excerpt,
603 condition: None,
604 });
605 }
606 TypeChange::PayloadDeprecationChanged {
607 type_name,
608 variant_name,
609 }
610 | TypeChange::FieldDeprecationChanged {
611 type_name,
612 variant_name,
613 field_name: _,
614 } => {
615 let (path, base_excerpt, submitted_excerpt) = match change {
616 TypeChange::FieldDeprecationChanged { field_name, .. } => (
617 field_path(base, type_name, variant_name, field_name),
618 type_named(base, type_name)
619 .and_then(|ty| variant_named(ty, variant_name))
620 .and_then(|v| field_named(v, field_name))
621 .and_then(|f| f.deprecation.clone()),
622 type_named(submitted, type_name)
623 .and_then(|ty| variant_named(ty, variant_name))
624 .and_then(|v| field_named(v, field_name))
625 .and_then(|f| f.deprecation.clone()),
626 ),
627 _ => (
628 variant_path(base, type_name, variant_name),
629 type_named(base, type_name)
630 .and_then(|ty| variant_named(ty, variant_name))
631 .and_then(|v| match &v.payload {
632 SchemaPayload::Value { deprecation, .. } => deprecation.clone(),
633 _ => v.deprecation.clone(),
634 }),
635 type_named(submitted, type_name)
636 .and_then(|ty| variant_named(ty, variant_name))
637 .and_then(|v| match &v.payload {
638 SchemaPayload::Value { deprecation, .. } => deprecation.clone(),
639 _ => v.deprecation.clone(),
640 }),
641 ),
642 };
643 findings.push(Finding {
644 code: CODE_DEPRECATION_CHANGED,
645 path,
646 kind: KIND_DEPRECATION_CHANGED,
647 class: Class::Deprecation,
648 base_excerpt,
649 submitted_excerpt,
650 condition: None,
651 });
652 }
653 TypeChange::FieldAdded {
654 type_name,
655 variant_name,
656 field_name,
657 roles,
658 } if roles.input || roles.output => findings.push(Finding {
659 code: CODE_FIELD_ADDED,
660 path: field_path(base, type_name, variant_name, field_name),
661 kind: KIND_FIELD_ADDED,
662 class: if roles.input {
663 Class::Incompatible
664 } else {
665 Class::Additive
666 },
667 base_excerpt: None,
668 submitted_excerpt: Some(field_name.clone()),
669 condition: None,
670 }),
671 TypeChange::FieldRemoved {
672 type_name,
673 variant_name,
674 field_name,
675 ..
676 } => findings.push(Finding {
677 code: CODE_FIELD_REMOVED,
678 path: field_path(base, type_name, variant_name, field_name),
679 kind: KIND_FIELD_REMOVED,
680 class: Class::Incompatible,
681 base_excerpt: Some(field_name.clone()),
682 submitted_excerpt: None,
683 condition: None,
684 }),
685 TypeChange::FieldTypeChanged {
686 type_name,
687 variant_name,
688 field_name,
689 } => findings.push(Finding {
690 code: CODE_FIELD_TYPE_CHANGED,
691 path: field_path(base, type_name, variant_name, field_name),
692 kind: KIND_FIELD_TYPE_CHANGED,
693 class: Class::Incompatible,
694 base_excerpt: type_named(base, type_name)
695 .and_then(|ty| variant_named(ty, variant_name))
696 .and_then(|v| field_named(v, field_name))
697 .map(|f| f.ty.canonical_name().to_owned()),
698 submitted_excerpt: type_named(submitted, type_name)
699 .and_then(|ty| variant_named(ty, variant_name))
700 .and_then(|v| field_named(v, field_name))
701 .map(|f| f.ty.canonical_name().to_owned()),
702 condition: None,
703 }),
704 TypeChange::VariantPayloadChanged { type_name, .. }
705 | TypeChange::PayloadTypeChanged { type_name, .. } => {
706 let paths = error_paths(base, submitted, type_name);
707 if paths.is_empty() {
708 unclassified = true;
709 }
710 let (base_excerpt, submitted_excerpt) = match change {
711 TypeChange::VariantPayloadChanged { variant_name, .. } => (
712 type_named(base, type_name)
713 .and_then(|ty| variant_named(ty, variant_name))
714 .map(|v| payload_kind_excerpt(&v.payload).to_owned()),
715 type_named(submitted, type_name)
716 .and_then(|ty| variant_named(ty, variant_name))
717 .map(|v| payload_kind_excerpt(&v.payload).to_owned()),
718 ),
719 TypeChange::PayloadTypeChanged { variant_name, .. } => (
720 type_named(base, type_name)
721 .and_then(|ty| variant_named(ty, variant_name))
722 .and_then(|v| match &v.payload {
723 SchemaPayload::Value { ty, .. } => {
724 Some(ty.canonical_name().to_owned())
725 }
726 _ => None,
727 }),
728 type_named(submitted, type_name)
729 .and_then(|ty| variant_named(ty, variant_name))
730 .and_then(|v| match &v.payload {
731 SchemaPayload::Value { ty, .. } => {
732 Some(ty.canonical_name().to_owned())
733 }
734 _ => None,
735 }),
736 ),
737 _ => (None, None),
738 };
739 findings.extend(paths.into_iter().map(|path| Finding {
740 code: CODE_PAYLOAD_CHANGED,
741 path,
742 kind: KIND_PAYLOAD_CHANGED,
743 class: Class::Incompatible,
744 base_excerpt: base_excerpt.clone(),
745 submitted_excerpt: submitted_excerpt.clone(),
746 condition: None,
747 }));
748 }
749 TypeChange::TypeAdded { .. }
751 | TypeChange::VariantAdded { .. }
752 | TypeChange::TypesReordered
753 | TypeChange::VariantsReordered { .. }
754 | TypeChange::FieldAdded { .. }
755 | TypeChange::FieldsReordered { .. } => {
756 unclassified = true;
757 }
758 }
759 }
760
761 let changes = capability_changes(base, submitted);
762 for change in &changes {
763 match change {
764 CapabilityChange::CapabilityAdded { name } => findings.push(Finding {
765 code: CODE_CAPABILITY_ADDED,
766 path: capability_path(base, name),
767 kind: KIND_CAPABILITY_ADDED,
768 class: Class::Additive,
769 base_excerpt: None,
770 submitted_excerpt: Some(name.clone()),
771 condition: None,
772 }),
773 CapabilityChange::CapabilityRemoved { name } => findings.push(Finding {
774 code: CODE_CAPABILITY_REMOVED,
775 path: capability_path(base, name),
776 kind: KIND_CAPABILITY_REMOVED,
777 class: Class::Incompatible,
778 base_excerpt: Some(name.clone()),
779 submitted_excerpt: None,
780 condition: None,
781 }),
782 CapabilityChange::InputNameChanged { name } => findings.push(Finding {
783 code: CODE_INPUT_NAME_CHANGED,
784 path: capability_input_path(base, name),
785 kind: KIND_INPUT_NAME_CHANGED,
786 class: Class::Incompatible,
787 base_excerpt: capability_named(base, name).map(|c| c.input.name.clone()),
788 submitted_excerpt: capability_named(submitted, name).map(|c| c.input.name.clone()),
789 condition: None,
790 }),
791 CapabilityChange::InputLeafChanged { name } => findings.push(Finding {
792 code: CODE_INPUT_LEAF_CHANGED,
793 path: capability_input_path(base, name),
794 kind: KIND_INPUT_LEAF_CHANGED,
795 class: Class::Incompatible,
796 base_excerpt: capability_named(base, name)
797 .map(|c| c.input.leaf.canonical_name().to_owned()),
798 submitted_excerpt: capability_named(submitted, name)
799 .map(|c| c.input.leaf.canonical_name().to_owned()),
800 condition: None,
801 }),
802 CapabilityChange::OutputLeafChanged { name } => findings.push(Finding {
803 code: CODE_OUTPUT_LEAF_CHANGED,
804 path: capability_output_path(base, name),
805 kind: KIND_OUTPUT_LEAF_CHANGED,
806 class: Class::Incompatible,
807 base_excerpt: capability_named(base, name)
808 .map(|c| c.output.leaf.canonical_name().to_owned()),
809 submitted_excerpt: capability_named(submitted, name)
810 .map(|c| c.output.leaf.canonical_name().to_owned()),
811 condition: None,
812 }),
813 CapabilityChange::CapabilityDocsChanged { name } => findings.push(Finding {
814 code: CODE_DOCS_CHANGED,
815 path: capability_path(base, name),
816 kind: KIND_DOCS_CHANGED,
817 class: Class::Documentation,
818 base_excerpt: capability_named(base, name).map(|c| docs_excerpt(&c.docs)),
819 submitted_excerpt: capability_named(submitted, name).map(|c| docs_excerpt(&c.docs)),
820 condition: None,
821 }),
822 CapabilityChange::CapabilityDeprecationChanged { name } => findings.push(Finding {
823 code: CODE_DEPRECATION_CHANGED,
824 path: capability_path(base, name),
825 kind: KIND_DEPRECATION_CHANGED,
826 class: Class::Deprecation,
827 base_excerpt: capability_named(base, name).and_then(|c| c.deprecation.clone()),
828 submitted_excerpt: capability_named(submitted, name)
829 .and_then(|c| c.deprecation.clone()),
830 condition: None,
831 }),
832 CapabilityChange::CapabilityErrorChanged { name } => findings.push(Finding {
833 code: CODE_ERROR_CHANGED,
834 path: capability_suffix_path(base, name, "error"),
835 kind: KIND_ERROR_CHANGED,
836 class: Class::Incompatible,
837 base_excerpt: capability_named(base, name).map(|c| c.error.clone()),
838 submitted_excerpt: capability_named(submitted, name).map(|c| c.error.clone()),
839 condition: None,
840 }),
841 CapabilityChange::CapabilityExposureChanged {
842 name,
843 base: base_level,
844 submitted: submitted_level,
845 } => match exposure_classification(*base_level, *submitted_level) {
846 Some((code, class)) => findings.push(Finding {
847 code,
848 path: capability_suffix_path(base, name, "exposure"),
849 kind: if code == CODE_EXPOSURE_RAISED {
850 KIND_EXPOSURE_RAISED
851 } else {
852 KIND_EXPOSURE_LOWERED
853 },
854 class,
855 base_excerpt: Some(exposure_excerpt(*base_level).to_owned()),
856 submitted_excerpt: Some(exposure_excerpt(*submitted_level).to_owned()),
857 condition: None,
858 }),
859 None => unclassified = true,
860 },
861 CapabilityChange::CapabilityIdempotencyChanged {
862 name,
863 base: base_property,
864 submitted: submitted_property,
865 } => match idempotency_classification(*base_property, *submitted_property) {
866 Some((code, class)) => findings.push(Finding {
867 code,
868 path: capability_suffix_path(base, name, "idempotency"),
869 kind: if code == CODE_IDEMPOTENCY_STRENGTHENED {
870 KIND_IDEMPOTENCY_STRENGTHENED
871 } else {
872 KIND_IDEMPOTENCY_WEAKENED
873 },
874 class,
875 base_excerpt: Some(idempotency_excerpt(*base_property).to_owned()),
876 submitted_excerpt: Some(idempotency_excerpt(*submitted_property).to_owned()),
877 condition: None,
878 }),
879 None => unclassified = true,
880 },
881 CapabilityChange::CapabilitiesReordered => {
882 unclassified = true;
883 }
884 }
885 }
886
887 if unclassified {
891 findings.push(fail_closed_finding(base));
892 }
893 findings
894}
895
896fn append_data_findings(
897 base: &SchemaDocument,
898 submitted: &SchemaDocument,
899 roles_by_type: &BTreeMap<&str, Roles>,
900 findings: &mut Vec<Finding>,
901) -> bool {
902 let mut unclassified = false;
903 for change in data_changes(base, submitted, roles_by_type) {
904 match change {
905 DataChange::TypeAdded { name, roles } if roles.input || roles.output => {
906 findings.push(data_finding(
907 CODE_TYPE_ADDED,
908 type_path(base, &name),
909 KIND_TYPE_ADDED,
910 Class::Additive,
911 None,
912 Some(name),
913 None,
914 ));
915 }
916 DataChange::TypeRemoved { name, .. } => findings.push(data_finding(
917 CODE_TYPE_REMOVED,
918 type_path(base, &name),
919 KIND_TYPE_REMOVED,
920 Class::Incompatible,
921 Some(name),
922 None,
923 None,
924 )),
925 DataChange::TypeKindChanged(name) if roles_by_type.contains_key(name.as_str()) => {
926 findings.push(data_finding(
927 CODE_TYPE_KIND_CHANGED,
928 type_path(base, &name),
929 KIND_TYPE_KIND_CHANGED,
930 Class::Incompatible,
931 declaration(base, &name).map(|item| item.0.to_owned()),
932 declaration(submitted, &name).map(|item| item.0.to_owned()),
933 None,
934 ))
935 }
936 DataChange::TypeDocsChanged { name } => findings.push(data_finding(
937 CODE_DOCS_CHANGED,
938 type_path(base, &name),
939 KIND_DOCS_CHANGED,
940 Class::Documentation,
941 declaration(base, &name).map(|item| docs_excerpt(item.1)),
942 declaration(submitted, &name).map(|item| docs_excerpt(item.1)),
943 None,
944 )),
945 DataChange::TypeDeprecationChanged { name } => findings.push(data_finding(
946 CODE_DEPRECATION_CHANGED,
947 type_path(base, &name),
948 KIND_DEPRECATION_CHANGED,
949 Class::Deprecation,
950 declaration(base, &name).and_then(|item| item.2.map(str::to_owned)),
951 declaration(submitted, &name).and_then(|item| item.2.map(str::to_owned)),
952 None,
953 )),
954 DataChange::FieldAdded {
955 type_name,
956 field_name,
957 roles,
958 } => {
959 if roles.input {
960 let field = data_type_named(submitted, &type_name)
961 .and_then(|item| data_field_named(item, &field_name))
962 .expect("an added field is present in the submitted type");
963 let optional = is_top_level_optional(&field.ty);
964 findings.push(data_finding(
965 CODE_FIELD_ADDED,
966 data_field_path(base, &type_name, &field_name),
967 KIND_FIELD_ADDED,
968 if optional {
969 Class::CompatibleWithConditions
970 } else {
971 Class::Incompatible
972 },
973 None,
974 Some(field_name.clone()),
975 optional.then_some(CONDITION_PROVIDER_FIRST),
976 ));
977 }
978 if roles.output {
979 findings.push(data_finding(
980 CODE_OUTPUT_FIELD_ADDED,
981 data_field_path(base, &type_name, &field_name),
982 KIND_FIELD_ADDED,
983 Class::Additive,
984 None,
985 Some(field_name),
986 None,
987 ));
988 }
989 unclassified |= !roles.input && !roles.output;
990 }
991 DataChange::FieldRemoved {
992 type_name,
993 field_name,
994 roles,
995 } => {
996 if roles.input {
997 findings.push(data_finding(
998 CODE_FIELD_REMOVED,
999 data_field_path(base, &type_name, &field_name),
1000 KIND_FIELD_REMOVED,
1001 Class::Incompatible,
1002 Some(field_name.clone()),
1003 None,
1004 None,
1005 ));
1006 }
1007 if roles.output {
1008 findings.push(data_finding(
1009 CODE_OUTPUT_FIELD_REMOVED,
1010 data_field_path(base, &type_name, &field_name),
1011 KIND_FIELD_REMOVED,
1012 Class::Incompatible,
1013 Some(field_name),
1014 None,
1015 None,
1016 ));
1017 }
1018 unclassified |= !roles.input && !roles.output;
1019 }
1020 DataChange::FieldTypeChanged {
1021 type_name,
1022 field_name,
1023 } => {
1024 let roles = roles_for(roles_by_type, &type_name);
1025 let path = data_field_path(base, &type_name, &field_name);
1026 let base_excerpt = data_type_named(base, &type_name)
1027 .and_then(|item| data_field_named(item, &field_name))
1028 .map(|item| item.ty.canonical_name());
1029 let submitted_excerpt = data_type_named(submitted, &type_name)
1030 .and_then(|item| data_field_named(item, &field_name))
1031 .map(|item| item.ty.canonical_name());
1032 if roles.input {
1033 findings.push(data_finding(
1034 CODE_FIELD_TYPE_CHANGED,
1035 path.clone(),
1036 KIND_FIELD_TYPE_CHANGED,
1037 Class::Incompatible,
1038 base_excerpt.clone(),
1039 submitted_excerpt.clone(),
1040 None,
1041 ));
1042 }
1043 if roles.output {
1044 findings.push(data_finding(
1045 CODE_OUTPUT_FIELD_TYPE_CHANGED,
1046 path,
1047 KIND_FIELD_TYPE_CHANGED,
1048 Class::Incompatible,
1049 base_excerpt,
1050 submitted_excerpt,
1051 None,
1052 ));
1053 }
1054 unclassified |= !roles.input && !roles.output;
1055 }
1056 DataChange::FieldDocsChanged {
1057 type_name,
1058 field_name,
1059 } => findings.push(data_finding(
1060 CODE_DOCS_CHANGED,
1061 data_field_path(base, &type_name, &field_name),
1062 KIND_DOCS_CHANGED,
1063 Class::Documentation,
1064 data_type_named(base, &type_name)
1065 .and_then(|item| data_field_named(item, &field_name))
1066 .map(|item| docs_excerpt(&item.docs)),
1067 data_type_named(submitted, &type_name)
1068 .and_then(|item| data_field_named(item, &field_name))
1069 .map(|item| docs_excerpt(&item.docs)),
1070 None,
1071 )),
1072 DataChange::FieldDeprecationChanged {
1073 type_name,
1074 field_name,
1075 } => findings.push(data_finding(
1076 CODE_DEPRECATION_CHANGED,
1077 data_field_path(base, &type_name, &field_name),
1078 KIND_DEPRECATION_CHANGED,
1079 Class::Deprecation,
1080 data_type_named(base, &type_name)
1081 .and_then(|item| data_field_named(item, &field_name))
1082 .and_then(|item| item.deprecation.clone()),
1083 data_type_named(submitted, &type_name)
1084 .and_then(|item| data_field_named(item, &field_name))
1085 .and_then(|item| item.deprecation.clone()),
1086 None,
1087 )),
1088 DataChange::VariantAdded {
1089 type_name,
1090 variant_name,
1091 roles,
1092 } => {
1093 if roles.input {
1094 findings.push(data_finding(
1095 CODE_INPUT_VARIANT_ADDED,
1096 variant_path(base, &type_name, &variant_name),
1097 KIND_ENUM_VARIANT_ADDED,
1098 Class::CompatibleWithConditions,
1099 None,
1100 Some(variant_name.clone()),
1101 Some(CONDITION_PROVIDER_FIRST),
1102 ));
1103 }
1104 if roles.output {
1105 findings.push(data_finding(
1106 CODE_OUTPUT_VARIANT_ADDED,
1107 variant_path(base, &type_name, &variant_name),
1108 KIND_ENUM_VARIANT_ADDED,
1109 Class::CompatibleWithConditions,
1110 None,
1111 Some(variant_name),
1112 Some(CONDITION_UNKNOWN_VARIANT),
1113 ));
1114 }
1115 unclassified |= !roles.input && !roles.output;
1116 }
1117 DataChange::VariantRemoved {
1118 type_name,
1119 variant_name,
1120 roles,
1121 } => {
1122 if roles.input {
1123 findings.push(data_finding(
1124 CODE_VARIANT_REMOVED,
1125 variant_path(base, &type_name, &variant_name),
1126 KIND_VARIANT_REMOVED,
1127 Class::Incompatible,
1128 Some(variant_name.clone()),
1129 None,
1130 None,
1131 ));
1132 }
1133 if roles.output {
1134 findings.push(data_finding(
1135 CODE_OUTPUT_VARIANT_REMOVED,
1136 variant_path(base, &type_name, &variant_name),
1137 KIND_VARIANT_REMOVED,
1138 Class::Incompatible,
1139 Some(variant_name),
1140 None,
1141 None,
1142 ));
1143 }
1144 unclassified |= !roles.input && !roles.output;
1145 }
1146 DataChange::VariantDocsChanged {
1147 type_name,
1148 variant_name,
1149 } => findings.push(data_finding(
1150 CODE_DOCS_CHANGED,
1151 variant_path(base, &type_name, &variant_name),
1152 KIND_DOCS_CHANGED,
1153 Class::Documentation,
1154 data_type_named(base, &type_name)
1155 .and_then(|item| data_variant_named(item, &variant_name))
1156 .map(|item| docs_excerpt(&item.docs)),
1157 data_type_named(submitted, &type_name)
1158 .and_then(|item| data_variant_named(item, &variant_name))
1159 .map(|item| docs_excerpt(&item.docs)),
1160 None,
1161 )),
1162 DataChange::VariantDeprecationChanged {
1163 type_name,
1164 variant_name,
1165 } => findings.push(data_finding(
1166 CODE_DEPRECATION_CHANGED,
1167 variant_path(base, &type_name, &variant_name),
1168 KIND_DEPRECATION_CHANGED,
1169 Class::Deprecation,
1170 data_type_named(base, &type_name)
1171 .and_then(|item| data_variant_named(item, &variant_name))
1172 .and_then(|item| item.deprecation.clone()),
1173 data_type_named(submitted, &type_name)
1174 .and_then(|item| data_variant_named(item, &variant_name))
1175 .and_then(|item| item.deprecation.clone()),
1176 None,
1177 )),
1178 DataChange::TypeAdded { .. }
1179 | DataChange::TypeKindChanged(_)
1180 | DataChange::TypesReordered
1181 | DataChange::FieldsReordered { .. }
1182 | DataChange::VariantsReordered { .. } => unclassified = true,
1183 }
1184 }
1185 unclassified
1186}
1187
1188fn exposure_classification(
1189 base: ExposureLevel,
1190 submitted: ExposureLevel,
1191) -> Option<(&'static str, Class)> {
1192 match (base, submitted) {
1193 (ExposureLevel::CodeOnly, ExposureLevel::Internal | ExposureLevel::External)
1194 | (ExposureLevel::Internal, ExposureLevel::External) => {
1195 Some((CODE_EXPOSURE_RAISED, Class::Additive))
1196 }
1197 (ExposureLevel::Internal | ExposureLevel::External, ExposureLevel::CodeOnly)
1198 | (ExposureLevel::External, ExposureLevel::Internal) => {
1199 Some((CODE_EXPOSURE_LOWERED, Class::Incompatible))
1200 }
1201 (ExposureLevel::CodeOnly, ExposureLevel::CodeOnly)
1202 | (ExposureLevel::Internal, ExposureLevel::Internal)
1203 | (ExposureLevel::External, ExposureLevel::External) => None,
1204 }
1205}
1206
1207fn idempotency_classification(
1208 base: Idempotency,
1209 submitted: Idempotency,
1210) -> Option<(&'static str, Class)> {
1211 match (base, submitted) {
1212 (Idempotency::None, Idempotency::Inherent) => {
1213 Some((CODE_IDEMPOTENCY_STRENGTHENED, Class::Additive))
1214 }
1215 (Idempotency::Inherent, Idempotency::None) => {
1216 Some((CODE_IDEMPOTENCY_WEAKENED, Class::Incompatible))
1217 }
1218 (Idempotency::None, Idempotency::None) | (Idempotency::Inherent, Idempotency::Inherent) => {
1219 None
1220 }
1221 }
1222}
1223
1224fn capability_path(base: &SchemaDocument, name: &str) -> String {
1225 [base.box_id.as_str(), ".", name].concat()
1226}
1227
1228fn capability_input_path(base: &SchemaDocument, name: &str) -> String {
1229 [capability_path(base, name).as_str(), "/input"].concat()
1230}
1231
1232fn capability_output_path(base: &SchemaDocument, name: &str) -> String {
1233 [capability_path(base, name).as_str(), "/output"].concat()
1234}
1235
1236fn capability_suffix_path(base: &SchemaDocument, name: &str, suffix: &str) -> String {
1237 [capability_path(base, name).as_str(), "/", suffix].concat()
1238}
1239
1240fn type_path(base: &SchemaDocument, name: &str) -> String {
1241 [base.box_id.as_str(), "/type/", name].concat()
1242}
1243
1244fn variant_path(base: &SchemaDocument, type_name: &str, variant_name: &str) -> String {
1245 [
1246 base.box_id.as_str(),
1247 "/type/",
1248 type_name,
1249 "/variant/",
1250 variant_name,
1251 ]
1252 .concat()
1253}
1254
1255fn data_field_path(base: &SchemaDocument, type_name: &str, field_name: &str) -> String {
1256 [type_path(base, type_name).as_str(), "/field/", field_name].concat()
1257}
1258
1259fn field_path(
1260 base: &SchemaDocument,
1261 type_name: &str,
1262 variant_name: &str,
1263 field_name: &str,
1264) -> String {
1265 [
1266 variant_path(base, type_name, variant_name).as_str(),
1267 "/field/",
1268 field_name,
1269 ]
1270 .concat()
1271}
1272
1273fn error_paths(base: &SchemaDocument, submitted: &SchemaDocument, type_name: &str) -> Vec<String> {
1274 let mut paths = Vec::new();
1275 for document in [base, submitted] {
1276 for capability in &document.capabilities {
1277 if capability.error == type_name {
1278 paths.push(capability_suffix_path(
1279 base,
1280 capability.name.as_str(),
1281 "error",
1282 ));
1283 }
1284 }
1285 }
1286 paths.sort();
1287 paths.dedup();
1288 paths
1289}
1290
1291#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1292struct Roles {
1293 input: bool,
1294 output: bool,
1295}
1296
1297#[derive(Debug, Eq, PartialEq)]
1298enum DataChange {
1299 TypeAdded {
1300 name: String,
1301 roles: Roles,
1302 },
1303 TypeRemoved {
1304 name: String,
1305 roles: Roles,
1306 },
1307 TypesReordered,
1308 TypeKindChanged(String),
1309 TypeDocsChanged {
1310 name: String,
1311 },
1312 TypeDeprecationChanged {
1313 name: String,
1314 },
1315 FieldAdded {
1316 type_name: String,
1317 field_name: String,
1318 roles: Roles,
1319 },
1320 FieldRemoved {
1321 type_name: String,
1322 field_name: String,
1323 roles: Roles,
1324 },
1325 FieldsReordered {
1326 type_name: String,
1327 },
1328 FieldDocsChanged {
1329 type_name: String,
1330 field_name: String,
1331 },
1332 FieldDeprecationChanged {
1333 type_name: String,
1334 field_name: String,
1335 },
1336 FieldTypeChanged {
1337 type_name: String,
1338 field_name: String,
1339 },
1340 VariantAdded {
1341 type_name: String,
1342 variant_name: String,
1343 roles: Roles,
1344 },
1345 VariantRemoved {
1346 type_name: String,
1347 variant_name: String,
1348 roles: Roles,
1349 },
1350 VariantsReordered {
1351 type_name: String,
1352 },
1353 VariantDocsChanged {
1354 type_name: String,
1355 variant_name: String,
1356 },
1357 VariantDeprecationChanged {
1358 type_name: String,
1359 variant_name: String,
1360 },
1361}
1362
1363#[derive(Debug, Eq, PartialEq)]
1364enum TypeChange {
1365 TypeAdded {
1366 name: String,
1367 roles: Roles,
1368 },
1369 TypeRemoved {
1370 name: String,
1371 roles: Roles,
1372 },
1373 TypesReordered,
1374 TypeDocsChanged {
1375 name: String,
1376 },
1377 TypeDeprecationChanged {
1378 name: String,
1379 },
1380 VariantAdded {
1381 type_name: String,
1382 variant_name: String,
1383 roles: Roles,
1384 },
1385 VariantRemoved {
1386 type_name: String,
1387 variant_name: String,
1388 roles: Roles,
1389 },
1390 VariantsReordered {
1391 type_name: String,
1392 },
1393 VariantDocsChanged {
1394 type_name: String,
1395 variant_name: String,
1396 },
1397 VariantDeprecationChanged {
1398 type_name: String,
1399 variant_name: String,
1400 },
1401 VariantPayloadChanged {
1402 type_name: String,
1403 variant_name: String,
1404 roles: Roles,
1405 },
1406 PayloadDocsChanged {
1407 type_name: String,
1408 variant_name: String,
1409 },
1410 PayloadDeprecationChanged {
1411 type_name: String,
1412 variant_name: String,
1413 },
1414 PayloadTypeChanged {
1415 type_name: String,
1416 variant_name: String,
1417 },
1418 FieldAdded {
1419 type_name: String,
1420 variant_name: String,
1421 field_name: String,
1422 roles: Roles,
1423 },
1424 FieldRemoved {
1425 type_name: String,
1426 variant_name: String,
1427 field_name: String,
1428 roles: Roles,
1429 },
1430 FieldDocsChanged {
1431 type_name: String,
1432 variant_name: String,
1433 field_name: String,
1434 },
1435 FieldDeprecationChanged {
1436 type_name: String,
1437 variant_name: String,
1438 field_name: String,
1439 },
1440 FieldTypeChanged {
1441 type_name: String,
1442 variant_name: String,
1443 field_name: String,
1444 },
1445 FieldsReordered {
1446 type_name: String,
1447 variant_name: String,
1448 },
1449}
1450
1451#[derive(Debug, Eq, PartialEq)]
1452enum CapabilityChange {
1453 CapabilityAdded {
1454 name: String,
1455 },
1456 CapabilityRemoved {
1457 name: String,
1458 },
1459 CapabilitiesReordered,
1460 CapabilityDocsChanged {
1461 name: String,
1462 },
1463 CapabilityDeprecationChanged {
1464 name: String,
1465 },
1466 CapabilityErrorChanged {
1467 name: String,
1468 },
1469 CapabilityExposureChanged {
1470 name: String,
1471 base: ExposureLevel,
1472 submitted: ExposureLevel,
1473 },
1474 CapabilityIdempotencyChanged {
1475 name: String,
1476 base: Idempotency,
1477 submitted: Idempotency,
1478 },
1479 InputNameChanged {
1480 name: String,
1481 },
1482 InputLeafChanged {
1483 name: String,
1484 },
1485 OutputLeafChanged {
1486 name: String,
1487 },
1488}
1489
1490fn reachability<'a>(
1497 base: &'a SchemaDocument,
1498 submitted: &'a SchemaDocument,
1499) -> BTreeMap<&'a str, Roles> {
1500 let mut roles = BTreeMap::new();
1501 for document in [base, submitted] {
1502 for capability in &document.capabilities {
1503 mark_expression(
1504 &mut roles,
1505 &capability.input.leaf,
1506 Roles {
1507 input: true,
1508 output: false,
1509 },
1510 );
1511 mark_expression(
1512 &mut roles,
1513 &capability.output.leaf,
1514 Roles {
1515 input: false,
1516 output: true,
1517 },
1518 );
1519 mark_roles(
1520 &mut roles,
1521 capability.error.as_str(),
1522 Roles {
1523 input: false,
1524 output: true,
1525 },
1526 );
1527 }
1528 }
1529 loop {
1530 let mut changed = false;
1531 for document in [base, submitted] {
1532 for data_type in &document.data_types {
1533 let inherited = roles_for(&roles, data_type.name.as_str());
1534 if let SchemaDataShape::Struct(fields) = &data_type.shape {
1535 for field in fields {
1536 changed |= mark_expression(&mut roles, &field.ty, inherited);
1537 }
1538 }
1539 }
1540 }
1541 if !changed {
1542 break;
1543 }
1544 }
1545 roles
1546}
1547
1548fn mark_expression<'a>(
1549 roles: &mut BTreeMap<&'a str, Roles>,
1550 expression: &'a TypeExpression,
1551 inherited: Roles,
1552) -> bool {
1553 if !inherited.input && !inherited.output {
1554 return false;
1555 }
1556 match expression {
1557 TypeExpression::Local(name) => mark_roles(roles, name, inherited),
1558 TypeExpression::Option(inner) | TypeExpression::Vec(inner) => {
1559 mark_expression(roles, inner, inherited)
1560 }
1561 _ => false,
1562 }
1563}
1564
1565fn mark_roles<'a>(roles: &mut BTreeMap<&'a str, Roles>, name: &'a str, inherited: Roles) -> bool {
1566 let entry = roles.entry(name).or_insert(Roles {
1567 input: false,
1568 output: false,
1569 });
1570 let before = *entry;
1571 entry.input |= inherited.input;
1572 entry.output |= inherited.output;
1573 *entry != before
1574}
1575
1576fn roles_for(roles: &BTreeMap<&str, Roles>, name: &str) -> Roles {
1577 roles.get(name).copied().unwrap_or(Roles {
1578 input: false,
1579 output: false,
1580 })
1581}
1582
1583fn index_types(types: &[SchemaType]) -> BTreeMap<&str, &SchemaType> {
1584 let mut index = BTreeMap::new();
1585 for schema_type in types {
1586 index.insert(schema_type.name.as_str(), schema_type);
1587 }
1588 index
1589}
1590
1591fn index_data_types(types: &[SchemaDataType]) -> BTreeMap<&str, &SchemaDataType> {
1592 types
1593 .iter()
1594 .map(|item| (item.name.as_str(), item))
1595 .collect()
1596}
1597
1598fn index_data_fields(fields: &[SchemaDataField]) -> BTreeMap<&str, &SchemaDataField> {
1599 fields
1600 .iter()
1601 .map(|item| (item.name.as_str(), item))
1602 .collect()
1603}
1604
1605fn index_data_variants(variants: &[SchemaDataVariant]) -> BTreeMap<&str, &SchemaDataVariant> {
1606 variants
1607 .iter()
1608 .map(|item| (item.name.as_str(), item))
1609 .collect()
1610}
1611
1612fn index_variants(variants: &[SchemaVariant]) -> BTreeMap<&str, &SchemaVariant> {
1613 let mut index = BTreeMap::new();
1614 for variant in variants {
1615 index.insert(variant.name.as_str(), variant);
1616 }
1617 index
1618}
1619
1620fn index_capabilities(capabilities: &[SchemaCapability]) -> BTreeMap<&str, &SchemaCapability> {
1621 let mut index = BTreeMap::new();
1622 for capability in capabilities {
1623 index.insert(capability.name.as_str(), capability);
1624 }
1625 index
1626}
1627
1628fn common_name_sequence_differs<'a, T, U>(
1629 base_names: impl Iterator<Item = &'a str>,
1630 submitted_names: impl Iterator<Item = &'a str>,
1631 base_index: &BTreeMap<&str, &T>,
1632 submitted_index: &BTreeMap<&str, &U>,
1633) -> bool {
1634 let mut base_common = Vec::new();
1635 for name in base_names {
1636 if submitted_index.contains_key(name) {
1637 base_common.push(name);
1638 }
1639 }
1640 let mut submitted_common = Vec::new();
1641 for name in submitted_names {
1642 if base_index.contains_key(name) {
1643 submitted_common.push(name);
1644 }
1645 }
1646 base_common != submitted_common
1647}
1648
1649fn append_cross_kind_changes(
1650 changes: &mut Vec<DataChange>,
1651 base: &SchemaDocument,
1652 submitted: &SchemaDocument,
1653 name: &str,
1654) {
1655 let (_, base_docs, base_deprecation) = declaration(base, name).expect("base declaration");
1656 let (_, submitted_docs, submitted_deprecation) =
1657 declaration(submitted, name).expect("submitted declaration");
1658 changes.push(DataChange::TypeKindChanged(name.to_owned()));
1659 if base_docs != submitted_docs {
1660 changes.push(DataChange::TypeDocsChanged {
1661 name: name.to_owned(),
1662 });
1663 }
1664 if base_deprecation != submitted_deprecation {
1665 changes.push(DataChange::TypeDeprecationChanged {
1666 name: name.to_owned(),
1667 });
1668 }
1669}
1670
1671fn data_changes(
1672 base: &SchemaDocument,
1673 submitted: &SchemaDocument,
1674 roles: &BTreeMap<&str, Roles>,
1675) -> Vec<DataChange> {
1676 let mut changes = Vec::new();
1677 let base_by_name = index_data_types(&base.data_types);
1678 let submitted_by_name = index_data_types(&submitted.data_types);
1679 for base_type in &base.data_types {
1680 match submitted_by_name.get(base_type.name.as_str()) {
1681 None if type_named(submitted, &base_type.name).is_some() => {
1682 append_cross_kind_changes(&mut changes, base, submitted, &base_type.name)
1683 }
1684 None => changes.push(DataChange::TypeRemoved {
1685 name: base_type.name.clone(),
1686 roles: roles_for(roles, base_type.name.as_str()),
1687 }),
1688 Some(submitted_type) => append_matched_data_type_changes(
1689 &mut changes,
1690 base_type,
1691 submitted_type,
1692 roles_for(roles, base_type.name.as_str()),
1693 ),
1694 }
1695 }
1696 for submitted_type in &submitted.data_types {
1697 if !base_by_name.contains_key(submitted_type.name.as_str()) {
1698 match type_named(base, &submitted_type.name) {
1699 Some(_) => {
1700 append_cross_kind_changes(&mut changes, base, submitted, &submitted_type.name)
1701 }
1702 None => changes.push(DataChange::TypeAdded {
1703 name: submitted_type.name.clone(),
1704 roles: roles_for(roles, submitted_type.name.as_str()),
1705 }),
1706 }
1707 }
1708 }
1709 if common_name_sequence_differs(
1710 base.data_types.iter().map(|item| item.name.as_str()),
1711 submitted.data_types.iter().map(|item| item.name.as_str()),
1712 &base_by_name,
1713 &submitted_by_name,
1714 ) {
1715 changes.push(DataChange::TypesReordered);
1716 }
1717 changes
1718}
1719
1720fn append_matched_data_type_changes(
1721 changes: &mut Vec<DataChange>,
1722 base: &SchemaDataType,
1723 submitted: &SchemaDataType,
1724 roles: Roles,
1725) {
1726 if base.docs != submitted.docs {
1727 changes.push(DataChange::TypeDocsChanged {
1728 name: base.name.clone(),
1729 });
1730 }
1731 if base.deprecation != submitted.deprecation {
1732 changes.push(DataChange::TypeDeprecationChanged {
1733 name: base.name.clone(),
1734 });
1735 }
1736 match (&base.shape, &submitted.shape) {
1737 (SchemaDataShape::Struct(base_fields), SchemaDataShape::Struct(submitted_fields)) => {
1738 append_data_field_changes(
1739 changes,
1740 base.name.as_str(),
1741 base_fields,
1742 submitted_fields,
1743 roles,
1744 );
1745 }
1746 (SchemaDataShape::Enum(base_variants), SchemaDataShape::Enum(submitted_variants)) => {
1747 append_data_variant_changes(
1748 changes,
1749 base.name.as_str(),
1750 base_variants,
1751 submitted_variants,
1752 roles,
1753 );
1754 }
1755 _ => changes.push(DataChange::TypeKindChanged(base.name.clone())),
1756 }
1757}
1758
1759fn append_data_field_changes(
1760 changes: &mut Vec<DataChange>,
1761 type_name: &str,
1762 base: &[SchemaDataField],
1763 submitted: &[SchemaDataField],
1764 roles: Roles,
1765) {
1766 let base_by_name = index_data_fields(base);
1767 let submitted_by_name = index_data_fields(submitted);
1768 for field in base {
1769 match submitted_by_name.get(field.name.as_str()) {
1770 None => changes.push(DataChange::FieldRemoved {
1771 type_name: type_name.to_owned(),
1772 field_name: field.name.clone(),
1773 roles,
1774 }),
1775 Some(submitted_field) => {
1776 if field.docs != submitted_field.docs {
1777 changes.push(DataChange::FieldDocsChanged {
1778 type_name: type_name.to_owned(),
1779 field_name: field.name.clone(),
1780 });
1781 }
1782 if field.deprecation != submitted_field.deprecation {
1783 changes.push(DataChange::FieldDeprecationChanged {
1784 type_name: type_name.to_owned(),
1785 field_name: field.name.clone(),
1786 });
1787 }
1788 if field.ty != submitted_field.ty {
1789 changes.push(DataChange::FieldTypeChanged {
1790 type_name: type_name.to_owned(),
1791 field_name: field.name.clone(),
1792 });
1793 }
1794 }
1795 }
1796 }
1797 for field in submitted {
1798 if !base_by_name.contains_key(field.name.as_str()) {
1799 changes.push(DataChange::FieldAdded {
1800 type_name: type_name.to_owned(),
1801 field_name: field.name.clone(),
1802 roles,
1803 });
1804 }
1805 }
1806 if common_name_sequence_differs(
1807 base.iter().map(|item| item.name.as_str()),
1808 submitted.iter().map(|item| item.name.as_str()),
1809 &base_by_name,
1810 &submitted_by_name,
1811 ) {
1812 changes.push(DataChange::FieldsReordered {
1813 type_name: type_name.to_owned(),
1814 });
1815 }
1816}
1817
1818fn append_data_variant_changes(
1819 changes: &mut Vec<DataChange>,
1820 type_name: &str,
1821 base: &[SchemaDataVariant],
1822 submitted: &[SchemaDataVariant],
1823 roles: Roles,
1824) {
1825 let base_by_name = index_data_variants(base);
1826 let submitted_by_name = index_data_variants(submitted);
1827 for variant in base {
1828 match submitted_by_name.get(variant.name.as_str()) {
1829 None => changes.push(DataChange::VariantRemoved {
1830 type_name: type_name.to_owned(),
1831 variant_name: variant.name.clone(),
1832 roles,
1833 }),
1834 Some(submitted_variant) => {
1835 if variant.docs != submitted_variant.docs {
1836 changes.push(DataChange::VariantDocsChanged {
1837 type_name: type_name.to_owned(),
1838 variant_name: variant.name.clone(),
1839 });
1840 }
1841 if variant.deprecation != submitted_variant.deprecation {
1842 changes.push(DataChange::VariantDeprecationChanged {
1843 type_name: type_name.to_owned(),
1844 variant_name: variant.name.clone(),
1845 });
1846 }
1847 }
1848 }
1849 }
1850 for variant in submitted {
1851 if !base_by_name.contains_key(variant.name.as_str()) {
1852 changes.push(DataChange::VariantAdded {
1853 type_name: type_name.to_owned(),
1854 variant_name: variant.name.clone(),
1855 roles,
1856 });
1857 }
1858 }
1859 if common_name_sequence_differs(
1860 base.iter().map(|item| item.name.as_str()),
1861 submitted.iter().map(|item| item.name.as_str()),
1862 &base_by_name,
1863 &submitted_by_name,
1864 ) {
1865 changes.push(DataChange::VariantsReordered {
1866 type_name: type_name.to_owned(),
1867 });
1868 }
1869}
1870
1871fn type_changes(
1872 base: &SchemaDocument,
1873 submitted: &SchemaDocument,
1874 roles: &BTreeMap<&str, Roles>,
1875) -> Vec<TypeChange> {
1876 let mut changes = Vec::new();
1877 let base_by_name = index_types(&base.types);
1878 let submitted_by_name = index_types(&submitted.types);
1879
1880 for base_type in &base.types {
1881 match submitted_by_name.get(base_type.name.as_str()) {
1882 None if data_type_named(submitted, &base_type.name).is_some() => {}
1883 None => {
1884 changes.push(TypeChange::TypeRemoved {
1885 name: base_type.name.clone(),
1886 roles: roles_for(roles, base_type.name.as_str()),
1887 });
1888 }
1889 Some(submitted_type) => {
1890 append_matched_type_changes(
1891 &mut changes,
1892 base_type,
1893 submitted_type,
1894 roles_for(roles, base_type.name.as_str()),
1895 );
1896 }
1897 }
1898 }
1899
1900 for submitted_type in &submitted.types {
1901 if base_by_name.contains_key(submitted_type.name.as_str())
1902 || data_type_named(base, &submitted_type.name).is_some()
1903 {
1904 continue;
1905 }
1906 changes.push(TypeChange::TypeAdded {
1907 name: submitted_type.name.clone(),
1908 roles: roles_for(roles, submitted_type.name.as_str()),
1909 });
1910 }
1911
1912 if common_name_sequence_differs(
1913 base.types
1914 .iter()
1915 .map(|schema_type| schema_type.name.as_str()),
1916 submitted
1917 .types
1918 .iter()
1919 .map(|schema_type| schema_type.name.as_str()),
1920 &base_by_name,
1921 &submitted_by_name,
1922 ) {
1923 changes.push(TypeChange::TypesReordered);
1924 }
1925
1926 changes
1927}
1928
1929fn capability_changes(base: &SchemaDocument, submitted: &SchemaDocument) -> Vec<CapabilityChange> {
1930 let mut changes = Vec::new();
1931 let base_by_name = index_capabilities(&base.capabilities);
1932 let submitted_by_name = index_capabilities(&submitted.capabilities);
1933
1934 for base_capability in &base.capabilities {
1935 match submitted_by_name.get(base_capability.name.as_str()) {
1936 None => changes.push(CapabilityChange::CapabilityRemoved {
1937 name: base_capability.name.as_str().to_owned(),
1938 }),
1939 Some(submitted_capability) => append_matched_capability_changes(
1940 &mut changes,
1941 base_capability,
1942 submitted_capability,
1943 ),
1944 }
1945 }
1946
1947 for submitted_capability in &submitted.capabilities {
1948 if base_by_name.contains_key(submitted_capability.name.as_str()) {
1949 continue;
1950 }
1951 changes.push(CapabilityChange::CapabilityAdded {
1952 name: submitted_capability.name.as_str().to_owned(),
1953 });
1954 }
1955
1956 if common_name_sequence_differs(
1957 base.capabilities
1958 .iter()
1959 .map(|capability| capability.name.as_str()),
1960 submitted
1961 .capabilities
1962 .iter()
1963 .map(|capability| capability.name.as_str()),
1964 &base_by_name,
1965 &submitted_by_name,
1966 ) {
1967 changes.push(CapabilityChange::CapabilitiesReordered);
1968 }
1969
1970 changes
1971}
1972
1973fn append_matched_capability_changes(
1974 changes: &mut Vec<CapabilityChange>,
1975 base: &SchemaCapability,
1976 submitted: &SchemaCapability,
1977) {
1978 if base.input.leaf != submitted.input.leaf {
1979 changes.push(CapabilityChange::InputLeafChanged {
1980 name: base.name.as_str().to_owned(),
1981 });
1982 }
1983 if base.input.name != submitted.input.name {
1984 changes.push(CapabilityChange::InputNameChanged {
1985 name: base.name.as_str().to_owned(),
1986 });
1987 }
1988 if base.output.leaf != submitted.output.leaf {
1989 changes.push(CapabilityChange::OutputLeafChanged {
1990 name: base.name.as_str().to_owned(),
1991 });
1992 }
1993 if base.docs != submitted.docs {
1994 changes.push(CapabilityChange::CapabilityDocsChanged {
1995 name: base.name.as_str().to_owned(),
1996 });
1997 }
1998 if base.deprecation != submitted.deprecation {
1999 changes.push(CapabilityChange::CapabilityDeprecationChanged {
2000 name: base.name.as_str().to_owned(),
2001 });
2002 }
2003 if base.error != submitted.error {
2004 changes.push(CapabilityChange::CapabilityErrorChanged {
2005 name: base.name.as_str().to_owned(),
2006 });
2007 }
2008 if base.max_exposure != submitted.max_exposure {
2009 changes.push(CapabilityChange::CapabilityExposureChanged {
2010 name: base.name.as_str().to_owned(),
2011 base: base.max_exposure,
2012 submitted: submitted.max_exposure,
2013 });
2014 }
2015 if base.idempotency != submitted.idempotency {
2016 changes.push(CapabilityChange::CapabilityIdempotencyChanged {
2017 name: base.name.as_str().to_owned(),
2018 base: base.idempotency,
2019 submitted: submitted.idempotency,
2020 });
2021 }
2022}
2023
2024fn append_matched_type_changes(
2025 changes: &mut Vec<TypeChange>,
2026 base: &SchemaType,
2027 submitted: &SchemaType,
2028 roles: Roles,
2029) {
2030 if base.docs != submitted.docs {
2031 changes.push(TypeChange::TypeDocsChanged {
2032 name: base.name.clone(),
2033 });
2034 }
2035 if base.deprecation != submitted.deprecation {
2036 changes.push(TypeChange::TypeDeprecationChanged {
2037 name: base.name.clone(),
2038 });
2039 }
2040
2041 let base_by_name = index_variants(&base.variants);
2042 let submitted_by_name = index_variants(&submitted.variants);
2043
2044 for base_variant in &base.variants {
2045 match submitted_by_name.get(base_variant.name.as_str()) {
2046 None => {
2047 changes.push(TypeChange::VariantRemoved {
2048 type_name: base.name.clone(),
2049 variant_name: base_variant.name.clone(),
2050 roles,
2051 });
2052 }
2053 Some(submitted_variant) => {
2054 append_matched_variant_changes(
2055 changes,
2056 base.name.as_str(),
2057 base_variant,
2058 submitted_variant,
2059 roles,
2060 );
2061 }
2062 }
2063 }
2064
2065 for submitted_variant in &submitted.variants {
2066 if base_by_name.contains_key(submitted_variant.name.as_str()) {
2067 continue;
2068 }
2069 changes.push(TypeChange::VariantAdded {
2070 type_name: base.name.clone(),
2071 variant_name: submitted_variant.name.clone(),
2072 roles,
2073 });
2074 }
2075
2076 if common_name_sequence_differs(
2077 base.variants.iter().map(|variant| variant.name.as_str()),
2078 submitted
2079 .variants
2080 .iter()
2081 .map(|variant| variant.name.as_str()),
2082 &base_by_name,
2083 &submitted_by_name,
2084 ) {
2085 changes.push(TypeChange::VariantsReordered {
2086 type_name: base.name.clone(),
2087 });
2088 }
2089}
2090
2091fn append_matched_variant_changes(
2092 changes: &mut Vec<TypeChange>,
2093 type_name: &str,
2094 base: &SchemaVariant,
2095 submitted: &SchemaVariant,
2096 roles: Roles,
2097) {
2098 if base.docs != submitted.docs {
2099 changes.push(TypeChange::VariantDocsChanged {
2100 type_name: type_name.to_owned(),
2101 variant_name: base.name.clone(),
2102 });
2103 }
2104 if base.deprecation != submitted.deprecation {
2105 changes.push(TypeChange::VariantDeprecationChanged {
2106 type_name: type_name.to_owned(),
2107 variant_name: base.name.clone(),
2108 });
2109 }
2110 match (&base.payload, &submitted.payload) {
2111 (SchemaPayload::Named(base_fields), SchemaPayload::Named(submitted_fields)) => {
2112 append_named_field_changes(
2113 changes,
2114 type_name,
2115 base.name.as_str(),
2116 base_fields,
2117 submitted_fields,
2118 roles,
2119 );
2120 }
2121 (
2122 SchemaPayload::Value {
2123 docs: base_docs,
2124 deprecation: base_deprecation,
2125 ty: base_type,
2126 },
2127 SchemaPayload::Value {
2128 docs: submitted_docs,
2129 deprecation: submitted_deprecation,
2130 ty: submitted_type,
2131 },
2132 ) => {
2133 if base_docs != submitted_docs {
2134 changes.push(TypeChange::PayloadDocsChanged {
2135 type_name: type_name.to_owned(),
2136 variant_name: base.name.clone(),
2137 });
2138 }
2139 if base_deprecation != submitted_deprecation {
2140 changes.push(TypeChange::PayloadDeprecationChanged {
2141 type_name: type_name.to_owned(),
2142 variant_name: base.name.clone(),
2143 });
2144 }
2145 if base_type != submitted_type {
2146 changes.push(TypeChange::PayloadTypeChanged {
2147 type_name: type_name.to_owned(),
2148 variant_name: base.name.clone(),
2149 });
2150 }
2151 }
2152 (base_payload, submitted_payload) if base_payload != submitted_payload => {
2153 changes.push(TypeChange::VariantPayloadChanged {
2154 type_name: type_name.to_owned(),
2155 variant_name: base.name.clone(),
2156 roles,
2157 });
2158 }
2159 _ => {}
2160 }
2161}
2162
2163fn index_fields(fields: &[SchemaField]) -> BTreeMap<&str, &SchemaField> {
2164 let mut index = BTreeMap::new();
2165 for field in fields {
2166 index.insert(field.name.as_str(), field);
2167 }
2168 index
2169}
2170
2171fn append_named_field_changes(
2172 changes: &mut Vec<TypeChange>,
2173 type_name: &str,
2174 variant_name: &str,
2175 base_fields: &[SchemaField],
2176 submitted_fields: &[SchemaField],
2177 roles: Roles,
2178) {
2179 let base_by_name = index_fields(base_fields);
2180 let submitted_by_name = index_fields(submitted_fields);
2181
2182 for base_field in base_fields {
2183 match submitted_by_name.get(base_field.name.as_str()) {
2184 None => changes.push(TypeChange::FieldRemoved {
2185 type_name: type_name.to_owned(),
2186 variant_name: variant_name.to_owned(),
2187 field_name: base_field.name.clone(),
2188 roles,
2189 }),
2190 Some(submitted_field) => {
2191 if base_field.docs != submitted_field.docs {
2192 changes.push(TypeChange::FieldDocsChanged {
2193 type_name: type_name.to_owned(),
2194 variant_name: variant_name.to_owned(),
2195 field_name: base_field.name.clone(),
2196 });
2197 }
2198 if base_field.deprecation != submitted_field.deprecation {
2199 changes.push(TypeChange::FieldDeprecationChanged {
2200 type_name: type_name.to_owned(),
2201 variant_name: variant_name.to_owned(),
2202 field_name: base_field.name.clone(),
2203 });
2204 }
2205 if base_field.ty != submitted_field.ty {
2206 changes.push(TypeChange::FieldTypeChanged {
2207 type_name: type_name.to_owned(),
2208 variant_name: variant_name.to_owned(),
2209 field_name: base_field.name.clone(),
2210 });
2211 }
2212 }
2213 }
2214 }
2215
2216 for submitted_field in submitted_fields {
2217 if !base_by_name.contains_key(submitted_field.name.as_str()) {
2218 changes.push(TypeChange::FieldAdded {
2219 type_name: type_name.to_owned(),
2220 variant_name: variant_name.to_owned(),
2221 field_name: submitted_field.name.clone(),
2222 roles,
2223 });
2224 }
2225 }
2226
2227 if common_name_sequence_differs(
2228 base_fields.iter().map(|field| field.name.as_str()),
2229 submitted_fields.iter().map(|field| field.name.as_str()),
2230 &base_by_name,
2231 &submitted_by_name,
2232 ) {
2233 changes.push(TypeChange::FieldsReordered {
2234 type_name: type_name.to_owned(),
2235 variant_name: variant_name.to_owned(),
2236 });
2237 }
2238}
2239
2240fn equal_modulo_provenance(base: &SchemaDocument, submitted: &SchemaDocument) -> bool {
2241 let SchemaDocument {
2242 box_id: base_box_id,
2243 capabilities: base_capabilities,
2244 data_types: base_data_types,
2245 provenance: _,
2246 revision: base_revision,
2247 types: base_types,
2248 } = base;
2249 let SchemaDocument {
2250 box_id: submitted_box_id,
2251 capabilities: submitted_capabilities,
2252 data_types: submitted_data_types,
2253 provenance: _,
2254 revision: submitted_revision,
2255 types: submitted_types,
2256 } = submitted;
2257 base_box_id == submitted_box_id
2258 && base_capabilities == submitted_capabilities
2259 && base_data_types == submitted_data_types
2260 && base_revision == submitted_revision
2261 && base_types == submitted_types
2262}
2263
2264fn report(findings: Vec<Finding>) -> ClassificationReport {
2265 let mut findings = findings;
2266 findings.sort_by(|left, right| {
2267 left.path
2268 .cmp(&right.path)
2269 .then_with(|| left.code.cmp(right.code))
2270 });
2271 let verdict = findings
2272 .iter()
2273 .map(|finding| finding.class)
2274 .max()
2275 .unwrap_or(Class::Unchanged);
2276 ClassificationReport { findings, verdict }
2277}
2278
2279mod report;
2280pub use report::{render_json, render_text};
2281
2282#[cfg(test)]
2283mod tests;