1use crate::{Capabilities, Error, Result};
4use opcda_bridge_proto::bridge as proto;
5use serde::Serialize;
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum CompatibilityFeature {
12 Core,
13 Namespace,
14 IndexedSearch,
15}
16
17impl CompatibilityFeature {
18 fn from_proto(value: i32) -> Result<Self> {
19 match proto::ProtocolFeatureKind::try_from(value).map_err(|_| {
20 Error::Protocol(format!("gateway returned unknown protocol feature {value}"))
21 })? {
22 proto::ProtocolFeatureKind::Core => Ok(Self::Core),
23 proto::ProtocolFeatureKind::Namespace => Ok(Self::Namespace),
24 proto::ProtocolFeatureKind::IndexedSearch => Ok(Self::IndexedSearch),
25 proto::ProtocolFeatureKind::Unspecified => Err(Error::Protocol(
26 "gateway returned an unspecified protocol feature".into(),
27 )),
28 }
29 }
30}
31
32impl fmt::Display for CompatibilityFeature {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(match self {
35 Self::Core => "core",
36 Self::Namespace => "namespace",
37 Self::IndexedSearch => "indexed-search",
38 })
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
44pub struct ProtocolVersionRange {
45 pub min: u32,
46 pub max: u32,
47}
48
49impl ProtocolVersionRange {
50 pub const fn new(min: u32, max: u32) -> Option<Self> {
52 if min > max {
53 None
54 } else {
55 Some(Self { min, max })
56 }
57 }
58
59 pub const fn exact(version: u32) -> Self {
61 Self {
62 min: version,
63 max: version,
64 }
65 }
66
67 pub const fn overlaps(self, other: Self) -> bool {
69 self.min <= other.max && other.min <= self.max
70 }
71
72 const fn negotiated_version(self, other: Self) -> Option<u32> {
73 if self.overlaps(other) {
74 Some(if self.min > other.min {
75 self.min
76 } else {
77 other.min
78 })
79 } else {
80 None
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
87pub struct ProtocolFeatureSupport {
88 pub feature: CompatibilityFeature,
89 pub versions: ProtocolVersionRange,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
94pub struct GatewayInfo {
95 pub application_version: String,
96 pub compatibility_schema_version: u32,
97 pub features: Vec<ProtocolFeatureSupport>,
98}
99
100impl TryFrom<proto::GetGatewayInfoResponse> for GatewayInfo {
101 type Error = Error;
102
103 fn try_from(value: proto::GetGatewayInfoResponse) -> Result<Self> {
104 let features = value
105 .features
106 .into_iter()
107 .map(|feature| {
108 let feature_kind = CompatibilityFeature::from_proto(feature.kind)?;
109 let versions = ProtocolVersionRange::new(feature.min_version, feature.max_version)
110 .ok_or_else(|| {
111 Error::Protocol(format!(
112 "gateway returned reversed {feature_kind} protocol version range"
113 ))
114 })?;
115 Ok(ProtocolFeatureSupport {
116 feature: feature_kind,
117 versions,
118 })
119 })
120 .collect::<Result<Vec<_>>>()?;
121 Ok(Self {
122 application_version: value.application_version,
123 compatibility_schema_version: value.compatibility_schema_version,
124 features,
125 })
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
131#[serde(rename_all = "snake_case")]
132pub enum CompatibilitySource {
133 GatewayInfo,
134 LegacyCapabilities,
135 Unknown,
136}
137
138impl fmt::Display for CompatibilitySource {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 f.write_str(match self {
141 Self::GatewayInfo => "gateway-info",
142 Self::LegacyCapabilities => "legacy-capabilities",
143 Self::Unknown => "unknown",
144 })
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
150pub struct ProtocolProfile {
151 pub application_version: Option<String>,
152 pub source: CompatibilitySource,
153 pub features: Vec<ProtocolFeatureSupport>,
154}
155
156impl ProtocolProfile {
157 pub fn from_gateway_info(info: &GatewayInfo) -> Self {
159 Self {
160 application_version: Some(info.application_version.clone()),
161 source: CompatibilitySource::GatewayInfo,
162 features: info.features.clone(),
163 }
164 }
165
166 fn feature(&self, feature: CompatibilityFeature) -> Option<ProtocolVersionRange> {
167 self.features
168 .iter()
169 .find(|support| support.feature == feature)
170 .map(|support| support.versions)
171 }
172}
173
174pub fn current_client_profile(application_version: impl Into<String>) -> ProtocolProfile {
176 let application_version = application_version.into();
177 let release_line =
178 opcda_bridge_proto::compatibility::release_line_for(env!("CARGO_PKG_VERSION"))
179 .expect("reusable client package version must be in the compatibility catalog");
180 ProtocolProfile {
181 application_version: Some(application_version),
182 source: CompatibilitySource::GatewayInfo,
183 features: vec![
184 ProtocolFeatureSupport {
185 feature: CompatibilityFeature::Core,
186 versions: ProtocolVersionRange::exact(release_line.core_protocol),
187 },
188 ProtocolFeatureSupport {
189 feature: CompatibilityFeature::Namespace,
190 versions: ProtocolVersionRange::exact(release_line.namespace_protocol),
191 },
192 ProtocolFeatureSupport {
193 feature: CompatibilityFeature::IndexedSearch,
194 versions: ProtocolVersionRange::exact(release_line.indexed_search_protocol),
195 },
196 ],
197 }
198}
199
200pub fn legacy_gateway_profile(capabilities: &Capabilities) -> ProtocolProfile {
202 let mut features = Vec::new();
203 let release_line =
204 opcda_bridge_proto::compatibility::release_line_for(&capabilities.application_version);
205 if let Some(namespace_version) = parse_namespace_protocol(&capabilities.protocol_version) {
206 features.push(ProtocolFeatureSupport {
207 feature: CompatibilityFeature::Core,
208 versions: ProtocolVersionRange::exact(release_line.map_or(
209 opcda_bridge_proto::compatibility::CORE_PROTOCOL_VERSION,
210 |line| line.core_protocol,
211 )),
212 });
213 features.push(ProtocolFeatureSupport {
214 feature: CompatibilityFeature::Namespace,
215 versions: ProtocolVersionRange::exact(namespace_version),
216 });
217 }
218 if capabilities.supports_indexed_search
219 && let Some(index_protocol) =
220 parse_index_protocol(&capabilities.indexed_search_protocol_version)
221 {
222 features.push(ProtocolFeatureSupport {
223 feature: CompatibilityFeature::IndexedSearch,
224 versions: ProtocolVersionRange::exact(index_protocol),
225 });
226 }
227 ProtocolProfile {
228 application_version: Some(capabilities.application_version.clone()),
229 source: CompatibilitySource::LegacyCapabilities,
230 features,
231 }
232}
233
234fn catalog_line(version: &str) -> Option<&'static str> {
235 opcda_bridge_proto::compatibility::release_line_for(version).map(|line| line.name)
236}
237
238fn evidence_status(value: &str) -> Option<CompatibilityEvidence> {
239 match value {
240 "contract-boundary-tested" => Some(CompatibilityEvidence::ContractBoundaryTested),
241 "exact-pair-tested" => Some(CompatibilityEvidence::ExactPairTested),
242 "unverified" => Some(CompatibilityEvidence::Unverified),
243 _ => None,
244 }
245}
246
247fn catalog_evidence(
248 client_version: Option<&str>,
249 gateway_version: Option<&str>,
250) -> CompatibilityEvidence {
251 let (Some(client_version), Some(gateway_version)) = (client_version, gateway_version) else {
252 return CompatibilityEvidence::Unverified;
253 };
254 let (Some(client_line), Some(gateway_line)) =
255 (catalog_line(client_version), catalog_line(gateway_version))
256 else {
257 return CompatibilityEvidence::Unverified;
258 };
259
260 for &(catalog_client_line, catalog_gateway_line, status, exact_client, exact_gateway) in
261 opcda_bridge_proto::compatibility::EVIDENCE
262 {
263 if catalog_client_line == client_line
264 && catalog_gateway_line == gateway_line
265 && !exact_client.is_empty()
266 && exact_client == client_version
267 && exact_gateway == gateway_version
268 && let Some(status) = evidence_status(status)
269 {
270 return status;
271 }
272 }
273 for &(catalog_client_line, catalog_gateway_line, status, exact_client, exact_gateway) in
274 opcda_bridge_proto::compatibility::EVIDENCE
275 {
276 if catalog_client_line == client_line
277 && catalog_gateway_line == gateway_line
278 && exact_client.is_empty()
279 && exact_gateway.is_empty()
280 && let Some(status) = evidence_status(status)
281 {
282 return status;
283 }
284 }
285 CompatibilityEvidence::Unverified
286}
287
288fn parse_namespace_protocol(value: &str) -> Option<u32> {
289 match value.trim() {
290 "1" | "1.0" => Some(1),
291 "2" | "2.0" => Some(2),
292 "0.3" | "0.3.0" => Some(2),
293 _ => None,
294 }
295}
296
297fn parse_index_protocol(value: &str) -> Option<u32> {
298 match value.trim() {
299 "1" | "1.0" => Some(1),
300 _ => None,
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
306pub struct FeatureCompatibility {
307 pub feature: CompatibilityFeature,
308 pub status: FeatureCompatibilityStatus,
309 pub client_versions: ProtocolVersionRange,
310 pub gateway_versions: Option<ProtocolVersionRange>,
311 pub negotiated_version: Option<u32>,
312 pub reason: String,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
317#[serde(rename_all = "snake_case")]
318pub enum FeatureCompatibilityStatus {
319 Compatible,
320 Unsupported,
321 Incompatible,
322 Unknown,
323}
324
325impl fmt::Display for FeatureCompatibilityStatus {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 f.write_str(match self {
328 Self::Compatible => "compatible",
329 Self::Unsupported => "unsupported",
330 Self::Incompatible => "incompatible",
331 Self::Unknown => "unknown",
332 })
333 }
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
338#[serde(rename_all = "snake_case")]
339pub enum CompatibilityStatus {
340 Full,
341 Partial,
342 Incompatible,
343 Unknown,
344}
345
346impl fmt::Display for CompatibilityStatus {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.write_str(match self {
349 Self::Full => "full",
350 Self::Partial => "partial",
351 Self::Incompatible => "incompatible",
352 Self::Unknown => "unknown",
353 })
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
359#[serde(rename_all = "snake_case")]
360pub enum CompatibilityEvidence {
361 ContractBoundaryTested,
362 ExactPairTested,
363 Unverified,
364}
365
366impl fmt::Display for CompatibilityEvidence {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 f.write_str(match self {
369 Self::ContractBoundaryTested => "contract-boundary-tested",
370 Self::ExactPairTested => "exact-pair-tested",
371 Self::Unverified => "unverified",
372 })
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
378pub struct CompatibilityReport {
379 pub client_version: String,
380 pub library_version: String,
381 pub gateway_version: Option<String>,
382 pub source: CompatibilitySource,
383 pub status: CompatibilityStatus,
384 pub evidence: CompatibilityEvidence,
385 pub features: Vec<FeatureCompatibility>,
386}
387
388impl CompatibilityReport {
389 pub fn feature(&self, feature: CompatibilityFeature) -> Option<&FeatureCompatibility> {
391 self.features
392 .iter()
393 .find(|result| result.feature == feature)
394 }
395
396 pub fn satisfies(&self, required: &[CompatibilityFeature]) -> bool {
398 required.iter().all(|feature| {
399 self.feature(*feature)
400 .is_some_and(|result| result.status == FeatureCompatibilityStatus::Compatible)
401 })
402 }
403}
404
405pub fn evaluate_compatibility(
407 client: &ProtocolProfile,
408 gateway: &ProtocolProfile,
409) -> CompatibilityReport {
410 let mut features = Vec::new();
411 for client_support in &client.features {
412 let gateway_versions = gateway.feature(client_support.feature);
413 let (status, negotiated_version, reason) = match gateway_versions {
414 Some(gateway_versions) if client_support.versions.overlaps(gateway_versions) => (
415 FeatureCompatibilityStatus::Compatible,
416 client_support.versions.negotiated_version(gateway_versions),
417 format!(
418 "{feature} protocol ranges overlap",
419 feature = client_support.feature
420 ),
421 ),
422 Some(gateway_versions) => (
423 FeatureCompatibilityStatus::Incompatible,
424 None,
425 format!(
426 "{feature} protocol ranges do not overlap: client {}-{}, gateway {}-{}",
427 client_support.versions.min,
428 client_support.versions.max,
429 gateway_versions.min,
430 gateway_versions.max,
431 feature = client_support.feature
432 ),
433 ),
434 None if client_support.feature == CompatibilityFeature::IndexedSearch => (
435 FeatureCompatibilityStatus::Unsupported,
436 None,
437 "gateway does not advertise indexed search".into(),
438 ),
439 None => (
440 FeatureCompatibilityStatus::Unknown,
441 None,
442 format!(
443 "gateway did not advertise the {feature} protocol",
444 feature = client_support.feature
445 ),
446 ),
447 };
448 features.push(FeatureCompatibility {
449 feature: client_support.feature,
450 status,
451 client_versions: client_support.versions,
452 gateway_versions,
453 negotiated_version,
454 reason,
455 });
456 }
457
458 let core_status = features
459 .iter()
460 .find(|result| result.feature == CompatibilityFeature::Core)
461 .map(|result| result.status);
462 let status = match core_status {
463 Some(FeatureCompatibilityStatus::Compatible) => {
464 if features
465 .iter()
466 .all(|result| result.status == FeatureCompatibilityStatus::Compatible)
467 {
468 CompatibilityStatus::Full
469 } else {
470 CompatibilityStatus::Partial
471 }
472 }
473 Some(FeatureCompatibilityStatus::Incompatible)
474 | Some(FeatureCompatibilityStatus::Unsupported) => CompatibilityStatus::Incompatible,
475 Some(FeatureCompatibilityStatus::Unknown) | None => CompatibilityStatus::Unknown,
476 };
477
478 CompatibilityReport {
479 client_version: client
480 .application_version
481 .clone()
482 .unwrap_or_else(|| "unknown".into()),
483 library_version: env!("CARGO_PKG_VERSION").into(),
484 gateway_version: gateway.application_version.clone(),
485 source: gateway.source,
486 status,
487 evidence: catalog_evidence(
488 client.application_version.as_deref(),
489 gateway.application_version.as_deref(),
490 ),
491 features,
492 }
493}
494
495pub fn unknown_compatibility_report(client_version: impl Into<String>) -> CompatibilityReport {
497 CompatibilityReport {
498 client_version: client_version.into(),
499 library_version: env!("CARGO_PKG_VERSION").into(),
500 gateway_version: None,
501 source: CompatibilitySource::Unknown,
502 status: CompatibilityStatus::Unknown,
503 evidence: CompatibilityEvidence::Unverified,
504 features: Vec::new(),
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 fn capabilities(protocol: &str, indexed: bool) -> Capabilities {
513 Capabilities {
514 application_version: "0.4.3".into(),
515 protocol_version: protocol.into(),
516 max_page_size: 200,
517 supports_browse_sessions: true,
518 supports_search: true,
519 organization: crate::NamespaceOrganization::Hierarchical,
520 source: crate::BrowseSource::Da2,
521 supports_indexed_search: indexed,
522 indexed_search_protocol_version: if indexed { "1" } else { "" }.into(),
523 max_indexed_search_results: 50,
524 search_index_state: crate::SearchIndexState::Ready,
525 search_index_promoting: false,
526 }
527 }
528
529 #[test]
530 fn ranges_validate_and_negotiate() {
531 assert!(ProtocolVersionRange::new(2, 1).is_none());
532 let left = ProtocolVersionRange::new(1, 3).unwrap();
533 let right = ProtocolVersionRange::new(3, 4).unwrap();
534 assert!(left.overlaps(right));
535 assert_eq!(left.negotiated_version(right), Some(3));
536 let higher_left = ProtocolVersionRange::new(4, 6).unwrap();
537 let lower_right = ProtocolVersionRange::new(1, 5).unwrap();
538 assert_eq!(higher_left.negotiated_version(lower_right), Some(4));
539 assert!(!ProtocolVersionRange::exact(1).overlaps(ProtocolVersionRange::exact(2)));
540 }
541
542 #[test]
543 fn feature_display_and_status_display_are_stable() {
544 assert_eq!(
545 CompatibilityFeature::IndexedSearch.to_string(),
546 "indexed-search"
547 );
548 assert_eq!(
549 FeatureCompatibilityStatus::Unsupported.to_string(),
550 "unsupported"
551 );
552 assert_eq!(CompatibilityStatus::Partial.to_string(), "partial");
553 assert_eq!(
554 CompatibilityEvidence::ContractBoundaryTested.to_string(),
555 "contract-boundary-tested"
556 );
557 assert_eq!(CompatibilitySource::GatewayInfo.to_string(), "gateway-info");
558 assert_eq!(
559 CompatibilitySource::LegacyCapabilities.to_string(),
560 "legacy-capabilities"
561 );
562 assert_eq!(CompatibilitySource::Unknown.to_string(), "unknown");
563 }
564
565 #[test]
566 fn current_profile_uses_generated_contract_versions() {
567 let profile = current_client_profile("0.4.3");
568 assert_eq!(
569 profile.feature(CompatibilityFeature::Core),
570 Some(ProtocolVersionRange::exact(1))
571 );
572 assert_eq!(
573 profile.feature(CompatibilityFeature::Namespace),
574 Some(ProtocolVersionRange::exact(2))
575 );
576 assert_eq!(
577 profile.feature(CompatibilityFeature::IndexedSearch),
578 Some(ProtocolVersionRange::exact(1))
579 );
580 assert_eq!(profile.application_version.as_deref(), Some("0.4.3"));
581
582 let relabeled = current_client_profile("application-build");
583 assert_eq!(
584 relabeled.feature(CompatibilityFeature::IndexedSearch),
585 Some(ProtocolVersionRange::exact(1))
586 );
587 }
588
589 #[test]
590 fn legacy_profiles_map_supported_and_unknown_protocol_strings() {
591 let profile = legacy_gateway_profile(&capabilities("2", true));
592 assert_eq!(profile.source, CompatibilitySource::LegacyCapabilities);
593 assert_eq!(
594 profile.feature(CompatibilityFeature::Namespace),
595 Some(ProtocolVersionRange::exact(2))
596 );
597 assert!(
598 profile
599 .feature(CompatibilityFeature::IndexedSearch)
600 .is_some()
601 );
602
603 let old = legacy_gateway_profile(&capabilities("1.0", false));
604 assert_eq!(
605 old.feature(CompatibilityFeature::Namespace),
606 Some(ProtocolVersionRange::exact(1))
607 );
608 assert!(old.feature(CompatibilityFeature::IndexedSearch).is_none());
609
610 let shorthand = legacy_gateway_profile(&capabilities("0.3", false));
611 assert_eq!(
612 shorthand.feature(CompatibilityFeature::Namespace),
613 Some(ProtocolVersionRange::exact(2))
614 );
615
616 let unknown = legacy_gateway_profile(&capabilities("future", false));
617 assert!(unknown.features.is_empty());
618
619 let mut unknown_version = capabilities("2", true);
620 unknown_version.application_version = "future".into();
621 let unknown_version_profile = legacy_gateway_profile(&unknown_version);
622 assert_eq!(
623 unknown_version_profile.feature(CompatibilityFeature::Core),
624 Some(ProtocolVersionRange::exact(1))
625 );
626 assert_eq!(
627 unknown_version_profile.feature(CompatibilityFeature::IndexedSearch),
628 Some(ProtocolVersionRange::exact(1))
629 );
630 let mut invalid_index = capabilities("2", true);
631 invalid_index.indexed_search_protocol_version = "future".into();
632 assert!(
633 legacy_gateway_profile(&invalid_index)
634 .feature(CompatibilityFeature::IndexedSearch)
635 .is_none()
636 );
637 }
638
639 #[test]
640 fn evaluates_full_partial_and_incompatible_profiles() {
641 let client = current_client_profile("0.4.3");
642 let full = evaluate_compatibility(
643 &client,
644 &ProtocolProfile {
645 application_version: Some("0.4.3".into()),
646 source: CompatibilitySource::GatewayInfo,
647 features: client.features.clone(),
648 },
649 );
650 assert_eq!(full.status, CompatibilityStatus::Full);
651 assert_eq!(full.evidence, CompatibilityEvidence::ExactPairTested);
652 assert_eq!(full.library_version, env!("CARGO_PKG_VERSION"));
653 assert!(full.satisfies(&[CompatibilityFeature::Core]));
654
655 let partial = evaluate_compatibility(
656 &client,
657 &ProtocolProfile {
658 application_version: Some("0.3.2".into()),
659 source: CompatibilitySource::LegacyCapabilities,
660 features: vec![
661 ProtocolFeatureSupport {
662 feature: CompatibilityFeature::Core,
663 versions: ProtocolVersionRange::exact(1),
664 },
665 ProtocolFeatureSupport {
666 feature: CompatibilityFeature::Namespace,
667 versions: ProtocolVersionRange::exact(2),
668 },
669 ],
670 },
671 );
672 assert_eq!(partial.status, CompatibilityStatus::Partial);
673 assert_eq!(
674 partial
675 .feature(CompatibilityFeature::IndexedSearch)
676 .unwrap()
677 .status,
678 FeatureCompatibilityStatus::Unsupported
679 );
680 assert_eq!(
681 partial.evidence,
682 CompatibilityEvidence::ContractBoundaryTested
683 );
684 assert!(!partial.satisfies(&[CompatibilityFeature::IndexedSearch]));
685
686 let incompatible = evaluate_compatibility(
687 &client,
688 &ProtocolProfile {
689 application_version: Some("future".into()),
690 source: CompatibilitySource::GatewayInfo,
691 features: vec![ProtocolFeatureSupport {
692 feature: CompatibilityFeature::Core,
693 versions: ProtocolVersionRange::exact(2),
694 }],
695 },
696 );
697 assert_eq!(incompatible.status, CompatibilityStatus::Incompatible);
698 assert_eq!(
699 incompatible
700 .feature(CompatibilityFeature::Core)
701 .unwrap()
702 .status,
703 FeatureCompatibilityStatus::Incompatible
704 );
705
706 let unknown = evaluate_compatibility(
707 &client,
708 &ProtocolProfile {
709 application_version: Some("0.4.3".into()),
710 source: CompatibilitySource::GatewayInfo,
711 features: Vec::new(),
712 },
713 );
714 assert_eq!(unknown.status, CompatibilityStatus::Unknown);
715 assert!(!unknown.satisfies(&[CompatibilityFeature::Core]));
716 }
717
718 #[test]
719 fn evaluates_unknown_and_converts_gateway_info() {
720 let unknown = unknown_compatibility_report("0.4.3");
721 assert_eq!(unknown.status, CompatibilityStatus::Unknown);
722 assert!(!unknown.satisfies(&[CompatibilityFeature::Core]));
723
724 let info = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
725 application_version: "0.4.3".into(),
726 compatibility_schema_version: 1,
727 features: vec![proto::ProtocolFeature {
728 kind: proto::ProtocolFeatureKind::Core as i32,
729 min_version: 1,
730 max_version: 2,
731 }],
732 })
733 .unwrap();
734 assert_eq!(info.features[0].versions.max, 2);
735 }
736
737 #[test]
738 fn rejects_invalid_gateway_feature_data() {
739 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
740 features: vec![proto::ProtocolFeature {
741 kind: 99,
742 min_version: 1,
743 max_version: 1,
744 }],
745 ..Default::default()
746 })
747 .unwrap_err();
748 assert!(error.to_string().contains("unknown protocol feature"));
749
750 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
751 features: vec![proto::ProtocolFeature {
752 kind: proto::ProtocolFeatureKind::Core as i32,
753 min_version: 2,
754 max_version: 1,
755 }],
756 ..Default::default()
757 })
758 .unwrap_err();
759 assert!(error.to_string().contains("reversed core"));
760
761 let error = GatewayInfo::try_from(proto::GetGatewayInfoResponse {
762 features: vec![proto::ProtocolFeature {
763 kind: proto::ProtocolFeatureKind::Unspecified as i32,
764 min_version: 1,
765 max_version: 1,
766 }],
767 ..Default::default()
768 })
769 .unwrap_err();
770 assert!(error.to_string().contains("unspecified protocol feature"));
771 }
772
773 #[test]
774 fn catalog_evidence_handles_exact_boundary_and_unknown_versions() {
775 assert_eq!(catalog_line("0.3.1"), Some("legacy"));
776 assert_eq!(catalog_line("0.3.2"), Some("paged"));
777 assert_eq!(catalog_line("0.4.0"), Some("indexed"));
778 assert_eq!(catalog_line("1.0.0"), None);
779 assert_eq!(
780 catalog_evidence(Some("0.4.0"), Some("0.4.3")),
781 CompatibilityEvidence::ExactPairTested
782 );
783 assert_eq!(
784 catalog_evidence(Some("0.4.3"), Some("0.3.2")),
785 CompatibilityEvidence::ContractBoundaryTested
786 );
787 assert_eq!(
788 catalog_evidence(Some("0.3.2"), Some("0.4.3")),
789 CompatibilityEvidence::ContractBoundaryTested
790 );
791 assert_eq!(
792 catalog_evidence(Some("0.3.1"), Some("0.3.1")),
793 CompatibilityEvidence::Unverified
794 );
795 assert_eq!(
796 catalog_evidence(Some("future"), Some("0.4.3")),
797 CompatibilityEvidence::Unverified
798 );
799 assert_eq!(
800 catalog_evidence(None, Some("0.4.3")),
801 CompatibilityEvidence::Unverified
802 );
803 assert_eq!(
804 evidence_status("unverified"),
805 Some(CompatibilityEvidence::Unverified)
806 );
807 assert!(catalog_line("0.4").is_none());
808 assert!(catalog_line("0.x.3").is_none());
809 assert_eq!(evidence_status("invalid"), None);
810 }
811}