1use std::{
2 cmp::Ordering,
3 collections::{BTreeMap, BTreeSet},
4 fmt::{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#[doc(hidden)]
96#[macro_export]
97macro_rules! metadata {
98 ( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
99 $crate::api::Metadata {
100 $( $field: $crate::metadata!(@field $field: $rhs) ),+
101 }
102 };
103
104 ( @field method: $method:ident ) => { $crate::exports::http::Method::$method };
105
106 ( @field authentication: $scheme:ident ) => { $crate::api::AuthScheme::$scheme };
107
108 ( @field history: {
109 $( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
110 $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
111 $( $( $version:literal $(| stable ($stable_feature:literal))? => $rhs:tt, )+ )?
112 } ) => {
113 $crate::metadata! {
114 @history_impl
115 [ $( $unstable_path $(= $unstable_feature)? ),* ]
116 $( stable ($stable_feature_only) => $stable_feature_path, )?
117 $( $( $rhs = $version $(| stable ($stable_feature))? ),+ )?
119 }
120 };
121
122 ( @field $_field:ident: $rhs:expr ) => { $rhs };
124
125 ( @history_impl
126 [ $( $unstable_path:literal $(= $unstable_feature:literal)? ),* ]
127 $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
128 $(
129 $( $stable_path:literal = $version:literal $(| stable ($stable_feature:literal))? ),+
130 $(,
131 deprecated = $deprecated_version:literal
132 $(, removed = $removed_version:literal )?
133 )?
134 )?
135 ) => {
136 $crate::api::VersionHistory::new(
137 &[ $(($crate::metadata!(@optional_feature $($unstable_feature)?), $unstable_path)),* ],
138 &[
139 $((
140 $crate::metadata!(@stable_path_selector stable($stable_feature_only)),
141 $stable_feature_path
142 ),)?
143 $($((
144 $crate::metadata!(@stable_path_selector $version $(| stable($stable_feature))?),
145 $stable_path
146 )),+)?
147 ],
148 $crate::metadata!(@optional_version $($( $deprecated_version )?)?),
149 $crate::metadata!(@optional_version $($($( $removed_version )?)?)?),
150 )
151 };
152
153 ( @optional_feature ) => { None };
154 ( @optional_feature $feature:literal ) => { Some($feature) };
155 ( @stable_path_selector stable($feature:literal)) => { $crate::api::StablePathSelector::Feature($feature) };
156 ( @stable_path_selector $version:literal | stable($feature:literal)) => {
157 $crate::api::StablePathSelector::FeatureAndVersion {
158 feature: $feature,
159 version: $crate::api::MatrixVersion::from_lit(stringify!($version)),
160 }
161 };
162 ( @stable_path_selector $version:literal) => { $crate::api::StablePathSelector::Version($crate::api::MatrixVersion::from_lit(stringify!($version))) };
163 ( @optional_version ) => { None };
164 ( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
165}
166
167#[derive(Clone, Debug, PartialEq, Eq)]
169#[allow(clippy::exhaustive_structs)]
170pub struct Metadata {
171 pub method: Method,
173
174 pub rate_limited: bool,
176
177 pub authentication: AuthScheme,
179
180 pub history: VersionHistory,
182}
183
184impl Metadata {
185 pub fn empty_request_body<B>(&self) -> B
190 where
191 B: Default + BufMut,
192 {
193 if self.method == Method::GET {
194 Default::default()
195 } else {
196 slice_to_buf(b"{}")
197 }
198 }
199
200 pub fn authorization_header(
206 &self,
207 access_token: SendAccessToken<'_>,
208 ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
209 Ok(match self.authentication {
210 AuthScheme::None => match access_token.get_not_required_for_endpoint() {
211 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
212 None => None,
213 },
214
215 AuthScheme::AccessToken => {
216 let token = access_token
217 .get_required_for_endpoint()
218 .ok_or(IntoHttpError::NeedsAuthentication)?;
219
220 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
221 }
222
223 AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
224 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
225 None => None,
226 },
227
228 AuthScheme::AppserviceToken => {
229 let token = access_token
230 .get_required_for_appservice()
231 .ok_or(IntoHttpError::NeedsAuthentication)?;
232
233 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
234 }
235
236 AuthScheme::AppserviceTokenOptional => match access_token.get_required_for_appservice()
237 {
238 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
239 None => None,
240 },
241
242 AuthScheme::ServerSignatures => None,
243 })
244 }
245
246 pub fn make_endpoint_url(
248 &self,
249 considering: &SupportedVersions,
250 base_url: &str,
251 path_args: &[&dyn Display],
252 query_string: &str,
253 ) -> Result<String, IntoHttpError> {
254 let path_with_placeholders = self.history.select_path(considering)?;
255
256 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
257 let mut segments = path_with_placeholders.split('/');
258 let mut path_args = path_args.iter();
259
260 let first_segment = segments.next().expect("split iterator is never empty");
261 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
262
263 for segment in segments {
264 if Self::extract_endpoint_path_segment_variable(segment).is_some() {
265 let arg = path_args
266 .next()
267 .expect("number of placeholders must match number of arguments")
268 .to_string();
269 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
270
271 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
272 } else {
273 res.reserve(segment.len() + 1);
274 res.push('/');
275 res.push_str(segment);
276 }
277 }
278
279 if !query_string.is_empty() {
280 res.push('?');
281 res.push_str(query_string);
282 }
283
284 Ok(res)
285 }
286
287 #[doc(hidden)]
291 pub fn _path_parameters(&self) -> Vec<&'static str> {
292 let path = self.history.all_paths().next().unwrap();
293 path.split('/').filter_map(Self::extract_endpoint_path_segment_variable).collect()
294 }
295
296 fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
310 if segment.starts_with(':') {
311 panic!(
312 "endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`"
313 );
314 }
315
316 if let Some(var) = segment.strip_prefix('{').map(|s| {
317 s.strip_suffix('}')
318 .expect("endpoint path segment variable braces mismatch: missing ending `}`")
319 }) {
320 return Some(var);
321 }
322
323 if segment.ends_with('}') {
324 panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
325 }
326
327 None
328 }
329}
330
331#[derive(Clone, Debug, PartialEq, Eq)]
336#[allow(clippy::exhaustive_structs)]
337pub struct VersionHistory {
338 unstable_paths: &'static [(Option<&'static str>, &'static str)],
343
344 stable_paths: &'static [(StablePathSelector, &'static str)],
348
349 deprecated: Option<MatrixVersion>,
356
357 removed: Option<MatrixVersion>,
362}
363
364impl VersionHistory {
365 pub const fn new(
387 unstable_paths: &'static [(Option<&'static str>, &'static str)],
388 stable_paths: &'static [(StablePathSelector, &'static str)],
389 deprecated: Option<MatrixVersion>,
390 removed: Option<MatrixVersion>,
391 ) -> Self {
392 use konst::{iter, slice, string};
393
394 const fn check_path_is_valid(path: &'static str) {
395 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
396 match *path_b {
397 0x21..=0x7E => {},
398 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
399 }
400 });
401 }
402
403 const fn check_path_args_equal(first: &'static str, second: &'static str) {
404 let mut second_iter = string::split(second, "/").next();
405
406 iter::for_each!(first_s in string::split(first, "/") => {
407 if let Some(first_arg) = string::strip_prefix(first_s, ":") {
408 let second_next_arg: Option<&'static str> = loop {
409 let Some((second_s, second_n_iter)) = second_iter else {
410 break None;
411 };
412
413 let maybe_second_arg = string::strip_prefix(second_s, ":");
414
415 second_iter = second_n_iter.next();
416
417 if let Some(second_arg) = maybe_second_arg {
418 break Some(second_arg);
419 }
420 };
421
422 if let Some(second_next_arg) = second_next_arg {
423 if !string::eq_str(second_next_arg, first_arg) {
424 panic!("Path Arguments do not match");
425 }
426 } else {
427 panic!("Amount of Path Arguments do not match");
428 }
429 }
430 });
431
432 while let Some((second_s, second_n_iter)) = second_iter {
434 if string::starts_with(second_s, ":") {
435 panic!("Amount of Path Arguments do not match");
436 }
437 second_iter = second_n_iter.next();
438 }
439 }
440
441 let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
443 s
444 } else if let Some((_, s)) = stable_paths.first() {
445 s
446 } else {
447 panic!("No paths supplied")
448 };
449
450 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
451 check_path_is_valid(unstable_path.1);
452 check_path_args_equal(ref_path, unstable_path.1);
453 });
454
455 let mut prev_seen_version: Option<MatrixVersion> = None;
456
457 iter::for_each!(version_path in slice::iter(stable_paths) => {
458 check_path_is_valid(version_path.1);
459 check_path_args_equal(ref_path, version_path.1);
460
461 if let Some(current_version) = version_path.0.version() {
462 if let Some(prev_seen_version) = prev_seen_version {
463 let cmp_result = current_version.const_ord(&prev_seen_version);
464
465 if cmp_result.is_eq() {
466 panic!("Duplicate matrix version in stable_paths")
468 } else if cmp_result.is_lt() {
469 panic!("No ascending order in stable_paths")
471 }
472 }
473
474 prev_seen_version = Some(current_version);
475 }
476 });
477
478 if let Some(deprecated) = deprecated {
479 if let Some(prev_seen_version) = prev_seen_version {
480 let ord_result = prev_seen_version.const_ord(&deprecated);
481 if !deprecated.is_legacy() && ord_result.is_eq() {
482 panic!("deprecated version is equal to latest stable path version")
486 } else if ord_result.is_gt() {
487 panic!("deprecated version is older than latest stable path version")
489 }
490 } else {
491 panic!("Defined deprecated version while no stable path exists")
492 }
493 }
494
495 if let Some(removed) = removed {
496 if let Some(deprecated) = deprecated {
497 let ord_result = deprecated.const_ord(&removed);
498 if ord_result.is_eq() {
499 panic!("removed version is equal to deprecated version")
501 } else if ord_result.is_gt() {
502 panic!("removed version is older than deprecated version")
504 }
505 } else {
506 panic!("Defined removed version while no deprecated version exists")
507 }
508 }
509
510 VersionHistory { unstable_paths, stable_paths, deprecated, removed }
511 }
512
513 pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
522 match self.versioning_decision_for(&considering.versions) {
523 VersioningDecision::Removed => false,
524 VersioningDecision::Version { .. } => true,
525 VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
526 }
527 }
528
529 fn select_path(&self, considering: &SupportedVersions) -> Result<&'static str, IntoHttpError> {
532 match self.versioning_decision_for(&considering.versions) {
533 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
534 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
535 )),
536 VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
537 if any_removed {
538 if all_deprecated {
539 warn!(
540 "endpoint is removed in some (and deprecated in ALL) \
541 of the following versions: {:?}",
542 considering.versions
543 );
544 } else if any_deprecated {
545 warn!(
546 "endpoint is removed (and deprecated) in some of the \
547 following versions: {:?}",
548 considering.versions
549 );
550 } else {
551 unreachable!("any_removed implies *_deprecated");
552 }
553 } else if all_deprecated {
554 warn!(
555 "endpoint is deprecated in ALL of the following versions: {:?}",
556 considering.versions
557 );
558 } else if any_deprecated {
559 warn!(
560 "endpoint is deprecated in some of the following versions: {:?}",
561 considering.versions
562 );
563 }
564
565 Ok(self
566 .version_path(&considering.versions)
567 .expect("VersioningDecision::Version implies that a version path exists"))
568 }
569 VersioningDecision::Feature => self
570 .feature_path(&considering.features)
571 .or_else(|| self.unstable())
572 .ok_or(IntoHttpError::NoUnstablePath),
573 }
574 }
575
576 pub fn versioning_decision_for(
588 &self,
589 versions: &BTreeSet<MatrixVersion>,
590 ) -> VersioningDecision {
591 let is_superset_any =
592 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
593 let is_superset_all =
594 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
595
596 if self.removed.is_some_and(is_superset_all) {
598 return VersioningDecision::Removed;
599 }
600
601 if self.added_in().is_some_and(is_superset_any) {
603 let all_deprecated = self.deprecated.is_some_and(is_superset_all);
604
605 return VersioningDecision::Version {
606 any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
607 all_deprecated,
608 any_removed: self.removed.is_some_and(is_superset_any),
609 };
610 }
611
612 VersioningDecision::Feature
613 }
614
615 pub fn added_in(&self) -> Option<MatrixVersion> {
619 self.stable_paths.iter().find_map(|(v, _)| v.version())
620 }
621
622 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
624 self.deprecated
625 }
626
627 pub fn removed_in(&self) -> Option<MatrixVersion> {
629 self.removed
630 }
631
632 pub fn unstable(&self) -> Option<&'static str> {
634 self.unstable_paths.last().map(|(_, path)| *path)
635 }
636
637 pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
639 self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
640 }
641
642 pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
644 self.unstable_paths.iter().copied()
645 }
646
647 pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
649 self.stable_paths.iter().copied()
650 }
651
652 pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
664 let version_paths = self
665 .stable_paths
666 .iter()
667 .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
668
669 for (ver, path) in version_paths.rev() {
671 if versions.iter().any(|v| v.is_superset_of(ver)) {
673 return Some(path);
674 }
675 }
676
677 None
678 }
679
680 pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
682 let unstable_feature_paths = self
683 .unstable_paths
684 .iter()
685 .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
686 let stable_feature_paths = self
687 .stable_paths
688 .iter()
689 .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
690
691 for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
693 if supported_features.iter().any(|supported| supported.as_str() == feature) {
695 return Some(path);
696 }
697 }
698
699 None
700 }
701}
702
703#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
705#[allow(clippy::exhaustive_enums)]
706pub enum VersioningDecision {
707 Feature,
709
710 Version {
712 any_deprecated: bool,
714
715 all_deprecated: bool,
717
718 any_removed: bool,
720 },
721
722 Removed,
724}
725
726#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
747#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
748pub enum MatrixVersion {
749 V1_0,
761
762 V1_1,
766
767 V1_2,
771
772 V1_3,
776
777 V1_4,
781
782 V1_5,
786
787 V1_6,
791
792 V1_7,
796
797 V1_8,
801
802 V1_9,
806
807 V1_10,
811
812 V1_11,
816
817 V1_12,
821
822 V1_13,
826
827 V1_14,
831
832 V1_15,
836}
837
838impl TryFrom<&str> for MatrixVersion {
839 type Error = UnknownVersionError;
840
841 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
842 use MatrixVersion::*;
843
844 Ok(match value {
845 "r0.2.0" | "r0.2.1" | "r0.3.0" |
848 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
850 "v1.1" => V1_1,
851 "v1.2" => V1_2,
852 "v1.3" => V1_3,
853 "v1.4" => V1_4,
854 "v1.5" => V1_5,
855 "v1.6" => V1_6,
856 "v1.7" => V1_7,
857 "v1.8" => V1_8,
858 "v1.9" => V1_9,
859 "v1.10" => V1_10,
860 "v1.11" => V1_11,
861 "v1.12" => V1_12,
862 "v1.13" => V1_13,
863 "v1.14" => V1_14,
864 "v1.15" => V1_15,
865 _ => return Err(UnknownVersionError),
866 })
867 }
868}
869
870impl FromStr for MatrixVersion {
871 type Err = UnknownVersionError;
872
873 fn from_str(s: &str) -> Result<Self, Self::Err> {
874 Self::try_from(s)
875 }
876}
877
878impl MatrixVersion {
879 pub fn is_superset_of(self, other: Self) -> bool {
889 self >= other
890 }
891
892 pub const fn as_str(self) -> Option<&'static str> {
899 let string = match self {
900 MatrixVersion::V1_0 => return None,
901 MatrixVersion::V1_1 => "v1.1",
902 MatrixVersion::V1_2 => "v1.2",
903 MatrixVersion::V1_3 => "v1.3",
904 MatrixVersion::V1_4 => "v1.4",
905 MatrixVersion::V1_5 => "v1.5",
906 MatrixVersion::V1_6 => "v1.6",
907 MatrixVersion::V1_7 => "v1.7",
908 MatrixVersion::V1_8 => "v1.8",
909 MatrixVersion::V1_9 => "v1.9",
910 MatrixVersion::V1_10 => "v1.10",
911 MatrixVersion::V1_11 => "v1.11",
912 MatrixVersion::V1_12 => "v1.12",
913 MatrixVersion::V1_13 => "v1.13",
914 MatrixVersion::V1_14 => "v1.14",
915 MatrixVersion::V1_15 => "v1.15",
916 };
917
918 Some(string)
919 }
920
921 const fn into_parts(self) -> (u8, u8) {
923 match self {
924 MatrixVersion::V1_0 => (1, 0),
925 MatrixVersion::V1_1 => (1, 1),
926 MatrixVersion::V1_2 => (1, 2),
927 MatrixVersion::V1_3 => (1, 3),
928 MatrixVersion::V1_4 => (1, 4),
929 MatrixVersion::V1_5 => (1, 5),
930 MatrixVersion::V1_6 => (1, 6),
931 MatrixVersion::V1_7 => (1, 7),
932 MatrixVersion::V1_8 => (1, 8),
933 MatrixVersion::V1_9 => (1, 9),
934 MatrixVersion::V1_10 => (1, 10),
935 MatrixVersion::V1_11 => (1, 11),
936 MatrixVersion::V1_12 => (1, 12),
937 MatrixVersion::V1_13 => (1, 13),
938 MatrixVersion::V1_14 => (1, 14),
939 MatrixVersion::V1_15 => (1, 15),
940 }
941 }
942
943 const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
945 match (major, minor) {
946 (1, 0) => Ok(MatrixVersion::V1_0),
947 (1, 1) => Ok(MatrixVersion::V1_1),
948 (1, 2) => Ok(MatrixVersion::V1_2),
949 (1, 3) => Ok(MatrixVersion::V1_3),
950 (1, 4) => Ok(MatrixVersion::V1_4),
951 (1, 5) => Ok(MatrixVersion::V1_5),
952 (1, 6) => Ok(MatrixVersion::V1_6),
953 (1, 7) => Ok(MatrixVersion::V1_7),
954 (1, 8) => Ok(MatrixVersion::V1_8),
955 (1, 9) => Ok(MatrixVersion::V1_9),
956 (1, 10) => Ok(MatrixVersion::V1_10),
957 (1, 11) => Ok(MatrixVersion::V1_11),
958 (1, 12) => Ok(MatrixVersion::V1_12),
959 (1, 13) => Ok(MatrixVersion::V1_13),
960 (1, 14) => Ok(MatrixVersion::V1_14),
961 (1, 15) => Ok(MatrixVersion::V1_15),
962 _ => Err(UnknownVersionError),
963 }
964 }
965
966 #[doc(hidden)]
970 pub const fn from_lit(lit: &'static str) -> Self {
971 use konst::{option, primitive::parse_u8, result, string};
972
973 let major: u8;
974 let minor: u8;
975
976 let mut lit_iter = string::split(lit, ".").next();
977
978 {
979 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
982 "major version is not a valid number"
983 ));
984
985 lit_iter = checked_split.next();
986 }
987
988 match lit_iter {
989 Some((checked_second, checked_split)) => {
990 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
991 "minor version is not a valid number"
992 ));
993
994 lit_iter = checked_split.next();
995 }
996 None => panic!("could not find dot to denote second number"),
997 }
998
999 if lit_iter.is_some() {
1000 panic!("version literal contains more than one dot")
1001 }
1002
1003 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
1004 "not a valid version literal"
1005 ))
1006 }
1007
1008 const fn const_ord(&self, other: &Self) -> Ordering {
1010 let self_parts = self.into_parts();
1011 let other_parts = other.into_parts();
1012
1013 use konst::primitive::cmp::cmp_u8;
1014
1015 let major_ord = cmp_u8(self_parts.0, other_parts.0);
1016 if major_ord.is_ne() {
1017 major_ord
1018 } else {
1019 cmp_u8(self_parts.1, other_parts.1)
1020 }
1021 }
1022
1023 const fn is_legacy(&self) -> bool {
1025 let self_parts = self.into_parts();
1026
1027 use konst::primitive::cmp::cmp_u8;
1028
1029 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
1030 }
1031
1032 pub fn default_room_version(&self) -> RoomVersionId {
1034 match self {
1035 MatrixVersion::V1_0
1037 | MatrixVersion::V1_1
1039 | MatrixVersion::V1_2 => RoomVersionId::V6,
1041 MatrixVersion::V1_3
1043 | MatrixVersion::V1_4
1045 | MatrixVersion::V1_5 => RoomVersionId::V9,
1047 MatrixVersion::V1_6
1049 | MatrixVersion::V1_7
1051 | MatrixVersion::V1_8
1053 | MatrixVersion::V1_9
1055 | MatrixVersion::V1_10
1057 | MatrixVersion::V1_11
1059 | MatrixVersion::V1_12
1061 | MatrixVersion::V1_13 => RoomVersionId::V10,
1063 | MatrixVersion::V1_14
1065 | MatrixVersion::V1_15 => RoomVersionId::V11,
1067 }
1068 }
1069}
1070
1071#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1073#[allow(clippy::exhaustive_enums)]
1074pub enum StablePathSelector {
1075 Feature(&'static str),
1077
1078 Version(MatrixVersion),
1080
1081 FeatureAndVersion {
1083 feature: &'static str,
1085 version: MatrixVersion,
1087 },
1088}
1089
1090impl StablePathSelector {
1091 pub const fn feature(self) -> Option<&'static str> {
1093 match self {
1094 Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
1095 _ => None,
1096 }
1097 }
1098
1099 pub const fn version(self) -> Option<MatrixVersion> {
1101 match self {
1102 Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
1103 _ => None,
1104 }
1105 }
1106}
1107
1108impl From<MatrixVersion> for StablePathSelector {
1109 fn from(value: MatrixVersion) -> Self {
1110 Self::Version(value)
1111 }
1112}
1113
1114#[derive(Debug, Clone)]
1116#[allow(clippy::exhaustive_structs)]
1117pub struct SupportedVersions {
1118 pub versions: BTreeSet<MatrixVersion>,
1122
1123 pub features: BTreeSet<FeatureFlag>,
1128}
1129
1130impl SupportedVersions {
1131 pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
1136 Self {
1137 versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
1138 features: unstable_features
1139 .iter()
1140 .filter(|(_, enabled)| **enabled)
1141 .map(|(feature, _)| feature.as_str().into())
1142 .collect(),
1143 }
1144 }
1145}
1146
1147#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
1153#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
1154#[non_exhaustive]
1155pub enum FeatureFlag {
1156 #[ruma_enum(rename = "fi.mau.msc2246")]
1162 Msc2246,
1163
1164 #[ruma_enum(rename = "org.matrix.msc2432")]
1170 Msc2432,
1171
1172 #[ruma_enum(rename = "fi.mau.msc2659")]
1178 Msc2659,
1179
1180 #[ruma_enum(rename = "fi.mau.msc2659.stable")]
1186 Msc2659Stable,
1187
1188 #[cfg(feature = "unstable-msc2666")]
1194 #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
1195 Msc2666,
1196
1197 #[ruma_enum(rename = "org.matrix.msc3030")]
1203 Msc3030,
1204
1205 #[ruma_enum(rename = "org.matrix.msc3882")]
1211 Msc3882,
1212
1213 #[ruma_enum(rename = "org.matrix.msc3916")]
1219 Msc3916,
1220
1221 #[ruma_enum(rename = "org.matrix.msc3916.stable")]
1227 Msc3916Stable,
1228
1229 #[cfg(feature = "unstable-msc4108")]
1235 #[ruma_enum(rename = "org.matrix.msc4108")]
1236 Msc4108,
1237
1238 #[cfg(feature = "unstable-msc4140")]
1244 #[ruma_enum(rename = "org.matrix.msc4140")]
1245 Msc4140,
1246
1247 #[cfg(feature = "unstable-msc4186")]
1253 #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
1254 Msc4186,
1255
1256 #[doc(hidden)]
1257 _Custom(PrivOwnedStr),
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262 use std::collections::{BTreeMap, BTreeSet};
1263
1264 use assert_matches2::assert_matches;
1265 use http::Method;
1266
1267 use super::{
1268 AuthScheme,
1269 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
1270 Metadata, StablePathSelector, SupportedVersions, VersionHistory,
1271 };
1272 use crate::api::error::IntoHttpError;
1273
1274 fn stable_only_metadata(
1275 stable_paths: &'static [(StablePathSelector, &'static str)],
1276 ) -> Metadata {
1277 Metadata {
1278 method: Method::GET,
1279 rate_limited: false,
1280 authentication: AuthScheme::None,
1281 history: VersionHistory {
1282 unstable_paths: &[],
1283 stable_paths,
1284 deprecated: None,
1285 removed: None,
1286 },
1287 }
1288 }
1289
1290 fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
1291 SupportedVersions {
1292 versions: versions.iter().copied().collect(),
1293 features: BTreeSet::new(),
1294 }
1295 }
1296
1297 #[test]
1300 fn make_simple_endpoint_url() {
1301 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s")]);
1302 let url = meta
1303 .make_endpoint_url(&version_only_supported(&[V1_0]), "https://example.org", &[], "")
1304 .unwrap();
1305 assert_eq!(url, "https://example.org/s");
1306 }
1307
1308 #[test]
1309 fn make_endpoint_url_with_path_args() {
1310 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1311 let url = meta
1312 .make_endpoint_url(
1313 &version_only_supported(&[V1_0]),
1314 "https://example.org",
1315 &[&"123"],
1316 "",
1317 )
1318 .unwrap();
1319 assert_eq!(url, "https://example.org/s/123");
1320 }
1321
1322 #[test]
1323 fn make_endpoint_url_with_path_args_with_dash() {
1324 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1325 let url = meta
1326 .make_endpoint_url(
1327 &version_only_supported(&[V1_0]),
1328 "https://example.org",
1329 &[&"my-path"],
1330 "",
1331 )
1332 .unwrap();
1333 assert_eq!(url, "https://example.org/s/my-path");
1334 }
1335
1336 #[test]
1337 fn make_endpoint_url_with_path_args_with_reserved_char() {
1338 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1339 let url = meta
1340 .make_endpoint_url(
1341 &version_only_supported(&[V1_0]),
1342 "https://example.org",
1343 &[&"#path"],
1344 "",
1345 )
1346 .unwrap();
1347 assert_eq!(url, "https://example.org/s/%23path");
1348 }
1349
1350 #[test]
1351 fn make_endpoint_url_with_query() {
1352 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/")]);
1353 let url = meta
1354 .make_endpoint_url(
1355 &version_only_supported(&[V1_0]),
1356 "https://example.org",
1357 &[],
1358 "foo=bar",
1359 )
1360 .unwrap();
1361 assert_eq!(url, "https://example.org/s/?foo=bar");
1362 }
1363
1364 #[test]
1365 #[should_panic]
1366 fn make_endpoint_url_wrong_num_path_args() {
1367 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1368 _ = meta.make_endpoint_url(
1369 &version_only_supported(&[V1_0]),
1370 "https://example.org",
1371 &[],
1372 "",
1373 );
1374 }
1375
1376 const EMPTY: VersionHistory =
1377 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1378
1379 #[test]
1380 fn select_version() {
1381 let version_supported = version_only_supported(&[V1_0, V1_1]);
1382 let superset_supported = version_only_supported(&[V1_1]);
1383
1384 let hist =
1386 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
1387 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1388 assert!(hist.is_supported(&version_supported));
1389 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1390 assert!(hist.is_supported(&superset_supported));
1391
1392 let hist = VersionHistory {
1394 stable_paths: &[(
1395 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
1396 "/s",
1397 )],
1398 ..EMPTY
1399 };
1400 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1401 assert!(hist.is_supported(&version_supported));
1402 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1403 assert!(hist.is_supported(&superset_supported));
1404
1405 let hist = VersionHistory {
1407 stable_paths: &[
1408 (StablePathSelector::Version(V1_0), "/s_v1"),
1409 (StablePathSelector::Version(V1_1), "/s_v2"),
1410 ],
1411 ..EMPTY
1412 };
1413 assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
1414 assert!(hist.is_supported(&version_supported));
1415
1416 let unstable_supported = SupportedVersions {
1418 versions: [V1_0].into(),
1419 features: ["org.boo.unstable".into()].into(),
1420 };
1421 let hist = VersionHistory {
1422 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1423 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
1424 ..EMPTY
1425 };
1426 assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
1427 assert!(hist.is_supported(&unstable_supported));
1428 }
1429
1430 #[test]
1431 fn select_stable_feature() {
1432 let supported = SupportedVersions {
1433 versions: [V1_1].into(),
1434 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
1435 };
1436
1437 let hist = VersionHistory {
1439 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1440 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
1441 ..EMPTY
1442 };
1443 assert_matches!(hist.select_path(&supported), Ok("/s"));
1444 assert!(hist.is_supported(&supported));
1445
1446 let hist = VersionHistory {
1448 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1449 stable_paths: &[(
1450 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1451 "/s",
1452 )],
1453 ..EMPTY
1454 };
1455 assert_matches!(hist.select_path(&supported), Ok("/s"));
1456 assert!(hist.is_supported(&supported));
1457 }
1458
1459 #[test]
1460 fn select_unstable_feature() {
1461 let supported = SupportedVersions {
1462 versions: [V1_1].into(),
1463 features: ["org.boo.unstable".into()].into(),
1464 };
1465
1466 let hist = VersionHistory {
1467 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1468 stable_paths: &[(
1469 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1470 "/s",
1471 )],
1472 ..EMPTY
1473 };
1474 assert_matches!(hist.select_path(&supported), Ok("/u"));
1475 assert!(hist.is_supported(&supported));
1476 }
1477
1478 #[test]
1479 fn select_unstable_fallback() {
1480 let supported = version_only_supported(&[V1_0]);
1481 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
1482 assert_matches!(hist.select_path(&supported), Ok("/u"));
1483 assert!(!hist.is_supported(&supported));
1484 }
1485
1486 #[test]
1487 fn select_r0() {
1488 let supported = version_only_supported(&[V1_0]);
1489 let hist =
1490 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
1491 assert_matches!(hist.select_path(&supported), Ok("/r"));
1492 assert!(hist.is_supported(&supported));
1493 }
1494
1495 #[test]
1496 fn select_removed_err() {
1497 let supported = version_only_supported(&[V1_3]);
1498 let hist = VersionHistory {
1499 stable_paths: &[
1500 (StablePathSelector::Version(V1_0), "/r"),
1501 (StablePathSelector::Version(V1_1), "/s"),
1502 ],
1503 unstable_paths: &[(None, "/u")],
1504 deprecated: Some(V1_2),
1505 removed: Some(V1_3),
1506 };
1507 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
1508 assert!(!hist.is_supported(&supported));
1509 }
1510
1511 #[test]
1512 fn partially_removed_but_stable() {
1513 let supported = version_only_supported(&[V1_2]);
1514 let hist = VersionHistory {
1515 stable_paths: &[
1516 (StablePathSelector::Version(V1_0), "/r"),
1517 (StablePathSelector::Version(V1_1), "/s"),
1518 ],
1519 unstable_paths: &[],
1520 deprecated: Some(V1_2),
1521 removed: Some(V1_3),
1522 };
1523 assert_matches!(hist.select_path(&supported), Ok("/s"));
1524 assert!(hist.is_supported(&supported));
1525 }
1526
1527 #[test]
1528 fn no_unstable() {
1529 let supported = version_only_supported(&[V1_0]);
1530 let hist =
1531 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
1532 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
1533 assert!(!hist.is_supported(&supported));
1534 }
1535
1536 #[test]
1537 fn version_literal() {
1538 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1539
1540 assert_eq!(LIT, V1_0);
1541 }
1542
1543 #[test]
1544 fn parse_as_str_sanity() {
1545 let version = MatrixVersion::try_from("r0.5.0").unwrap();
1546 assert_eq!(version, V1_0);
1547 assert_eq!(version.as_str(), None);
1548
1549 let version = MatrixVersion::try_from("v1.1").unwrap();
1550 assert_eq!(version, V1_1);
1551 assert_eq!(version.as_str(), Some("v1.1"));
1552 }
1553
1554 #[test]
1555 fn supported_versions_from_parts() {
1556 let empty_features = BTreeMap::new();
1557
1558 let none = &[];
1559 let none_supported = SupportedVersions::from_parts(none, &empty_features);
1560 assert_eq!(none_supported.versions, BTreeSet::new());
1561 assert_eq!(none_supported.features, BTreeSet::new());
1562
1563 let single_known = &["r0.6.0".to_owned()];
1564 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1565 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
1566 assert_eq!(single_known_supported.features, BTreeSet::new());
1567
1568 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1569 let multiple_known_supported =
1570 SupportedVersions::from_parts(multiple_known, &empty_features);
1571 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
1572 assert_eq!(multiple_known_supported.features, BTreeSet::new());
1573
1574 let single_unknown = &["v0.0".to_owned()];
1575 let single_unknown_supported =
1576 SupportedVersions::from_parts(single_unknown, &empty_features);
1577 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
1578 assert_eq!(single_unknown_supported.features, BTreeSet::new());
1579
1580 let mut features = BTreeMap::new();
1581 features.insert("org.bar.enabled_1".to_owned(), true);
1582 features.insert("org.bar.disabled".to_owned(), false);
1583 features.insert("org.bar.enabled_2".to_owned(), true);
1584
1585 let features_supported = SupportedVersions::from_parts(single_known, &features);
1586 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
1587 assert_eq!(
1588 features_supported.features,
1589 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1590 );
1591 }
1592
1593 #[test]
1594 fn supported_versions_from_parts_order() {
1595 let empty_features = BTreeMap::new();
1596
1597 let sorted = &[
1598 "r0.0.1".to_owned(),
1599 "r0.5.0".to_owned(),
1600 "r0.6.0".to_owned(),
1601 "r0.6.1".to_owned(),
1602 "v1.1".to_owned(),
1603 "v1.2".to_owned(),
1604 ];
1605 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
1606 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1607
1608 let sorted_reverse = &[
1609 "v1.2".to_owned(),
1610 "v1.1".to_owned(),
1611 "r0.6.1".to_owned(),
1612 "r0.6.0".to_owned(),
1613 "r0.5.0".to_owned(),
1614 "r0.0.1".to_owned(),
1615 ];
1616 let sorted_reverse_supported =
1617 SupportedVersions::from_parts(sorted_reverse, &empty_features);
1618 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1619
1620 let random_order = &[
1621 "v1.1".to_owned(),
1622 "r0.6.1".to_owned(),
1623 "r0.5.0".to_owned(),
1624 "r0.6.0".to_owned(),
1625 "r0.0.1".to_owned(),
1626 "v1.2".to_owned(),
1627 ];
1628 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1629 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1630 }
1631
1632 #[test]
1633 #[should_panic]
1634 fn make_endpoint_url_with_path_args_old_syntax() {
1635 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1636 let url = meta
1637 .make_endpoint_url(
1638 &version_only_supported(&[V1_0]),
1639 "https://example.org",
1640 &[&"123"],
1641 "",
1642 )
1643 .unwrap();
1644 assert_eq!(url, "https://example.org/s/123");
1645 }
1646}