1use std::{
5 borrow::Cow,
6 collections::BTreeSet,
7 fmt::{Display, Write},
8};
9
10use konst::{iter, slice, string};
11use percent_encoding::utf8_percent_encode;
12use tracing::warn;
13
14use super::{FeatureFlag, MatrixVersion, SupportedVersions, error::IntoHttpError};
15use crate::percent_encode::PATH_PERCENT_ENCODE_SET;
16
17pub trait PathBuilder: Sized {
22 type Input<'a>;
24
25 fn select_path(&self, input: Self::Input<'_>) -> Result<&'static str, IntoHttpError>;
29
30 fn make_endpoint_url(
49 &self,
50 input: Self::Input<'_>,
51 base_url: &str,
52 path_args: &[&dyn Display],
53 query_string: &str,
54 ) -> Result<String, IntoHttpError> {
55 let path_with_placeholders = self.select_path(input)?;
56
57 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
58 let mut segments = path_with_placeholders.split('/');
59 let mut path_args = path_args.iter();
60
61 let first_segment = segments.next().expect("split iterator is never empty");
62 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
63
64 for segment in segments {
65 if extract_endpoint_path_segment_variable(segment).is_some() {
66 let arg = path_args
67 .next()
68 .expect("number of placeholders must match number of arguments")
69 .to_string();
70 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
71
72 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
73 } else {
74 res.reserve(segment.len() + 1);
75 res.push('/');
76 res.push_str(segment);
77 }
78 }
79
80 if !query_string.is_empty() {
81 res.push('?');
82 res.push_str(query_string);
83 }
84
85 Ok(res)
86 }
87
88 fn all_paths(&self) -> impl Iterator<Item = &'static str>;
92
93 #[doc(hidden)]
97 fn _path_parameters(&self) -> Vec<&'static str>;
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
105#[allow(clippy::exhaustive_structs)]
106pub struct VersionHistory {
107 unstable_paths: &'static [(Option<&'static str>, &'static str)],
112
113 stable_paths: &'static [(StablePathSelector, &'static str)],
117
118 deprecated: Option<MatrixVersion>,
125
126 removed: Option<MatrixVersion>,
131}
132
133impl VersionHistory {
134 pub const fn new(
156 unstable_paths: &'static [(Option<&'static str>, &'static str)],
157 stable_paths: &'static [(StablePathSelector, &'static str)],
158 deprecated: Option<MatrixVersion>,
159 removed: Option<MatrixVersion>,
160 ) -> Self {
161 const fn check_path_args_equal(first: &'static str, second: &'static str) {
162 let mut second_iter = string::split(second, "/");
163
164 iter::for_each!(first_s in string::split(first, '/') => {
165 if let Some(first_arg) = extract_endpoint_path_segment_variable(first_s) {
166 let second_next_arg: Option<&'static str> = loop {
167 let Some(second_s) = second_iter.next() else {
168 break None;
169 };
170
171 let maybe_second_arg = extract_endpoint_path_segment_variable(second_s);
172
173 if let Some(second_arg) = maybe_second_arg {
174 break Some(second_arg);
175 }
176 };
177
178 if let Some(second_next_arg) = second_next_arg {
179 if !string::eq_str(second_next_arg, first_arg) {
180 panic!("names of endpoint path segment variables do not match");
181 }
182 } else {
183 panic!("counts of endpoint path segment variables do not match");
184 }
185 }
186 });
187
188 while let Some(second_s) = second_iter.next() {
190 if extract_endpoint_path_segment_variable(second_s).is_some() {
191 panic!("counts of endpoint path segment variables do not match");
192 }
193 }
194 }
195
196 let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
198 s
199 } else if let Some((_, s)) = stable_paths.first() {
200 s
201 } else {
202 panic!("no endpoint paths supplied")
203 };
204
205 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
206 check_path_is_valid(unstable_path.1);
207 check_path_args_equal(ref_path, unstable_path.1);
208 });
209
210 let mut prev_seen_version: Option<MatrixVersion> = None;
211
212 iter::for_each!(version_path in slice::iter(stable_paths) => {
213 check_path_is_valid(version_path.1);
214 check_path_args_equal(ref_path, version_path.1);
215
216 if let Some(current_version) = version_path.0.version() {
217 if let Some(prev_seen_version) = prev_seen_version {
218 let cmp_result = current_version.const_ord(&prev_seen_version);
219
220 if cmp_result.is_eq() {
221 panic!("duplicate matrix version in stable paths")
223 } else if cmp_result.is_lt() {
224 panic!("stable paths are not in ascending order")
226 }
227 }
228
229 prev_seen_version = Some(current_version);
230 }
231 });
232
233 if let Some(deprecated) = deprecated {
234 if let Some(prev_seen_version) = prev_seen_version {
235 let ord_result = prev_seen_version.const_ord(&deprecated);
236 if !deprecated.is_legacy() && ord_result.is_eq() {
237 panic!("deprecated version is equal to latest stable path version")
241 } else if ord_result.is_gt() {
242 panic!("deprecated version is older than latest stable path version")
244 }
245 } else {
246 panic!("defined deprecated version while no stable path exists")
247 }
248 }
249
250 if let Some(removed) = removed {
251 if let Some(deprecated) = deprecated {
252 let ord_result = deprecated.const_ord(&removed);
253 if ord_result.is_eq() {
254 panic!("removed version is equal to deprecated version")
256 } else if ord_result.is_gt() {
257 panic!("removed version is older than deprecated version")
259 }
260 } else {
261 panic!("defined removed version while no deprecated version exists")
262 }
263 }
264
265 Self { unstable_paths, stable_paths, deprecated, removed }
266 }
267
268 pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
277 match self.versioning_decision_for(&considering.versions) {
278 VersioningDecision::Removed => false,
279 VersioningDecision::Version { .. } => true,
280 VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
281 }
282 }
283
284 pub fn versioning_decision_for(
296 &self,
297 versions: &BTreeSet<MatrixVersion>,
298 ) -> VersioningDecision {
299 let is_superset_any =
300 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
301 let is_superset_all =
302 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
303
304 if self.removed.is_some_and(is_superset_all) {
306 return VersioningDecision::Removed;
307 }
308
309 if self.added_in().is_some_and(is_superset_any) {
311 let all_deprecated = self.deprecated.is_some_and(is_superset_all);
312
313 return VersioningDecision::Version {
314 any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
315 all_deprecated,
316 any_removed: self.removed.is_some_and(is_superset_any),
317 };
318 }
319
320 VersioningDecision::Feature
321 }
322
323 pub fn added_in(&self) -> Option<MatrixVersion> {
327 self.stable_paths.iter().find_map(|(v, _)| v.version())
328 }
329
330 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
332 self.deprecated
333 }
334
335 pub fn removed_in(&self) -> Option<MatrixVersion> {
337 self.removed
338 }
339
340 pub fn unstable(&self) -> Option<&'static str> {
342 self.unstable_paths.last().map(|(_, path)| *path)
343 }
344
345 pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
347 self.unstable_paths.iter().copied()
348 }
349
350 pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
352 self.stable_paths.iter().copied()
353 }
354
355 pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
367 let version_paths = self
368 .stable_paths
369 .iter()
370 .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
371
372 for (ver, path) in version_paths.rev() {
374 if versions.iter().any(|v| v.is_superset_of(ver)) {
376 return Some(path);
377 }
378 }
379
380 None
381 }
382
383 pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
385 let unstable_feature_paths = self
386 .unstable_paths
387 .iter()
388 .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
389 let stable_feature_paths = self
390 .stable_paths
391 .iter()
392 .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
393
394 for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
396 if supported_features.iter().any(|supported| supported.as_str() == feature) {
398 return Some(path);
399 }
400 }
401
402 None
403 }
404}
405
406impl PathBuilder for VersionHistory {
407 type Input<'a> = Cow<'a, SupportedVersions>;
408
409 fn select_path(
420 &self,
421 input: Cow<'_, SupportedVersions>,
422 ) -> Result<&'static str, IntoHttpError> {
423 match self.versioning_decision_for(&input.versions) {
424 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
425 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
426 )),
427 VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
428 if any_removed {
429 if all_deprecated {
430 warn!(
431 "endpoint is removed in some (and deprecated in ALL) \
432 of the following versions: {:?}",
433 input.versions
434 );
435 } else if any_deprecated {
436 warn!(
437 "endpoint is removed (and deprecated) in some of the \
438 following versions: {:?}",
439 input.versions
440 );
441 } else {
442 unreachable!("any_removed implies *_deprecated");
443 }
444 } else if all_deprecated {
445 warn!(
446 "endpoint is deprecated in ALL of the following versions: {:?}",
447 input.versions
448 );
449 } else if any_deprecated {
450 warn!(
451 "endpoint is deprecated in some of the following versions: {:?}",
452 input.versions
453 );
454 }
455
456 Ok(self
457 .version_path(&input.versions)
458 .expect("VersioningDecision::Version implies that a version path exists"))
459 }
460 VersioningDecision::Feature => self
461 .feature_path(&input.features)
462 .or_else(|| self.unstable())
463 .ok_or(IntoHttpError::NoUnstablePath),
464 }
465 }
466
467 fn all_paths(&self) -> impl Iterator<Item = &'static str> {
468 self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
469 }
470
471 fn _path_parameters(&self) -> Vec<&'static str> {
472 let path = self.all_paths().next().unwrap();
473 path.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
474 }
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
479#[allow(clippy::exhaustive_enums)]
480pub enum VersioningDecision {
481 Feature,
483
484 Version {
486 any_deprecated: bool,
488
489 all_deprecated: bool,
491
492 any_removed: bool,
494 },
495
496 Removed,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502#[allow(clippy::exhaustive_enums)]
503pub enum StablePathSelector {
504 Feature(&'static str),
506
507 Version(MatrixVersion),
509
510 FeatureAndVersion {
512 feature: &'static str,
514 version: MatrixVersion,
516 },
517}
518
519impl StablePathSelector {
520 pub const fn feature(self) -> Option<&'static str> {
522 match self {
523 Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
524 _ => None,
525 }
526 }
527
528 pub const fn version(self) -> Option<MatrixVersion> {
530 match self {
531 Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
532 _ => None,
533 }
534 }
535}
536
537impl From<MatrixVersion> for StablePathSelector {
538 fn from(value: MatrixVersion) -> Self {
539 Self::Version(value)
540 }
541}
542
543#[derive(Clone, Debug, PartialEq, Eq)]
548#[allow(clippy::exhaustive_structs)]
549pub struct SinglePath(&'static str);
550
551impl SinglePath {
552 pub const fn new(path: &'static str) -> Self {
554 check_path_is_valid(path);
555
556 iter::for_each!(segment in string::split(path, '/') => {
558 extract_endpoint_path_segment_variable(segment);
559 });
560
561 Self(path)
562 }
563
564 pub fn path(&self) -> &'static str {
566 self.0
567 }
568}
569
570impl PathBuilder for SinglePath {
571 type Input<'a> = ();
572
573 fn select_path(&self, _input: ()) -> Result<&'static str, IntoHttpError> {
574 Ok(self.0)
575 }
576
577 fn all_paths(&self) -> impl Iterator<Item = &'static str> {
578 std::iter::once(self.0)
579 }
580
581 fn _path_parameters(&self) -> Vec<&'static str> {
582 self.0.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
583 }
584}
585
586const fn check_path_is_valid(path: &'static str) {
590 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
591 match *path_b {
592 0x21..=0x7E => {},
593 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
594 }
595 });
596}
597
598pub const fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
611 if string::starts_with(segment, ':') {
612 panic!("endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`");
613 }
614
615 if let Some(s) = string::strip_prefix(segment, '{') {
616 let var = string::strip_suffix(s, '}')
617 .expect("endpoint path segment variable braces mismatch: missing ending `}`");
618 return Some(var);
619 }
620
621 if string::ends_with(segment, '}') {
622 panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
623 }
624
625 None
626}
627
628#[cfg(test)]
629mod tests {
630 use std::{
631 borrow::Cow,
632 collections::{BTreeMap, BTreeSet},
633 };
634
635 use assert_matches2::assert_matches;
636
637 use super::{PathBuilder, StablePathSelector, VersionHistory};
638 use crate::api::{
639 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
640 SupportedVersions,
641 error::IntoHttpError,
642 };
643
644 fn stable_only_history(
645 stable_paths: &'static [(StablePathSelector, &'static str)],
646 ) -> VersionHistory {
647 VersionHistory { unstable_paths: &[], stable_paths, deprecated: None, removed: None }
648 }
649
650 fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
651 SupportedVersions {
652 versions: versions.iter().copied().collect(),
653 features: BTreeSet::new(),
654 }
655 }
656
657 #[test]
660 fn make_simple_endpoint_url() {
661 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s")]);
662
663 let url = history
664 .make_endpoint_url(
665 Cow::Owned(version_only_supported(&[V1_0])),
666 "https://example.org",
667 &[],
668 "",
669 )
670 .unwrap();
671 assert_eq!(url, "https://example.org/s");
672 }
673
674 #[test]
675 fn make_endpoint_url_with_path_args() {
676 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
677 let url = history
678 .make_endpoint_url(
679 Cow::Owned(version_only_supported(&[V1_0])),
680 "https://example.org",
681 &[&"123"],
682 "",
683 )
684 .unwrap();
685 assert_eq!(url, "https://example.org/s/123");
686 }
687
688 #[test]
689 fn make_endpoint_url_with_path_args_with_dash() {
690 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
691 let url = history
692 .make_endpoint_url(
693 Cow::Owned(version_only_supported(&[V1_0])),
694 "https://example.org",
695 &[&"my-path"],
696 "",
697 )
698 .unwrap();
699 assert_eq!(url, "https://example.org/s/my-path");
700 }
701
702 #[test]
703 fn make_endpoint_url_with_path_args_with_reserved_char() {
704 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
705 let url = history
706 .make_endpoint_url(
707 Cow::Owned(version_only_supported(&[V1_0])),
708 "https://example.org",
709 &[&"#path"],
710 "",
711 )
712 .unwrap();
713 assert_eq!(url, "https://example.org/s/%23path");
714 }
715
716 #[test]
717 fn make_endpoint_url_with_query() {
718 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/")]);
719 let url = history
720 .make_endpoint_url(
721 Cow::Owned(version_only_supported(&[V1_0])),
722 "https://example.org",
723 &[],
724 "foo=bar",
725 )
726 .unwrap();
727 assert_eq!(url, "https://example.org/s/?foo=bar");
728 }
729
730 #[test]
731 #[should_panic]
732 fn make_endpoint_url_wrong_num_path_args() {
733 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
734 _ = history.make_endpoint_url(
735 Cow::Owned(version_only_supported(&[V1_0])),
736 "https://example.org",
737 &[],
738 "",
739 );
740 }
741
742 const EMPTY: VersionHistory =
743 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
744
745 #[test]
746 fn select_version() {
747 let version_supported = version_only_supported(&[V1_0, V1_1]);
748 let superset_supported = version_only_supported(&[V1_1]);
749
750 let hist =
752 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
753 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
754 assert!(hist.is_supported(&version_supported));
755 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
756 assert!(hist.is_supported(&superset_supported));
757
758 let hist = VersionHistory {
760 stable_paths: &[(
761 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
762 "/s",
763 )],
764 ..EMPTY
765 };
766 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
767 assert!(hist.is_supported(&version_supported));
768 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
769 assert!(hist.is_supported(&superset_supported));
770
771 let hist = VersionHistory {
773 stable_paths: &[
774 (StablePathSelector::Version(V1_0), "/s_v1"),
775 (StablePathSelector::Version(V1_1), "/s_v2"),
776 ],
777 ..EMPTY
778 };
779 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s_v2"));
780 assert!(hist.is_supported(&version_supported));
781
782 let unstable_supported = SupportedVersions {
784 versions: [V1_0].into(),
785 features: ["org.boo.unstable".into()].into(),
786 };
787 let hist = VersionHistory {
788 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
789 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
790 ..EMPTY
791 };
792 assert_matches!(hist.select_path(Cow::Borrowed(&unstable_supported)), Ok("/s"));
793 assert!(hist.is_supported(&unstable_supported));
794 }
795
796 #[test]
797 fn select_stable_feature() {
798 let supported = SupportedVersions {
799 versions: [V1_1].into(),
800 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
801 };
802
803 let hist = VersionHistory {
805 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
806 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
807 ..EMPTY
808 };
809 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
810 assert!(hist.is_supported(&supported));
811
812 let hist = VersionHistory {
814 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
815 stable_paths: &[(
816 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
817 "/s",
818 )],
819 ..EMPTY
820 };
821 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
822 assert!(hist.is_supported(&supported));
823 }
824
825 #[test]
826 fn select_unstable_feature() {
827 let supported = SupportedVersions {
828 versions: [V1_1].into(),
829 features: ["org.boo.unstable".into()].into(),
830 };
831
832 let hist = VersionHistory {
833 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
834 stable_paths: &[(
835 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
836 "/s",
837 )],
838 ..EMPTY
839 };
840 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
841 assert!(hist.is_supported(&supported));
842 }
843
844 #[test]
845 fn select_unstable_fallback() {
846 let supported = version_only_supported(&[V1_0]);
847 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
848 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
849 assert!(!hist.is_supported(&supported));
850 }
851
852 #[test]
853 fn select_r0() {
854 let supported = version_only_supported(&[V1_0]);
855 let hist =
856 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
857 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/r"));
858 assert!(hist.is_supported(&supported));
859 }
860
861 #[test]
862 fn select_removed_err() {
863 let supported = version_only_supported(&[V1_3]);
864 let hist = VersionHistory {
865 stable_paths: &[
866 (StablePathSelector::Version(V1_0), "/r"),
867 (StablePathSelector::Version(V1_1), "/s"),
868 ],
869 unstable_paths: &[(None, "/u")],
870 deprecated: Some(V1_2),
871 removed: Some(V1_3),
872 };
873 assert_matches!(
874 hist.select_path(Cow::Borrowed(&supported)),
875 Err(IntoHttpError::EndpointRemoved(V1_3))
876 );
877 assert!(!hist.is_supported(&supported));
878 }
879
880 #[test]
881 fn partially_removed_but_stable() {
882 let supported = version_only_supported(&[V1_2]);
883 let hist = VersionHistory {
884 stable_paths: &[
885 (StablePathSelector::Version(V1_0), "/r"),
886 (StablePathSelector::Version(V1_1), "/s"),
887 ],
888 unstable_paths: &[],
889 deprecated: Some(V1_2),
890 removed: Some(V1_3),
891 };
892 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
893 assert!(hist.is_supported(&supported));
894 }
895
896 #[test]
897 fn no_unstable() {
898 let supported = version_only_supported(&[V1_0]);
899 let hist =
900 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
901 assert_matches!(
902 hist.select_path(Cow::Borrowed(&supported)),
903 Err(IntoHttpError::NoUnstablePath)
904 );
905 assert!(!hist.is_supported(&supported));
906 }
907
908 #[test]
909 fn version_literal() {
910 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
911
912 assert_eq!(LIT, V1_0);
913 }
914
915 #[test]
916 fn parse_as_str_sanity() {
917 let version = MatrixVersion::try_from("r0.5.0").unwrap();
918 assert_eq!(version, V1_0);
919 assert_eq!(version.as_str(), None);
920
921 let version = MatrixVersion::try_from("v1.1").unwrap();
922 assert_eq!(version, V1_1);
923 assert_eq!(version.as_str(), Some("v1.1"));
924 }
925
926 #[test]
927 fn supported_versions_from_parts() {
928 let empty_features = BTreeMap::new();
929
930 let none = &[];
931 let none_supported = SupportedVersions::from_parts(none, &empty_features);
932 assert_eq!(none_supported.versions, BTreeSet::new());
933 assert_eq!(none_supported.features, BTreeSet::new());
934
935 let single_known = &["r0.6.0".to_owned()];
936 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
937 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
938 assert_eq!(single_known_supported.features, BTreeSet::new());
939
940 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
941 let multiple_known_supported =
942 SupportedVersions::from_parts(multiple_known, &empty_features);
943 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
944 assert_eq!(multiple_known_supported.features, BTreeSet::new());
945
946 let single_unknown = &["v0.0".to_owned()];
947 let single_unknown_supported =
948 SupportedVersions::from_parts(single_unknown, &empty_features);
949 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
950 assert_eq!(single_unknown_supported.features, BTreeSet::new());
951
952 let mut features = BTreeMap::new();
953 features.insert("org.bar.enabled_1".to_owned(), true);
954 features.insert("org.bar.disabled".to_owned(), false);
955 features.insert("org.bar.enabled_2".to_owned(), true);
956
957 let features_supported = SupportedVersions::from_parts(single_known, &features);
958 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
959 assert_eq!(
960 features_supported.features,
961 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
962 );
963 }
964
965 #[test]
966 fn supported_versions_from_parts_order() {
967 let empty_features = BTreeMap::new();
968
969 let sorted = &[
970 "r0.0.1".to_owned(),
971 "r0.5.0".to_owned(),
972 "r0.6.0".to_owned(),
973 "r0.6.1".to_owned(),
974 "v1.1".to_owned(),
975 "v1.2".to_owned(),
976 ];
977 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
978 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
979
980 let sorted_reverse = &[
981 "v1.2".to_owned(),
982 "v1.1".to_owned(),
983 "r0.6.1".to_owned(),
984 "r0.6.0".to_owned(),
985 "r0.5.0".to_owned(),
986 "r0.0.1".to_owned(),
987 ];
988 let sorted_reverse_supported =
989 SupportedVersions::from_parts(sorted_reverse, &empty_features);
990 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
991
992 let random_order = &[
993 "v1.1".to_owned(),
994 "r0.6.1".to_owned(),
995 "r0.5.0".to_owned(),
996 "r0.6.0".to_owned(),
997 "r0.0.1".to_owned(),
998 "v1.2".to_owned(),
999 ];
1000 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1001 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1002 }
1003
1004 #[test]
1005 #[should_panic]
1006 fn make_endpoint_url_with_path_args_old_syntax() {
1007 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1008 let url = history
1009 .make_endpoint_url(
1010 Cow::Owned(version_only_supported(&[V1_0])),
1011 "https://example.org",
1012 &[&"123"],
1013 "",
1014 )
1015 .unwrap();
1016 assert_eq!(url, "https://example.org/s/123");
1017 }
1018}