1use std::{
2 cmp::Ordering,
3 collections::{BTreeMap, BTreeSet},
4 fmt::{self, Display, Write},
5 str::FromStr,
6};
7
8use bytes::BufMut;
9use http::{
10 header::{self, HeaderName, HeaderValue},
11 Method,
12};
13use percent_encoding::utf8_percent_encode;
14use ruma_macros::{OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, StringEnum};
15use tracing::warn;
16
17use super::{
18 error::{IntoHttpError, UnknownVersionError},
19 AuthScheme, SendAccessToken,
20};
21use crate::{
22 percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, PrivOwnedStr, RoomVersionId,
23};
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27#[allow(clippy::exhaustive_structs)]
28pub struct Metadata {
29 pub method: Method,
31
32 pub rate_limited: bool,
34
35 pub authentication: AuthScheme,
37
38 pub history: VersionHistory,
40}
41
42impl Metadata {
43 pub fn empty_request_body<B>(&self) -> B
48 where
49 B: Default + BufMut,
50 {
51 if self.method == Method::GET {
52 Default::default()
53 } else {
54 slice_to_buf(b"{}")
55 }
56 }
57
58 pub fn authorization_header(
64 &self,
65 access_token: SendAccessToken<'_>,
66 ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
67 Ok(match self.authentication {
68 AuthScheme::None => match access_token.get_not_required_for_endpoint() {
69 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
70 None => None,
71 },
72
73 AuthScheme::AccessToken => {
74 let token = access_token
75 .get_required_for_endpoint()
76 .ok_or(IntoHttpError::NeedsAuthentication)?;
77
78 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
79 }
80
81 AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
82 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
83 None => None,
84 },
85
86 AuthScheme::AppserviceToken => match access_token.get_required_for_appservice() {
87 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
88 None => None,
89 },
90
91 AuthScheme::ServerSignatures => None,
92 })
93 }
94
95 pub fn make_endpoint_url(
97 &self,
98 versions: &[MatrixVersion],
99 base_url: &str,
100 path_args: &[&dyn Display],
101 query_string: &str,
102 ) -> Result<String, IntoHttpError> {
103 let path_with_placeholders = self.history.select_path(versions)?;
104
105 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
106 let mut segments = path_with_placeholders.split('/');
107 let mut path_args = path_args.iter();
108
109 let first_segment = segments.next().expect("split iterator is never empty");
110 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
111
112 for segment in segments {
113 if segment.starts_with(':') {
114 let arg = path_args
115 .next()
116 .expect("number of placeholders must match number of arguments")
117 .to_string();
118 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
119
120 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
121 } else {
122 res.reserve(segment.len() + 1);
123 res.push('/');
124 res.push_str(segment);
125 }
126 }
127
128 if !query_string.is_empty() {
129 res.push('?');
130 res.push_str(query_string);
131 }
132
133 Ok(res)
134 }
135
136 #[doc(hidden)]
138 pub fn _path_parameters(&self) -> Vec<&'static str> {
139 let path = self.history.all_paths().next().unwrap();
140 path.split('/').filter_map(|segment| segment.strip_prefix(':')).collect()
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
149#[allow(clippy::exhaustive_structs)]
150pub struct VersionHistory {
151 unstable_paths: &'static [&'static str],
155
156 stable_paths: &'static [(MatrixVersion, &'static str)],
160
161 deprecated: Option<MatrixVersion>,
168
169 removed: Option<MatrixVersion>,
174}
175
176impl VersionHistory {
177 pub const fn new(
190 unstable_paths: &'static [&'static str],
191 stable_paths: &'static [(MatrixVersion, &'static str)],
192 deprecated: Option<MatrixVersion>,
193 removed: Option<MatrixVersion>,
194 ) -> Self {
195 use konst::{iter, slice, string};
196
197 const fn check_path_is_valid(path: &'static str) {
198 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
199 match *path_b {
200 0x21..=0x7E => {},
201 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
202 }
203 });
204 }
205
206 const fn check_path_args_equal(first: &'static str, second: &'static str) {
207 let mut second_iter = string::split(second, "/").next();
208
209 iter::for_each!(first_s in string::split(first, "/") => {
210 if let Some(first_arg) = string::strip_prefix(first_s, ":") {
211 let second_next_arg: Option<&'static str> = loop {
212 let (second_s, second_n_iter) = match second_iter {
213 Some(tuple) => tuple,
214 None => break None,
215 };
216
217 let maybe_second_arg = string::strip_prefix(second_s, ":");
218
219 second_iter = second_n_iter.next();
220
221 if let Some(second_arg) = maybe_second_arg {
222 break Some(second_arg);
223 }
224 };
225
226 if let Some(second_next_arg) = second_next_arg {
227 if !string::eq_str(second_next_arg, first_arg) {
228 panic!("Path Arguments do not match");
229 }
230 } else {
231 panic!("Amount of Path Arguments do not match");
232 }
233 }
234 });
235
236 while let Some((second_s, second_n_iter)) = second_iter {
238 if string::starts_with(second_s, ":") {
239 panic!("Amount of Path Arguments do not match");
240 }
241 second_iter = second_n_iter.next();
242 }
243 }
244
245 let ref_path: &str = if let Some(s) = unstable_paths.first() {
247 s
248 } else if let Some((_, s)) = stable_paths.first() {
249 s
250 } else {
251 panic!("No paths supplied")
252 };
253
254 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
255 check_path_is_valid(unstable_path);
256 check_path_args_equal(ref_path, unstable_path);
257 });
258
259 let mut prev_seen_version: Option<MatrixVersion> = None;
260
261 iter::for_each!(stable_path in slice::iter(stable_paths) => {
262 check_path_is_valid(stable_path.1);
263 check_path_args_equal(ref_path, stable_path.1);
264
265 let current_version = stable_path.0;
266
267 if let Some(prev_seen_version) = prev_seen_version {
268 let cmp_result = current_version.const_ord(&prev_seen_version);
269
270 if cmp_result.is_eq() {
271 panic!("Duplicate matrix version in stable_paths")
273 } else if cmp_result.is_lt() {
274 panic!("No ascending order in stable_paths")
276 }
277 }
278
279 prev_seen_version = Some(current_version);
280 });
281
282 if let Some(deprecated) = deprecated {
283 if let Some(prev_seen_version) = prev_seen_version {
284 let ord_result = prev_seen_version.const_ord(&deprecated);
285 if !deprecated.is_legacy() && ord_result.is_eq() {
286 panic!("deprecated version is equal to latest stable path version")
290 } else if ord_result.is_gt() {
291 panic!("deprecated version is older than latest stable path version")
293 }
294 } else {
295 panic!("Defined deprecated version while no stable path exists")
296 }
297 }
298
299 if let Some(removed) = removed {
300 if let Some(deprecated) = deprecated {
301 let ord_result = deprecated.const_ord(&removed);
302 if ord_result.is_eq() {
303 panic!("removed version is equal to deprecated version")
305 } else if ord_result.is_gt() {
306 panic!("removed version is older than deprecated version")
308 }
309 } else {
310 panic!("Defined removed version while no deprecated version exists")
311 }
312 }
313
314 VersionHistory { unstable_paths, stable_paths, deprecated, removed }
315 }
316
317 fn select_path(&self, versions: &[MatrixVersion]) -> Result<&'static str, IntoHttpError> {
319 match self.versioning_decision_for(versions) {
320 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
321 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
322 )),
323 VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => {
324 if any_removed {
325 if all_deprecated {
326 warn!(
327 "endpoint is removed in some (and deprecated in ALL) \
328 of the following versions: {versions:?}",
329 );
330 } else if any_deprecated {
331 warn!(
332 "endpoint is removed (and deprecated) in some of the \
333 following versions: {versions:?}",
334 );
335 } else {
336 unreachable!("any_removed implies *_deprecated");
337 }
338 } else if all_deprecated {
339 warn!(
340 "endpoint is deprecated in ALL of the following versions: \
341 {versions:?}",
342 );
343 } else if any_deprecated {
344 warn!(
345 "endpoint is deprecated in some of the following versions: \
346 {versions:?}",
347 );
348 }
349
350 Ok(self
351 .stable_endpoint_for(versions)
352 .expect("VersioningDecision::Stable implies that a stable path exists"))
353 }
354 VersioningDecision::Unstable => self.unstable().ok_or(IntoHttpError::NoUnstablePath),
355 }
356 }
357
358 pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision {
369 let greater_or_equal_any =
370 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
371 let greater_or_equal_all =
372 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
373
374 if self.removed.is_some_and(greater_or_equal_all) {
376 return VersioningDecision::Removed;
377 }
378
379 if self.added_in().is_some_and(greater_or_equal_any) {
381 let all_deprecated = self.deprecated.is_some_and(greater_or_equal_all);
382
383 return VersioningDecision::Stable {
384 any_deprecated: all_deprecated || self.deprecated.is_some_and(greater_or_equal_any),
385 all_deprecated,
386 any_removed: self.removed.is_some_and(greater_or_equal_any),
387 };
388 }
389
390 VersioningDecision::Unstable
391 }
392
393 pub fn added_in(&self) -> Option<MatrixVersion> {
397 self.stable_paths.first().map(|(v, _)| *v)
398 }
399
400 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
402 self.deprecated
403 }
404
405 pub fn removed_in(&self) -> Option<MatrixVersion> {
407 self.removed
408 }
409
410 pub fn unstable(&self) -> Option<&'static str> {
412 self.unstable_paths.last().copied()
413 }
414
415 pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
417 self.unstable_paths().chain(self.stable_paths().map(|(_, path)| path))
418 }
419
420 pub fn unstable_paths(&self) -> impl Iterator<Item = &'static str> {
422 self.unstable_paths.iter().copied()
423 }
424
425 pub fn stable_paths(&self) -> impl Iterator<Item = (MatrixVersion, &'static str)> {
427 self.stable_paths.iter().map(|(version, data)| (*version, *data))
428 }
429
430 pub fn stable_endpoint_for(&self, versions: &[MatrixVersion]) -> Option<&'static str> {
442 for (ver, path) in self.stable_paths.iter().rev() {
444 if versions.iter().any(|v| v.is_superset_of(*ver)) {
446 return Some(path);
447 }
448 }
449
450 None
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
456#[allow(clippy::exhaustive_enums)]
457pub enum VersioningDecision {
458 Unstable,
460
461 Stable {
463 any_deprecated: bool,
465
466 all_deprecated: bool,
468
469 any_removed: bool,
471 },
472
473 Removed,
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
498#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
499pub enum MatrixVersion {
500 V1_0,
512
513 V1_1,
517
518 V1_2,
522
523 V1_3,
527
528 V1_4,
532
533 V1_5,
537
538 V1_6,
542
543 V1_7,
547
548 V1_8,
552
553 V1_9,
557
558 V1_10,
562
563 V1_11,
567
568 V1_12,
572
573 V1_13,
577
578 V1_14,
582
583 V1_15,
587}
588
589impl TryFrom<&str> for MatrixVersion {
590 type Error = UnknownVersionError;
591
592 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
593 use MatrixVersion::*;
594
595 Ok(match value {
596 "r0.2.0" | "r0.2.1" | "r0.3.0" |
599 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
601 "v1.1" => V1_1,
602 "v1.2" => V1_2,
603 "v1.3" => V1_3,
604 "v1.4" => V1_4,
605 "v1.5" => V1_5,
606 "v1.6" => V1_6,
607 "v1.7" => V1_7,
608 "v1.8" => V1_8,
609 "v1.9" => V1_9,
610 "v1.10" => V1_10,
611 "v1.11" => V1_11,
612 "v1.12" => V1_12,
613 "v1.13" => V1_13,
614 "v1.14" => V1_14,
615 "v1.15" => V1_15,
616 _ => return Err(UnknownVersionError),
617 })
618 }
619}
620
621impl FromStr for MatrixVersion {
622 type Err = UnknownVersionError;
623
624 fn from_str(s: &str) -> Result<Self, Self::Err> {
625 Self::try_from(s)
626 }
627}
628
629impl MatrixVersion {
630 pub fn is_superset_of(self, other: Self) -> bool {
640 self >= other
641 }
642
643 pub const fn into_parts(self) -> (u8, u8) {
645 match self {
646 MatrixVersion::V1_0 => (1, 0),
647 MatrixVersion::V1_1 => (1, 1),
648 MatrixVersion::V1_2 => (1, 2),
649 MatrixVersion::V1_3 => (1, 3),
650 MatrixVersion::V1_4 => (1, 4),
651 MatrixVersion::V1_5 => (1, 5),
652 MatrixVersion::V1_6 => (1, 6),
653 MatrixVersion::V1_7 => (1, 7),
654 MatrixVersion::V1_8 => (1, 8),
655 MatrixVersion::V1_9 => (1, 9),
656 MatrixVersion::V1_10 => (1, 10),
657 MatrixVersion::V1_11 => (1, 11),
658 MatrixVersion::V1_12 => (1, 12),
659 MatrixVersion::V1_13 => (1, 13),
660 MatrixVersion::V1_14 => (1, 14),
661 MatrixVersion::V1_15 => (1, 15),
662 }
663 }
664
665 pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
667 match (major, minor) {
668 (1, 0) => Ok(MatrixVersion::V1_0),
669 (1, 1) => Ok(MatrixVersion::V1_1),
670 (1, 2) => Ok(MatrixVersion::V1_2),
671 (1, 3) => Ok(MatrixVersion::V1_3),
672 (1, 4) => Ok(MatrixVersion::V1_4),
673 (1, 5) => Ok(MatrixVersion::V1_5),
674 (1, 6) => Ok(MatrixVersion::V1_6),
675 (1, 7) => Ok(MatrixVersion::V1_7),
676 (1, 8) => Ok(MatrixVersion::V1_8),
677 (1, 9) => Ok(MatrixVersion::V1_9),
678 (1, 10) => Ok(MatrixVersion::V1_10),
679 (1, 11) => Ok(MatrixVersion::V1_11),
680 (1, 12) => Ok(MatrixVersion::V1_12),
681 (1, 13) => Ok(MatrixVersion::V1_13),
682 (1, 14) => Ok(MatrixVersion::V1_14),
683 (1, 15) => Ok(MatrixVersion::V1_15),
684 _ => Err(UnknownVersionError),
685 }
686 }
687
688 #[doc(hidden)]
692 pub const fn from_lit(lit: &'static str) -> Self {
693 use konst::{option, primitive::parse_u8, result, string};
694
695 let major: u8;
696 let minor: u8;
697
698 let mut lit_iter = string::split(lit, ".").next();
699
700 {
701 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
704 "major version is not a valid number"
705 ));
706
707 lit_iter = checked_split.next();
708 }
709
710 match lit_iter {
711 Some((checked_second, checked_split)) => {
712 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
713 "minor version is not a valid number"
714 ));
715
716 lit_iter = checked_split.next();
717 }
718 None => panic!("could not find dot to denote second number"),
719 }
720
721 if lit_iter.is_some() {
722 panic!("version literal contains more than one dot")
723 }
724
725 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
726 "not a valid version literal"
727 ))
728 }
729
730 const fn const_ord(&self, other: &Self) -> Ordering {
732 let self_parts = self.into_parts();
733 let other_parts = other.into_parts();
734
735 use konst::primitive::cmp::cmp_u8;
736
737 let major_ord = cmp_u8(self_parts.0, other_parts.0);
738 if major_ord.is_ne() {
739 major_ord
740 } else {
741 cmp_u8(self_parts.1, other_parts.1)
742 }
743 }
744
745 const fn is_legacy(&self) -> bool {
747 let self_parts = self.into_parts();
748
749 use konst::primitive::cmp::cmp_u8;
750
751 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
752 }
753
754 pub fn default_room_version(&self) -> RoomVersionId {
756 match self {
757 MatrixVersion::V1_0
759 | MatrixVersion::V1_1
761 | MatrixVersion::V1_2 => RoomVersionId::V6,
763 MatrixVersion::V1_3
765 | MatrixVersion::V1_4
767 | MatrixVersion::V1_5 => RoomVersionId::V9,
769 MatrixVersion::V1_6
771 | MatrixVersion::V1_7
773 | MatrixVersion::V1_8
775 | MatrixVersion::V1_9
777 | MatrixVersion::V1_10
779 | MatrixVersion::V1_11
781 | MatrixVersion::V1_12
783 | MatrixVersion::V1_13 => RoomVersionId::V10,
785 | MatrixVersion::V1_14
787 | MatrixVersion::V1_15 => RoomVersionId::V11,
789 }
790 }
791}
792
793impl Display for MatrixVersion {
794 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795 let (major, minor) = self.into_parts();
796 f.write_str(&format!("v{major}.{minor}"))
797 }
798}
799
800#[derive(Debug, Clone)]
802#[allow(clippy::exhaustive_structs)]
803pub struct SupportedVersions {
804 pub versions: Box<[MatrixVersion]>,
808
809 pub features: BTreeSet<FeatureFlag>,
814}
815
816impl SupportedVersions {
817 pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
822 Self {
823 versions: versions
824 .iter()
825 .flat_map(|s| s.parse::<MatrixVersion>())
827 .map(|v| (v.into_parts(), v))
830 .collect::<BTreeMap<_, _>>()
832 .into_values()
834 .collect(),
835 features: unstable_features
836 .iter()
837 .filter(|(_, enabled)| **enabled)
838 .map(|(feature, _)| feature.as_str().into())
839 .collect(),
840 }
841 }
842}
843
844#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
850#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
851#[non_exhaustive]
852pub enum FeatureFlag {
853 #[ruma_enum(rename = "fi.mau.msc2246")]
859 Msc2246,
860
861 #[ruma_enum(rename = "org.matrix.msc2432")]
867 Msc2432,
868
869 #[ruma_enum(rename = "fi.mau.msc2659")]
875 Msc2659,
876
877 #[ruma_enum(rename = "fi.mau.msc2659.stable")]
883 Msc2659Stable,
884
885 #[cfg(feature = "unstable-msc2666")]
891 #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
892 Msc2666,
893
894 #[ruma_enum(rename = "org.matrix.msc3030")]
900 Msc3030,
901
902 #[ruma_enum(rename = "org.matrix.msc3882")]
908 Msc3882,
909
910 #[ruma_enum(rename = "org.matrix.msc3916")]
916 Msc3916,
917
918 #[ruma_enum(rename = "org.matrix.msc3916.stable")]
924 Msc3916Stable,
925
926 #[cfg(feature = "unstable-msc4108")]
932 #[ruma_enum(rename = "org.matrix.msc4108")]
933 Msc4108,
934
935 #[cfg(feature = "unstable-msc4140")]
941 #[ruma_enum(rename = "org.matrix.msc4140")]
942 Msc4140,
943
944 #[cfg(feature = "unstable-msc4186")]
950 #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
951 Msc4186,
952
953 #[doc(hidden)]
954 _Custom(PrivOwnedStr),
955}
956
957#[cfg(test)]
958mod tests {
959 use std::collections::{BTreeMap, BTreeSet};
960
961 use assert_matches2::assert_matches;
962 use http::Method;
963
964 use super::{
965 AuthScheme,
966 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
967 Metadata, SupportedVersions, VersionHistory,
968 };
969 use crate::api::error::IntoHttpError;
970
971 fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
972 Metadata {
973 method: Method::GET,
974 rate_limited: false,
975 authentication: AuthScheme::None,
976 history: VersionHistory {
977 unstable_paths: &[],
978 stable_paths,
979 deprecated: None,
980 removed: None,
981 },
982 }
983 }
984
985 #[test]
988 fn make_simple_endpoint_url() {
989 let meta = stable_only_metadata(&[(V1_0, "/s")]);
990 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
991 assert_eq!(url, "https://example.org/s");
992 }
993
994 #[test]
995 fn make_endpoint_url_with_path_args() {
996 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
997 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
998 assert_eq!(url, "https://example.org/s/123");
999 }
1000
1001 #[test]
1002 fn make_endpoint_url_with_path_args_with_dash() {
1003 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1004 let url =
1005 meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
1006 assert_eq!(url, "https://example.org/s/my-path");
1007 }
1008
1009 #[test]
1010 fn make_endpoint_url_with_path_args_with_reserved_char() {
1011 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1012 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
1013 assert_eq!(url, "https://example.org/s/%23path");
1014 }
1015
1016 #[test]
1017 fn make_endpoint_url_with_query() {
1018 let meta = stable_only_metadata(&[(V1_0, "/s/")]);
1019 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
1020 assert_eq!(url, "https://example.org/s/?foo=bar");
1021 }
1022
1023 #[test]
1024 #[should_panic]
1025 fn make_endpoint_url_wrong_num_path_args() {
1026 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
1027 _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
1028 }
1029
1030 const EMPTY: VersionHistory =
1031 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1032
1033 #[test]
1034 fn select_latest_stable() {
1035 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
1036 assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
1037 }
1038
1039 #[test]
1040 fn select_unstable() {
1041 let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
1042 assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
1043 }
1044
1045 #[test]
1046 fn select_r0() {
1047 let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
1048 assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
1049 }
1050
1051 #[test]
1052 fn select_removed_err() {
1053 let hist = VersionHistory {
1054 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
1055 unstable_paths: &["/u"],
1056 deprecated: Some(V1_2),
1057 removed: Some(V1_3),
1058 };
1059 assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
1060 }
1061
1062 #[test]
1063 fn partially_removed_but_stable() {
1064 let hist = VersionHistory {
1065 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
1066 unstable_paths: &[],
1067 deprecated: Some(V1_2),
1068 removed: Some(V1_3),
1069 };
1070 assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
1071 }
1072
1073 #[test]
1074 fn no_unstable() {
1075 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
1076 assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
1077 }
1078
1079 #[test]
1080 fn version_literal() {
1081 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1082
1083 assert_eq!(LIT, V1_0);
1084 }
1085
1086 #[test]
1087 fn supported_versions_from_parts() {
1088 let empty_features = BTreeMap::new();
1089
1090 let none = &[];
1091 let none_supported = SupportedVersions::from_parts(none, &empty_features);
1092 assert_eq!(none_supported.versions.as_ref(), &[]);
1093 assert_eq!(none_supported.features, BTreeSet::new());
1094
1095 let single_known = &["r0.6.0".to_owned()];
1096 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1097 assert_eq!(single_known_supported.versions.as_ref(), &[V1_0]);
1098 assert_eq!(single_known_supported.features, BTreeSet::new());
1099
1100 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1101 let multiple_known_supported =
1102 SupportedVersions::from_parts(multiple_known, &empty_features);
1103 assert_eq!(multiple_known_supported.versions.as_ref(), &[V1_0, V1_1]);
1104 assert_eq!(multiple_known_supported.features, BTreeSet::new());
1105
1106 let single_unknown = &["v0.0".to_owned()];
1107 let single_unknown_supported =
1108 SupportedVersions::from_parts(single_unknown, &empty_features);
1109 assert_eq!(single_unknown_supported.versions.as_ref(), &[]);
1110 assert_eq!(single_unknown_supported.features, BTreeSet::new());
1111
1112 let mut features = BTreeMap::new();
1113 features.insert("org.bar.enabled_1".to_owned(), true);
1114 features.insert("org.bar.disabled".to_owned(), false);
1115 features.insert("org.bar.enabled_2".to_owned(), true);
1116
1117 let features_supported = SupportedVersions::from_parts(single_known, &features);
1118 assert_eq!(features_supported.versions.as_ref(), &[V1_0]);
1119 assert_eq!(
1120 features_supported.features,
1121 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1122 );
1123 }
1124}