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