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, "/").next();
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_n_iter)) = second_iter else {
168 break None;
169 };
170
171 let maybe_second_arg = extract_endpoint_path_segment_variable(second_s);
172
173 second_iter = second_n_iter.next();
174
175 if let Some(second_arg) = maybe_second_arg {
176 break Some(second_arg);
177 }
178 };
179
180 if let Some(second_next_arg) = second_next_arg {
181 if !string::eq_str(second_next_arg, first_arg) {
182 panic!("names of endpoint path segment variables do not match");
183 }
184 } else {
185 panic!("counts of endpoint path segment variables do not match");
186 }
187 }
188 });
189
190 while let Some((second_s, second_n_iter)) = second_iter {
192 if extract_endpoint_path_segment_variable(second_s).is_some() {
193 panic!("counts of endpoint path segment variables do not match");
194 }
195 second_iter = second_n_iter.next();
196 }
197 }
198
199 let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
201 s
202 } else if let Some((_, s)) = stable_paths.first() {
203 s
204 } else {
205 panic!("no endpoint paths supplied")
206 };
207
208 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
209 check_path_is_valid(unstable_path.1);
210 check_path_args_equal(ref_path, unstable_path.1);
211 });
212
213 let mut prev_seen_version: Option<MatrixVersion> = None;
214
215 iter::for_each!(version_path in slice::iter(stable_paths) => {
216 check_path_is_valid(version_path.1);
217 check_path_args_equal(ref_path, version_path.1);
218
219 if let Some(current_version) = version_path.0.version() {
220 if let Some(prev_seen_version) = prev_seen_version {
221 let cmp_result = current_version.const_ord(&prev_seen_version);
222
223 if cmp_result.is_eq() {
224 panic!("duplicate matrix version in stable paths")
226 } else if cmp_result.is_lt() {
227 panic!("stable paths are not in ascending order")
229 }
230 }
231
232 prev_seen_version = Some(current_version);
233 }
234 });
235
236 if let Some(deprecated) = deprecated {
237 if let Some(prev_seen_version) = prev_seen_version {
238 let ord_result = prev_seen_version.const_ord(&deprecated);
239 if !deprecated.is_legacy() && ord_result.is_eq() {
240 panic!("deprecated version is equal to latest stable path version")
244 } else if ord_result.is_gt() {
245 panic!("deprecated version is older than latest stable path version")
247 }
248 } else {
249 panic!("defined deprecated version while no stable path exists")
250 }
251 }
252
253 if let Some(removed) = removed {
254 if let Some(deprecated) = deprecated {
255 let ord_result = deprecated.const_ord(&removed);
256 if ord_result.is_eq() {
257 panic!("removed version is equal to deprecated version")
259 } else if ord_result.is_gt() {
260 panic!("removed version is older than deprecated version")
262 }
263 } else {
264 panic!("defined removed version while no deprecated version exists")
265 }
266 }
267
268 Self { unstable_paths, stable_paths, deprecated, removed }
269 }
270
271 pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
280 match self.versioning_decision_for(&considering.versions) {
281 VersioningDecision::Removed => false,
282 VersioningDecision::Version { .. } => true,
283 VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
284 }
285 }
286
287 pub fn versioning_decision_for(
299 &self,
300 versions: &BTreeSet<MatrixVersion>,
301 ) -> VersioningDecision {
302 let is_superset_any =
303 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
304 let is_superset_all =
305 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
306
307 if self.removed.is_some_and(is_superset_all) {
309 return VersioningDecision::Removed;
310 }
311
312 if self.added_in().is_some_and(is_superset_any) {
314 let all_deprecated = self.deprecated.is_some_and(is_superset_all);
315
316 return VersioningDecision::Version {
317 any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
318 all_deprecated,
319 any_removed: self.removed.is_some_and(is_superset_any),
320 };
321 }
322
323 VersioningDecision::Feature
324 }
325
326 pub fn added_in(&self) -> Option<MatrixVersion> {
330 self.stable_paths.iter().find_map(|(v, _)| v.version())
331 }
332
333 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
335 self.deprecated
336 }
337
338 pub fn removed_in(&self) -> Option<MatrixVersion> {
340 self.removed
341 }
342
343 pub fn unstable(&self) -> Option<&'static str> {
345 self.unstable_paths.last().map(|(_, path)| *path)
346 }
347
348 pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
350 self.unstable_paths.iter().copied()
351 }
352
353 pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
355 self.stable_paths.iter().copied()
356 }
357
358 pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
370 let version_paths = self
371 .stable_paths
372 .iter()
373 .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
374
375 for (ver, path) in version_paths.rev() {
377 if versions.iter().any(|v| v.is_superset_of(ver)) {
379 return Some(path);
380 }
381 }
382
383 None
384 }
385
386 pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
388 let unstable_feature_paths = self
389 .unstable_paths
390 .iter()
391 .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
392 let stable_feature_paths = self
393 .stable_paths
394 .iter()
395 .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
396
397 for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
399 if supported_features.iter().any(|supported| supported.as_str() == feature) {
401 return Some(path);
402 }
403 }
404
405 None
406 }
407}
408
409impl PathBuilder for VersionHistory {
410 type Input<'a> = Cow<'a, SupportedVersions>;
411
412 fn select_path(
423 &self,
424 input: Cow<'_, SupportedVersions>,
425 ) -> Result<&'static str, IntoHttpError> {
426 match self.versioning_decision_for(&input.versions) {
427 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
428 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
429 )),
430 VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
431 if any_removed {
432 if all_deprecated {
433 warn!(
434 "endpoint is removed in some (and deprecated in ALL) \
435 of the following versions: {:?}",
436 input.versions
437 );
438 } else if any_deprecated {
439 warn!(
440 "endpoint is removed (and deprecated) in some of the \
441 following versions: {:?}",
442 input.versions
443 );
444 } else {
445 unreachable!("any_removed implies *_deprecated");
446 }
447 } else if all_deprecated {
448 warn!(
449 "endpoint is deprecated in ALL of the following versions: {:?}",
450 input.versions
451 );
452 } else if any_deprecated {
453 warn!(
454 "endpoint is deprecated in some of the following versions: {:?}",
455 input.versions
456 );
457 }
458
459 Ok(self
460 .version_path(&input.versions)
461 .expect("VersioningDecision::Version implies that a version path exists"))
462 }
463 VersioningDecision::Feature => self
464 .feature_path(&input.features)
465 .or_else(|| self.unstable())
466 .ok_or(IntoHttpError::NoUnstablePath),
467 }
468 }
469
470 fn all_paths(&self) -> impl Iterator<Item = &'static str> {
471 self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
472 }
473
474 fn _path_parameters(&self) -> Vec<&'static str> {
475 let path = self.all_paths().next().unwrap();
476 path.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
477 }
478}
479
480#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
482#[allow(clippy::exhaustive_enums)]
483pub enum VersioningDecision {
484 Feature,
486
487 Version {
489 any_deprecated: bool,
491
492 all_deprecated: bool,
494
495 any_removed: bool,
497 },
498
499 Removed,
501}
502
503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505#[allow(clippy::exhaustive_enums)]
506pub enum StablePathSelector {
507 Feature(&'static str),
509
510 Version(MatrixVersion),
512
513 FeatureAndVersion {
515 feature: &'static str,
517 version: MatrixVersion,
519 },
520}
521
522impl StablePathSelector {
523 pub const fn feature(self) -> Option<&'static str> {
525 match self {
526 Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
527 _ => None,
528 }
529 }
530
531 pub const fn version(self) -> Option<MatrixVersion> {
533 match self {
534 Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
535 _ => None,
536 }
537 }
538}
539
540impl From<MatrixVersion> for StablePathSelector {
541 fn from(value: MatrixVersion) -> Self {
542 Self::Version(value)
543 }
544}
545
546#[derive(Clone, Debug, PartialEq, Eq)]
551#[allow(clippy::exhaustive_structs)]
552pub struct SinglePath(&'static str);
553
554impl SinglePath {
555 pub const fn new(path: &'static str) -> Self {
557 check_path_is_valid(path);
558
559 iter::for_each!(segment in string::split(path, '/') => {
561 extract_endpoint_path_segment_variable(segment);
562 });
563
564 Self(path)
565 }
566
567 pub fn path(&self) -> &'static str {
569 self.0
570 }
571}
572
573impl PathBuilder for SinglePath {
574 type Input<'a> = ();
575
576 fn select_path(&self, _input: ()) -> Result<&'static str, IntoHttpError> {
577 Ok(self.0)
578 }
579
580 fn all_paths(&self) -> impl Iterator<Item = &'static str> {
581 std::iter::once(self.0)
582 }
583
584 fn _path_parameters(&self) -> Vec<&'static str> {
585 self.0.split('/').filter_map(extract_endpoint_path_segment_variable).collect()
586 }
587}
588
589const fn check_path_is_valid(path: &'static str) {
593 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
594 match *path_b {
595 0x21..=0x7E => {},
596 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
597 }
598 });
599}
600
601pub const fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
614 if string::starts_with(segment, ':') {
615 panic!("endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`");
616 }
617
618 if let Some(s) = string::strip_prefix(segment, '{') {
619 let var = string::strip_suffix(s, '}')
620 .expect("endpoint path segment variable braces mismatch: missing ending `}`");
621 return Some(var);
622 }
623
624 if string::ends_with(segment, '}') {
625 panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
626 }
627
628 None
629}
630
631#[cfg(test)]
632mod tests {
633 use std::{
634 borrow::Cow,
635 collections::{BTreeMap, BTreeSet},
636 };
637
638 use assert_matches2::assert_matches;
639
640 use super::{PathBuilder, StablePathSelector, VersionHistory};
641 use crate::api::{
642 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
643 SupportedVersions,
644 error::IntoHttpError,
645 };
646
647 fn stable_only_history(
648 stable_paths: &'static [(StablePathSelector, &'static str)],
649 ) -> VersionHistory {
650 VersionHistory { unstable_paths: &[], stable_paths, deprecated: None, removed: None }
651 }
652
653 fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
654 SupportedVersions {
655 versions: versions.iter().copied().collect(),
656 features: BTreeSet::new(),
657 }
658 }
659
660 #[test]
663 fn make_simple_endpoint_url() {
664 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s")]);
665
666 let url = history
667 .make_endpoint_url(
668 Cow::Owned(version_only_supported(&[V1_0])),
669 "https://example.org",
670 &[],
671 "",
672 )
673 .unwrap();
674 assert_eq!(url, "https://example.org/s");
675 }
676
677 #[test]
678 fn make_endpoint_url_with_path_args() {
679 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
680 let url = history
681 .make_endpoint_url(
682 Cow::Owned(version_only_supported(&[V1_0])),
683 "https://example.org",
684 &[&"123"],
685 "",
686 )
687 .unwrap();
688 assert_eq!(url, "https://example.org/s/123");
689 }
690
691 #[test]
692 fn make_endpoint_url_with_path_args_with_dash() {
693 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
694 let url = history
695 .make_endpoint_url(
696 Cow::Owned(version_only_supported(&[V1_0])),
697 "https://example.org",
698 &[&"my-path"],
699 "",
700 )
701 .unwrap();
702 assert_eq!(url, "https://example.org/s/my-path");
703 }
704
705 #[test]
706 fn make_endpoint_url_with_path_args_with_reserved_char() {
707 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
708 let url = history
709 .make_endpoint_url(
710 Cow::Owned(version_only_supported(&[V1_0])),
711 "https://example.org",
712 &[&"#path"],
713 "",
714 )
715 .unwrap();
716 assert_eq!(url, "https://example.org/s/%23path");
717 }
718
719 #[test]
720 fn make_endpoint_url_with_query() {
721 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/")]);
722 let url = history
723 .make_endpoint_url(
724 Cow::Owned(version_only_supported(&[V1_0])),
725 "https://example.org",
726 &[],
727 "foo=bar",
728 )
729 .unwrap();
730 assert_eq!(url, "https://example.org/s/?foo=bar");
731 }
732
733 #[test]
734 #[should_panic]
735 fn make_endpoint_url_wrong_num_path_args() {
736 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
737 _ = history.make_endpoint_url(
738 Cow::Owned(version_only_supported(&[V1_0])),
739 "https://example.org",
740 &[],
741 "",
742 );
743 }
744
745 const EMPTY: VersionHistory =
746 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
747
748 #[test]
749 fn select_version() {
750 let version_supported = version_only_supported(&[V1_0, V1_1]);
751 let superset_supported = version_only_supported(&[V1_1]);
752
753 let hist =
755 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
756 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
757 assert!(hist.is_supported(&version_supported));
758 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
759 assert!(hist.is_supported(&superset_supported));
760
761 let hist = VersionHistory {
763 stable_paths: &[(
764 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
765 "/s",
766 )],
767 ..EMPTY
768 };
769 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s"));
770 assert!(hist.is_supported(&version_supported));
771 assert_matches!(hist.select_path(Cow::Borrowed(&superset_supported)), Ok("/s"));
772 assert!(hist.is_supported(&superset_supported));
773
774 let hist = VersionHistory {
776 stable_paths: &[
777 (StablePathSelector::Version(V1_0), "/s_v1"),
778 (StablePathSelector::Version(V1_1), "/s_v2"),
779 ],
780 ..EMPTY
781 };
782 assert_matches!(hist.select_path(Cow::Borrowed(&version_supported)), Ok("/s_v2"));
783 assert!(hist.is_supported(&version_supported));
784
785 let unstable_supported = SupportedVersions {
787 versions: [V1_0].into(),
788 features: ["org.boo.unstable".into()].into(),
789 };
790 let hist = VersionHistory {
791 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
792 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
793 ..EMPTY
794 };
795 assert_matches!(hist.select_path(Cow::Borrowed(&unstable_supported)), Ok("/s"));
796 assert!(hist.is_supported(&unstable_supported));
797 }
798
799 #[test]
800 fn select_stable_feature() {
801 let supported = SupportedVersions {
802 versions: [V1_1].into(),
803 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
804 };
805
806 let hist = VersionHistory {
808 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
809 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
810 ..EMPTY
811 };
812 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
813 assert!(hist.is_supported(&supported));
814
815 let hist = VersionHistory {
817 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
818 stable_paths: &[(
819 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
820 "/s",
821 )],
822 ..EMPTY
823 };
824 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
825 assert!(hist.is_supported(&supported));
826 }
827
828 #[test]
829 fn select_unstable_feature() {
830 let supported = SupportedVersions {
831 versions: [V1_1].into(),
832 features: ["org.boo.unstable".into()].into(),
833 };
834
835 let hist = VersionHistory {
836 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
837 stable_paths: &[(
838 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
839 "/s",
840 )],
841 ..EMPTY
842 };
843 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
844 assert!(hist.is_supported(&supported));
845 }
846
847 #[test]
848 fn select_unstable_fallback() {
849 let supported = version_only_supported(&[V1_0]);
850 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
851 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/u"));
852 assert!(!hist.is_supported(&supported));
853 }
854
855 #[test]
856 fn select_r0() {
857 let supported = version_only_supported(&[V1_0]);
858 let hist =
859 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
860 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/r"));
861 assert!(hist.is_supported(&supported));
862 }
863
864 #[test]
865 fn select_removed_err() {
866 let supported = version_only_supported(&[V1_3]);
867 let hist = VersionHistory {
868 stable_paths: &[
869 (StablePathSelector::Version(V1_0), "/r"),
870 (StablePathSelector::Version(V1_1), "/s"),
871 ],
872 unstable_paths: &[(None, "/u")],
873 deprecated: Some(V1_2),
874 removed: Some(V1_3),
875 };
876 assert_matches!(
877 hist.select_path(Cow::Borrowed(&supported)),
878 Err(IntoHttpError::EndpointRemoved(V1_3))
879 );
880 assert!(!hist.is_supported(&supported));
881 }
882
883 #[test]
884 fn partially_removed_but_stable() {
885 let supported = version_only_supported(&[V1_2]);
886 let hist = VersionHistory {
887 stable_paths: &[
888 (StablePathSelector::Version(V1_0), "/r"),
889 (StablePathSelector::Version(V1_1), "/s"),
890 ],
891 unstable_paths: &[],
892 deprecated: Some(V1_2),
893 removed: Some(V1_3),
894 };
895 assert_matches!(hist.select_path(Cow::Borrowed(&supported)), Ok("/s"));
896 assert!(hist.is_supported(&supported));
897 }
898
899 #[test]
900 fn no_unstable() {
901 let supported = version_only_supported(&[V1_0]);
902 let hist =
903 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
904 assert_matches!(
905 hist.select_path(Cow::Borrowed(&supported)),
906 Err(IntoHttpError::NoUnstablePath)
907 );
908 assert!(!hist.is_supported(&supported));
909 }
910
911 #[test]
912 fn version_literal() {
913 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
914
915 assert_eq!(LIT, V1_0);
916 }
917
918 #[test]
919 fn parse_as_str_sanity() {
920 let version = MatrixVersion::try_from("r0.5.0").unwrap();
921 assert_eq!(version, V1_0);
922 assert_eq!(version.as_str(), None);
923
924 let version = MatrixVersion::try_from("v1.1").unwrap();
925 assert_eq!(version, V1_1);
926 assert_eq!(version.as_str(), Some("v1.1"));
927 }
928
929 #[test]
930 fn supported_versions_from_parts() {
931 let empty_features = BTreeMap::new();
932
933 let none = &[];
934 let none_supported = SupportedVersions::from_parts(none, &empty_features);
935 assert_eq!(none_supported.versions, BTreeSet::new());
936 assert_eq!(none_supported.features, BTreeSet::new());
937
938 let single_known = &["r0.6.0".to_owned()];
939 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
940 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
941 assert_eq!(single_known_supported.features, BTreeSet::new());
942
943 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
944 let multiple_known_supported =
945 SupportedVersions::from_parts(multiple_known, &empty_features);
946 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
947 assert_eq!(multiple_known_supported.features, BTreeSet::new());
948
949 let single_unknown = &["v0.0".to_owned()];
950 let single_unknown_supported =
951 SupportedVersions::from_parts(single_unknown, &empty_features);
952 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
953 assert_eq!(single_unknown_supported.features, BTreeSet::new());
954
955 let mut features = BTreeMap::new();
956 features.insert("org.bar.enabled_1".to_owned(), true);
957 features.insert("org.bar.disabled".to_owned(), false);
958 features.insert("org.bar.enabled_2".to_owned(), true);
959
960 let features_supported = SupportedVersions::from_parts(single_known, &features);
961 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
962 assert_eq!(
963 features_supported.features,
964 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
965 );
966 }
967
968 #[test]
969 fn supported_versions_from_parts_order() {
970 let empty_features = BTreeMap::new();
971
972 let sorted = &[
973 "r0.0.1".to_owned(),
974 "r0.5.0".to_owned(),
975 "r0.6.0".to_owned(),
976 "r0.6.1".to_owned(),
977 "v1.1".to_owned(),
978 "v1.2".to_owned(),
979 ];
980 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
981 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
982
983 let sorted_reverse = &[
984 "v1.2".to_owned(),
985 "v1.1".to_owned(),
986 "r0.6.1".to_owned(),
987 "r0.6.0".to_owned(),
988 "r0.5.0".to_owned(),
989 "r0.0.1".to_owned(),
990 ];
991 let sorted_reverse_supported =
992 SupportedVersions::from_parts(sorted_reverse, &empty_features);
993 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
994
995 let random_order = &[
996 "v1.1".to_owned(),
997 "r0.6.1".to_owned(),
998 "r0.5.0".to_owned(),
999 "r0.6.0".to_owned(),
1000 "r0.0.1".to_owned(),
1001 "v1.2".to_owned(),
1002 ];
1003 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1004 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1005 }
1006
1007 #[test]
1008 #[should_panic]
1009 fn make_endpoint_url_with_path_args_old_syntax() {
1010 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1011 let url = history
1012 .make_endpoint_url(
1013 Cow::Owned(version_only_supported(&[V1_0])),
1014 "https://example.org",
1015 &[&"123"],
1016 "",
1017 )
1018 .unwrap();
1019 assert_eq!(url, "https://example.org/s/123");
1020 }
1021}