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 V1_16,
841}
842
843impl TryFrom<&str> for MatrixVersion {
844 type Error = UnknownVersionError;
845
846 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
847 use MatrixVersion::*;
848
849 Ok(match value {
850 "r0.2.0" | "r0.2.1" | "r0.3.0" |
853 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
855 "v1.1" => V1_1,
856 "v1.2" => V1_2,
857 "v1.3" => V1_3,
858 "v1.4" => V1_4,
859 "v1.5" => V1_5,
860 "v1.6" => V1_6,
861 "v1.7" => V1_7,
862 "v1.8" => V1_8,
863 "v1.9" => V1_9,
864 "v1.10" => V1_10,
865 "v1.11" => V1_11,
866 "v1.12" => V1_12,
867 "v1.13" => V1_13,
868 "v1.14" => V1_14,
869 "v1.15" => V1_15,
870 "v1.16" => V1_16,
871 _ => return Err(UnknownVersionError),
872 })
873 }
874}
875
876impl FromStr for MatrixVersion {
877 type Err = UnknownVersionError;
878
879 fn from_str(s: &str) -> Result<Self, Self::Err> {
880 Self::try_from(s)
881 }
882}
883
884impl MatrixVersion {
885 pub fn is_superset_of(self, other: Self) -> bool {
895 self >= other
896 }
897
898 pub const fn as_str(self) -> Option<&'static str> {
905 let string = match self {
906 MatrixVersion::V1_0 => return None,
907 MatrixVersion::V1_1 => "v1.1",
908 MatrixVersion::V1_2 => "v1.2",
909 MatrixVersion::V1_3 => "v1.3",
910 MatrixVersion::V1_4 => "v1.4",
911 MatrixVersion::V1_5 => "v1.5",
912 MatrixVersion::V1_6 => "v1.6",
913 MatrixVersion::V1_7 => "v1.7",
914 MatrixVersion::V1_8 => "v1.8",
915 MatrixVersion::V1_9 => "v1.9",
916 MatrixVersion::V1_10 => "v1.10",
917 MatrixVersion::V1_11 => "v1.11",
918 MatrixVersion::V1_12 => "v1.12",
919 MatrixVersion::V1_13 => "v1.13",
920 MatrixVersion::V1_14 => "v1.14",
921 MatrixVersion::V1_15 => "v1.15",
922 MatrixVersion::V1_16 => "v1.16",
923 };
924
925 Some(string)
926 }
927
928 const fn into_parts(self) -> (u8, u8) {
930 match self {
931 MatrixVersion::V1_0 => (1, 0),
932 MatrixVersion::V1_1 => (1, 1),
933 MatrixVersion::V1_2 => (1, 2),
934 MatrixVersion::V1_3 => (1, 3),
935 MatrixVersion::V1_4 => (1, 4),
936 MatrixVersion::V1_5 => (1, 5),
937 MatrixVersion::V1_6 => (1, 6),
938 MatrixVersion::V1_7 => (1, 7),
939 MatrixVersion::V1_8 => (1, 8),
940 MatrixVersion::V1_9 => (1, 9),
941 MatrixVersion::V1_10 => (1, 10),
942 MatrixVersion::V1_11 => (1, 11),
943 MatrixVersion::V1_12 => (1, 12),
944 MatrixVersion::V1_13 => (1, 13),
945 MatrixVersion::V1_14 => (1, 14),
946 MatrixVersion::V1_15 => (1, 15),
947 MatrixVersion::V1_16 => (1, 16),
948 }
949 }
950
951 const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
953 match (major, minor) {
954 (1, 0) => Ok(MatrixVersion::V1_0),
955 (1, 1) => Ok(MatrixVersion::V1_1),
956 (1, 2) => Ok(MatrixVersion::V1_2),
957 (1, 3) => Ok(MatrixVersion::V1_3),
958 (1, 4) => Ok(MatrixVersion::V1_4),
959 (1, 5) => Ok(MatrixVersion::V1_5),
960 (1, 6) => Ok(MatrixVersion::V1_6),
961 (1, 7) => Ok(MatrixVersion::V1_7),
962 (1, 8) => Ok(MatrixVersion::V1_8),
963 (1, 9) => Ok(MatrixVersion::V1_9),
964 (1, 10) => Ok(MatrixVersion::V1_10),
965 (1, 11) => Ok(MatrixVersion::V1_11),
966 (1, 12) => Ok(MatrixVersion::V1_12),
967 (1, 13) => Ok(MatrixVersion::V1_13),
968 (1, 14) => Ok(MatrixVersion::V1_14),
969 (1, 15) => Ok(MatrixVersion::V1_15),
970 (1, 16) => Ok(MatrixVersion::V1_16),
971 _ => Err(UnknownVersionError),
972 }
973 }
974
975 #[doc(hidden)]
979 pub const fn from_lit(lit: &'static str) -> Self {
980 use konst::{option, primitive::parse_u8, result, string};
981
982 let major: u8;
983 let minor: u8;
984
985 let mut lit_iter = string::split(lit, ".").next();
986
987 {
988 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
991 "major version is not a valid number"
992 ));
993
994 lit_iter = checked_split.next();
995 }
996
997 match lit_iter {
998 Some((checked_second, checked_split)) => {
999 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
1000 "minor version is not a valid number"
1001 ));
1002
1003 lit_iter = checked_split.next();
1004 }
1005 None => panic!("could not find dot to denote second number"),
1006 }
1007
1008 if lit_iter.is_some() {
1009 panic!("version literal contains more than one dot")
1010 }
1011
1012 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
1013 "not a valid version literal"
1014 ))
1015 }
1016
1017 const fn const_ord(&self, other: &Self) -> Ordering {
1019 let self_parts = self.into_parts();
1020 let other_parts = other.into_parts();
1021
1022 use konst::primitive::cmp::cmp_u8;
1023
1024 let major_ord = cmp_u8(self_parts.0, other_parts.0);
1025 if major_ord.is_ne() {
1026 major_ord
1027 } else {
1028 cmp_u8(self_parts.1, other_parts.1)
1029 }
1030 }
1031
1032 const fn is_legacy(&self) -> bool {
1034 let self_parts = self.into_parts();
1035
1036 use konst::primitive::cmp::cmp_u8;
1037
1038 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
1039 }
1040
1041 pub fn default_room_version(&self) -> RoomVersionId {
1043 match self {
1044 MatrixVersion::V1_0
1046 | MatrixVersion::V1_1
1048 | MatrixVersion::V1_2 => RoomVersionId::V6,
1050 MatrixVersion::V1_3
1052 | MatrixVersion::V1_4
1054 | MatrixVersion::V1_5 => RoomVersionId::V9,
1056 MatrixVersion::V1_6
1058 | MatrixVersion::V1_7
1060 | MatrixVersion::V1_8
1062 | MatrixVersion::V1_9
1064 | MatrixVersion::V1_10
1066 | MatrixVersion::V1_11
1068 | MatrixVersion::V1_12
1070 | MatrixVersion::V1_13 => RoomVersionId::V10,
1072 | MatrixVersion::V1_14
1074 | MatrixVersion::V1_15 => RoomVersionId::V11,
1076 MatrixVersion::V1_16 => RoomVersionId::V12,
1078 }
1079 }
1080}
1081
1082#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1084#[allow(clippy::exhaustive_enums)]
1085pub enum StablePathSelector {
1086 Feature(&'static str),
1088
1089 Version(MatrixVersion),
1091
1092 FeatureAndVersion {
1094 feature: &'static str,
1096 version: MatrixVersion,
1098 },
1099}
1100
1101impl StablePathSelector {
1102 pub const fn feature(self) -> Option<&'static str> {
1104 match self {
1105 Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
1106 _ => None,
1107 }
1108 }
1109
1110 pub const fn version(self) -> Option<MatrixVersion> {
1112 match self {
1113 Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
1114 _ => None,
1115 }
1116 }
1117}
1118
1119impl From<MatrixVersion> for StablePathSelector {
1120 fn from(value: MatrixVersion) -> Self {
1121 Self::Version(value)
1122 }
1123}
1124
1125#[derive(Debug, Clone)]
1127#[allow(clippy::exhaustive_structs)]
1128pub struct SupportedVersions {
1129 pub versions: BTreeSet<MatrixVersion>,
1133
1134 pub features: BTreeSet<FeatureFlag>,
1139}
1140
1141impl SupportedVersions {
1142 pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
1147 Self {
1148 versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
1149 features: unstable_features
1150 .iter()
1151 .filter(|(_, enabled)| **enabled)
1152 .map(|(feature, _)| feature.as_str().into())
1153 .collect(),
1154 }
1155 }
1156}
1157
1158#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
1164#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, Hash, PartialOrdAsRefStr, OrdAsRefStr)]
1165#[non_exhaustive]
1166pub enum FeatureFlag {
1167 #[ruma_enum(rename = "fi.mau.msc2246")]
1173 Msc2246,
1174
1175 #[ruma_enum(rename = "org.matrix.msc2432")]
1181 Msc2432,
1182
1183 #[ruma_enum(rename = "fi.mau.msc2659")]
1189 Msc2659,
1190
1191 #[ruma_enum(rename = "fi.mau.msc2659.stable")]
1197 Msc2659Stable,
1198
1199 #[cfg(feature = "unstable-msc2666")]
1205 #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
1206 Msc2666,
1207
1208 #[ruma_enum(rename = "org.matrix.msc3030")]
1214 Msc3030,
1215
1216 #[ruma_enum(rename = "org.matrix.msc3882")]
1222 Msc3882,
1223
1224 #[ruma_enum(rename = "org.matrix.msc3916")]
1230 Msc3916,
1231
1232 #[ruma_enum(rename = "org.matrix.msc3916.stable")]
1238 Msc3916Stable,
1239
1240 #[cfg(feature = "unstable-msc4108")]
1246 #[ruma_enum(rename = "org.matrix.msc4108")]
1247 Msc4108,
1248
1249 #[cfg(feature = "unstable-msc4140")]
1255 #[ruma_enum(rename = "org.matrix.msc4140")]
1256 Msc4140,
1257
1258 #[cfg(feature = "unstable-msc4186")]
1264 #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
1265 Msc4186,
1266
1267 #[doc(hidden)]
1268 _Custom(PrivOwnedStr),
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use std::collections::{BTreeMap, BTreeSet};
1274
1275 use assert_matches2::assert_matches;
1276 use http::Method;
1277
1278 use super::{
1279 AuthScheme,
1280 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
1281 Metadata, StablePathSelector, SupportedVersions, VersionHistory,
1282 };
1283 use crate::api::error::IntoHttpError;
1284
1285 fn stable_only_metadata(
1286 stable_paths: &'static [(StablePathSelector, &'static str)],
1287 ) -> Metadata {
1288 Metadata {
1289 method: Method::GET,
1290 rate_limited: false,
1291 authentication: AuthScheme::None,
1292 history: VersionHistory {
1293 unstable_paths: &[],
1294 stable_paths,
1295 deprecated: None,
1296 removed: None,
1297 },
1298 }
1299 }
1300
1301 fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
1302 SupportedVersions {
1303 versions: versions.iter().copied().collect(),
1304 features: BTreeSet::new(),
1305 }
1306 }
1307
1308 #[test]
1311 fn make_simple_endpoint_url() {
1312 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s")]);
1313 let url = meta
1314 .make_endpoint_url(&version_only_supported(&[V1_0]), "https://example.org", &[], "")
1315 .unwrap();
1316 assert_eq!(url, "https://example.org/s");
1317 }
1318
1319 #[test]
1320 fn make_endpoint_url_with_path_args() {
1321 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1322 let url = meta
1323 .make_endpoint_url(
1324 &version_only_supported(&[V1_0]),
1325 "https://example.org",
1326 &[&"123"],
1327 "",
1328 )
1329 .unwrap();
1330 assert_eq!(url, "https://example.org/s/123");
1331 }
1332
1333 #[test]
1334 fn make_endpoint_url_with_path_args_with_dash() {
1335 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1336 let url = meta
1337 .make_endpoint_url(
1338 &version_only_supported(&[V1_0]),
1339 "https://example.org",
1340 &[&"my-path"],
1341 "",
1342 )
1343 .unwrap();
1344 assert_eq!(url, "https://example.org/s/my-path");
1345 }
1346
1347 #[test]
1348 fn make_endpoint_url_with_path_args_with_reserved_char() {
1349 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1350 let url = meta
1351 .make_endpoint_url(
1352 &version_only_supported(&[V1_0]),
1353 "https://example.org",
1354 &[&"#path"],
1355 "",
1356 )
1357 .unwrap();
1358 assert_eq!(url, "https://example.org/s/%23path");
1359 }
1360
1361 #[test]
1362 fn make_endpoint_url_with_query() {
1363 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/")]);
1364 let url = meta
1365 .make_endpoint_url(
1366 &version_only_supported(&[V1_0]),
1367 "https://example.org",
1368 &[],
1369 "foo=bar",
1370 )
1371 .unwrap();
1372 assert_eq!(url, "https://example.org/s/?foo=bar");
1373 }
1374
1375 #[test]
1376 #[should_panic]
1377 fn make_endpoint_url_wrong_num_path_args() {
1378 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1379 _ = meta.make_endpoint_url(
1380 &version_only_supported(&[V1_0]),
1381 "https://example.org",
1382 &[],
1383 "",
1384 );
1385 }
1386
1387 const EMPTY: VersionHistory =
1388 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1389
1390 #[test]
1391 fn select_version() {
1392 let version_supported = version_only_supported(&[V1_0, V1_1]);
1393 let superset_supported = version_only_supported(&[V1_1]);
1394
1395 let hist =
1397 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
1398 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1399 assert!(hist.is_supported(&version_supported));
1400 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1401 assert!(hist.is_supported(&superset_supported));
1402
1403 let hist = VersionHistory {
1405 stable_paths: &[(
1406 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
1407 "/s",
1408 )],
1409 ..EMPTY
1410 };
1411 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1412 assert!(hist.is_supported(&version_supported));
1413 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1414 assert!(hist.is_supported(&superset_supported));
1415
1416 let hist = VersionHistory {
1418 stable_paths: &[
1419 (StablePathSelector::Version(V1_0), "/s_v1"),
1420 (StablePathSelector::Version(V1_1), "/s_v2"),
1421 ],
1422 ..EMPTY
1423 };
1424 assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
1425 assert!(hist.is_supported(&version_supported));
1426
1427 let unstable_supported = SupportedVersions {
1429 versions: [V1_0].into(),
1430 features: ["org.boo.unstable".into()].into(),
1431 };
1432 let hist = VersionHistory {
1433 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1434 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
1435 ..EMPTY
1436 };
1437 assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
1438 assert!(hist.is_supported(&unstable_supported));
1439 }
1440
1441 #[test]
1442 fn select_stable_feature() {
1443 let supported = SupportedVersions {
1444 versions: [V1_1].into(),
1445 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
1446 };
1447
1448 let hist = VersionHistory {
1450 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1451 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
1452 ..EMPTY
1453 };
1454 assert_matches!(hist.select_path(&supported), Ok("/s"));
1455 assert!(hist.is_supported(&supported));
1456
1457 let hist = VersionHistory {
1459 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1460 stable_paths: &[(
1461 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1462 "/s",
1463 )],
1464 ..EMPTY
1465 };
1466 assert_matches!(hist.select_path(&supported), Ok("/s"));
1467 assert!(hist.is_supported(&supported));
1468 }
1469
1470 #[test]
1471 fn select_unstable_feature() {
1472 let supported = SupportedVersions {
1473 versions: [V1_1].into(),
1474 features: ["org.boo.unstable".into()].into(),
1475 };
1476
1477 let hist = VersionHistory {
1478 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1479 stable_paths: &[(
1480 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1481 "/s",
1482 )],
1483 ..EMPTY
1484 };
1485 assert_matches!(hist.select_path(&supported), Ok("/u"));
1486 assert!(hist.is_supported(&supported));
1487 }
1488
1489 #[test]
1490 fn select_unstable_fallback() {
1491 let supported = version_only_supported(&[V1_0]);
1492 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
1493 assert_matches!(hist.select_path(&supported), Ok("/u"));
1494 assert!(!hist.is_supported(&supported));
1495 }
1496
1497 #[test]
1498 fn select_r0() {
1499 let supported = version_only_supported(&[V1_0]);
1500 let hist =
1501 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
1502 assert_matches!(hist.select_path(&supported), Ok("/r"));
1503 assert!(hist.is_supported(&supported));
1504 }
1505
1506 #[test]
1507 fn select_removed_err() {
1508 let supported = version_only_supported(&[V1_3]);
1509 let hist = VersionHistory {
1510 stable_paths: &[
1511 (StablePathSelector::Version(V1_0), "/r"),
1512 (StablePathSelector::Version(V1_1), "/s"),
1513 ],
1514 unstable_paths: &[(None, "/u")],
1515 deprecated: Some(V1_2),
1516 removed: Some(V1_3),
1517 };
1518 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
1519 assert!(!hist.is_supported(&supported));
1520 }
1521
1522 #[test]
1523 fn partially_removed_but_stable() {
1524 let supported = version_only_supported(&[V1_2]);
1525 let hist = VersionHistory {
1526 stable_paths: &[
1527 (StablePathSelector::Version(V1_0), "/r"),
1528 (StablePathSelector::Version(V1_1), "/s"),
1529 ],
1530 unstable_paths: &[],
1531 deprecated: Some(V1_2),
1532 removed: Some(V1_3),
1533 };
1534 assert_matches!(hist.select_path(&supported), Ok("/s"));
1535 assert!(hist.is_supported(&supported));
1536 }
1537
1538 #[test]
1539 fn no_unstable() {
1540 let supported = version_only_supported(&[V1_0]);
1541 let hist =
1542 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
1543 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
1544 assert!(!hist.is_supported(&supported));
1545 }
1546
1547 #[test]
1548 fn version_literal() {
1549 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1550
1551 assert_eq!(LIT, V1_0);
1552 }
1553
1554 #[test]
1555 fn parse_as_str_sanity() {
1556 let version = MatrixVersion::try_from("r0.5.0").unwrap();
1557 assert_eq!(version, V1_0);
1558 assert_eq!(version.as_str(), None);
1559
1560 let version = MatrixVersion::try_from("v1.1").unwrap();
1561 assert_eq!(version, V1_1);
1562 assert_eq!(version.as_str(), Some("v1.1"));
1563 }
1564
1565 #[test]
1566 fn supported_versions_from_parts() {
1567 let empty_features = BTreeMap::new();
1568
1569 let none = &[];
1570 let none_supported = SupportedVersions::from_parts(none, &empty_features);
1571 assert_eq!(none_supported.versions, BTreeSet::new());
1572 assert_eq!(none_supported.features, BTreeSet::new());
1573
1574 let single_known = &["r0.6.0".to_owned()];
1575 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1576 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
1577 assert_eq!(single_known_supported.features, BTreeSet::new());
1578
1579 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1580 let multiple_known_supported =
1581 SupportedVersions::from_parts(multiple_known, &empty_features);
1582 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
1583 assert_eq!(multiple_known_supported.features, BTreeSet::new());
1584
1585 let single_unknown = &["v0.0".to_owned()];
1586 let single_unknown_supported =
1587 SupportedVersions::from_parts(single_unknown, &empty_features);
1588 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
1589 assert_eq!(single_unknown_supported.features, BTreeSet::new());
1590
1591 let mut features = BTreeMap::new();
1592 features.insert("org.bar.enabled_1".to_owned(), true);
1593 features.insert("org.bar.disabled".to_owned(), false);
1594 features.insert("org.bar.enabled_2".to_owned(), true);
1595
1596 let features_supported = SupportedVersions::from_parts(single_known, &features);
1597 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
1598 assert_eq!(
1599 features_supported.features,
1600 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1601 );
1602 }
1603
1604 #[test]
1605 fn supported_versions_from_parts_order() {
1606 let empty_features = BTreeMap::new();
1607
1608 let sorted = &[
1609 "r0.0.1".to_owned(),
1610 "r0.5.0".to_owned(),
1611 "r0.6.0".to_owned(),
1612 "r0.6.1".to_owned(),
1613 "v1.1".to_owned(),
1614 "v1.2".to_owned(),
1615 ];
1616 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
1617 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1618
1619 let sorted_reverse = &[
1620 "v1.2".to_owned(),
1621 "v1.1".to_owned(),
1622 "r0.6.1".to_owned(),
1623 "r0.6.0".to_owned(),
1624 "r0.5.0".to_owned(),
1625 "r0.0.1".to_owned(),
1626 ];
1627 let sorted_reverse_supported =
1628 SupportedVersions::from_parts(sorted_reverse, &empty_features);
1629 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1630
1631 let random_order = &[
1632 "v1.1".to_owned(),
1633 "r0.6.1".to_owned(),
1634 "r0.5.0".to_owned(),
1635 "r0.6.0".to_owned(),
1636 "r0.0.1".to_owned(),
1637 "v1.2".to_owned(),
1638 ];
1639 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1640 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1641 }
1642
1643 #[test]
1644 #[should_panic]
1645 fn make_endpoint_url_with_path_args_old_syntax() {
1646 let meta = stable_only_metadata(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1647 let url = meta
1648 .make_endpoint_url(
1649 &version_only_supported(&[V1_0]),
1650 "https://example.org",
1651 &[&"123"],
1652 "",
1653 )
1654 .unwrap();
1655 assert_eq!(url, "https://example.org/s/123");
1656 }
1657}