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_matches::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_path_args_with_backslash() {
718 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
719 let url = history
720 .make_endpoint_url(
721 Cow::Owned(version_only_supported(&[V1_0])),
722 "https://example.org",
723 &[&r"C:\Path\To"],
724 "",
725 )
726 .unwrap();
727 assert_eq!(url, "https://example.org/s/C:%5CPath%5CTo");
728 }
729
730 #[test]
731 fn make_endpoint_url_with_query() {
732 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/")]);
733 let url = history
734 .make_endpoint_url(
735 Cow::Owned(version_only_supported(&[V1_0])),
736 "https://example.org",
737 &[],
738 "foo=bar",
739 )
740 .unwrap();
741 assert_eq!(url, "https://example.org/s/?foo=bar");
742 }
743
744 #[test]
745 #[should_panic]
746 fn make_endpoint_url_wrong_num_path_args() {
747 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
748 _ = history.make_endpoint_url(
749 Cow::Owned(version_only_supported(&[V1_0])),
750 "https://example.org",
751 &[],
752 "",
753 );
754 }
755
756 const EMPTY: VersionHistory =
757 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
758
759 #[test]
760 fn select_version() {
761 let version_supported = version_only_supported(&[V1_0, V1_1]);
762 let superset_supported = version_only_supported(&[V1_1]);
763
764 let hist =
766 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
767 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
768 assert!(hist.is_supported(&version_supported));
769 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
770 assert!(hist.is_supported(&superset_supported));
771
772 let hist = VersionHistory {
774 stable_paths: &[(
775 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
776 "/s",
777 )],
778 ..EMPTY
779 };
780 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
781 assert!(hist.is_supported(&version_supported));
782 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
783 assert!(hist.is_supported(&superset_supported));
784
785 let hist = VersionHistory {
787 stable_paths: &[
788 (StablePathSelector::Version(V1_0), "/s_v1"),
789 (StablePathSelector::Version(V1_1), "/s_v2"),
790 ],
791 ..EMPTY
792 };
793 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s_v2"));
794 assert!(hist.is_supported(&version_supported));
795
796 let unstable_supported = SupportedVersions {
798 versions: [V1_0].into(),
799 features: ["org.boo.unstable".into()].into(),
800 };
801 let hist = VersionHistory {
802 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
803 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
804 ..EMPTY
805 };
806 assert_matches!(hist.select_path(Cow::Borrowed(&unstable_supported)), Ok("/s"));
807 assert!(hist.is_supported(&unstable_supported));
808 }
809
810 #[test]
811 fn select_stable_feature() {
812 let supported = SupportedVersions {
813 versions: [V1_1].into(),
814 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
815 };
816
817 let hist = VersionHistory {
819 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
820 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
821 ..EMPTY
822 };
823 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
824 assert!(hist.is_supported(&supported));
825
826 let hist = VersionHistory {
828 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
829 stable_paths: &[(
830 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
831 "/s",
832 )],
833 ..EMPTY
834 };
835 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
836 assert!(hist.is_supported(&supported));
837 }
838
839 #[test]
840 fn select_unstable_feature() {
841 let supported = SupportedVersions {
842 versions: [V1_1].into(),
843 features: ["org.boo.unstable".into()].into(),
844 };
845
846 let hist = VersionHistory {
847 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
848 stable_paths: &[(
849 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
850 "/s",
851 )],
852 ..EMPTY
853 };
854 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
855 assert!(hist.is_supported(&supported));
856 }
857
858 #[test]
859 fn select_unstable_fallback() {
860 let supported = version_only_supported(&[V1_0]);
861 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
862 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
863 assert!(!hist.is_supported(&supported));
864 }
865
866 #[test]
867 fn select_r0() {
868 let supported = version_only_supported(&[V1_0]);
869 let hist =
870 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
871 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/r"));
872 assert!(hist.is_supported(&supported));
873 }
874
875 #[test]
876 fn select_removed_err() {
877 let supported = version_only_supported(&[V1_3]);
878 let hist = VersionHistory {
879 stable_paths: &[
880 (StablePathSelector::Version(V1_0), "/r"),
881 (StablePathSelector::Version(V1_1), "/s"),
882 ],
883 unstable_paths: &[(None, "/u")],
884 deprecated: Some(V1_2),
885 removed: Some(V1_3),
886 };
887 assert_matches!(
888 hist.select_path(Cow::Borrowed(&supported)),
889 Err(IntoHttpError::EndpointRemoved(V1_3))
890 );
891 assert!(!hist.is_supported(&supported));
892 }
893
894 #[test]
895 fn partially_removed_but_stable() {
896 let supported = version_only_supported(&[V1_2]);
897 let hist = VersionHistory {
898 stable_paths: &[
899 (StablePathSelector::Version(V1_0), "/r"),
900 (StablePathSelector::Version(V1_1), "/s"),
901 ],
902 unstable_paths: &[],
903 deprecated: Some(V1_2),
904 removed: Some(V1_3),
905 };
906 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
907 assert!(hist.is_supported(&supported));
908 }
909
910 #[test]
911 fn no_unstable() {
912 let supported = version_only_supported(&[V1_0]);
913 let hist =
914 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
915 assert_matches!(
916 hist.select_path(Cow::Borrowed(&supported)),
917 Err(IntoHttpError::NoUnstablePath)
918 );
919 assert!(!hist.is_supported(&supported));
920 }
921
922 #[test]
923 fn version_literal() {
924 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
925
926 assert_eq!(LIT, V1_0);
927 }
928
929 #[test]
930 fn parse_as_str_sanity() {
931 let version = MatrixVersion::try_from("r0.5.0").unwrap();
932 assert_eq!(version, V1_0);
933 assert_eq!(version.as_str(), None);
934
935 let version = MatrixVersion::try_from("v1.1").unwrap();
936 assert_eq!(version, V1_1);
937 assert_eq!(version.as_str(), Some("v1.1"));
938 }
939
940 #[test]
941 fn supported_versions_from_parts() {
942 let empty_features = BTreeMap::new();
943
944 let none = &[];
945 let none_supported = SupportedVersions::from_parts(none, &empty_features);
946 assert_eq!(none_supported.versions, BTreeSet::new());
947 assert_eq!(none_supported.features, BTreeSet::new());
948
949 let single_known = &["r0.6.0".to_owned()];
950 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
951 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
952 assert_eq!(single_known_supported.features, BTreeSet::new());
953
954 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
955 let multiple_known_supported =
956 SupportedVersions::from_parts(multiple_known, &empty_features);
957 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
958 assert_eq!(multiple_known_supported.features, BTreeSet::new());
959
960 let single_unknown = &["v0.0".to_owned()];
961 let single_unknown_supported =
962 SupportedVersions::from_parts(single_unknown, &empty_features);
963 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
964 assert_eq!(single_unknown_supported.features, BTreeSet::new());
965
966 let mut features = BTreeMap::new();
967 features.insert("org.bar.enabled_1".to_owned(), true);
968 features.insert("org.bar.disabled".to_owned(), false);
969 features.insert("org.bar.enabled_2".to_owned(), true);
970
971 let features_supported = SupportedVersions::from_parts(single_known, &features);
972 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
973 assert_eq!(
974 features_supported.features,
975 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
976 );
977 }
978
979 #[test]
980 fn supported_versions_from_parts_order() {
981 let empty_features = BTreeMap::new();
982
983 let sorted = &[
984 "r0.0.1".to_owned(),
985 "r0.5.0".to_owned(),
986 "r0.6.0".to_owned(),
987 "r0.6.1".to_owned(),
988 "v1.1".to_owned(),
989 "v1.2".to_owned(),
990 ];
991 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
992 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
993
994 let sorted_reverse = &[
995 "v1.2".to_owned(),
996 "v1.1".to_owned(),
997 "r0.6.1".to_owned(),
998 "r0.6.0".to_owned(),
999 "r0.5.0".to_owned(),
1000 "r0.0.1".to_owned(),
1001 ];
1002 let sorted_reverse_supported =
1003 SupportedVersions::from_parts(sorted_reverse, &empty_features);
1004 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1005
1006 let random_order = &[
1007 "v1.1".to_owned(),
1008 "r0.6.1".to_owned(),
1009 "r0.5.0".to_owned(),
1010 "r0.6.0".to_owned(),
1011 "r0.0.1".to_owned(),
1012 "v1.2".to_owned(),
1013 ];
1014 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1015 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1016 }
1017
1018 #[test]
1019 #[should_panic]
1020 fn make_endpoint_url_with_path_args_old_syntax() {
1021 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1022 let url = history
1023 .make_endpoint_url(
1024 Cow::Owned(version_only_supported(&[V1_0])),
1025 "https://example.org",
1026 &[&"123"],
1027 "",
1028 )
1029 .unwrap();
1030 assert_eq!(url, "https://example.org/s/123");
1031 }
1032}