1use std::{
2 cmp::Ordering,
3 collections::{BTreeMap, BTreeSet},
4 fmt::{Display, Write},
5 str::FromStr,
6};
7
8use bytes::BufMut;
9use http::Method;
10use percent_encoding::utf8_percent_encode;
11use ruma_macros::StringEnum;
12use tracing::warn;
13
14use super::{
15 auth_scheme::AuthScheme,
16 error::{IntoHttpError, UnknownVersionError},
17};
18use crate::{
19 percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, PrivOwnedStr, RoomVersionId,
20};
21
22#[doc(hidden)]
104#[macro_export]
105macro_rules! metadata {
106 ( @for $request_type:ty, $( $field:ident: $rhs:tt ),+ $(,)? ) => {
107 #[allow(deprecated)]
108 impl $crate::api::Metadata for $request_type {
109 $( $crate::metadata!(@field $field: $rhs); )+
110 }
111 };
112
113 ( $( $field:ident: $rhs:tt ),+ $(,)? ) => {
114 $crate::metadata!{ @for Request, $( $field: $rhs),+ }
115 };
116
117 ( @field method: $method:ident ) => {
118 const METHOD: $crate::exports::http::Method = $crate::exports::http::Method::$method;
119 };
120
121 ( @field rate_limited: $rate_limited:literal ) => { const RATE_LIMITED: bool = $rate_limited; };
122
123 ( @field authentication: $scheme:ident ) => {
124 type Authentication = $crate::api::auth_scheme::$scheme;
125 };
126
127 ( @field history: {
128 $( unstable $(($unstable_feature:literal))? => $unstable_path:literal, )*
129 $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
130 $( $( $version:literal $(| stable ($stable_feature:literal))? => $rhs:tt, )+ )?
131 } ) => {
132 $crate::metadata! {
133 @history_impl
134 [ $( $unstable_path $(= $unstable_feature)? ),* ]
135 $( stable ($stable_feature_only) => $stable_feature_path, )?
136 $( $( $rhs = $version $(| stable ($stable_feature))? ),+ )?
138 }
139 };
140
141 ( @history_impl
142 [ $( $unstable_path:literal $(= $unstable_feature:literal)? ),* ]
143 $( stable ($stable_feature_only:literal) => $stable_feature_path:literal, )?
144 $(
145 $( $stable_path:literal = $version:literal $(| stable ($stable_feature:literal))? ),+
146 $(,
147 deprecated = $deprecated_version:literal
148 $(, removed = $removed_version:literal )?
149 )?
150 )?
151 ) => {
152 const HISTORY: $crate::api::VersionHistory = $crate::api::VersionHistory::new(
153 &[ $(($crate::metadata!(@optional_feature $($unstable_feature)?), $unstable_path)),* ],
154 &[
155 $((
156 $crate::metadata!(@stable_path_selector stable($stable_feature_only)),
157 $stable_feature_path
158 ),)?
159 $($((
160 $crate::metadata!(@stable_path_selector $version $(| stable($stable_feature))?),
161 $stable_path
162 )),+)?
163 ],
164 $crate::metadata!(@optional_version $($( $deprecated_version )?)?),
165 $crate::metadata!(@optional_version $($($( $removed_version )?)?)?),
166 );
167 };
168
169 ( @optional_feature ) => { None };
170 ( @optional_feature $feature:literal ) => { Some($feature) };
171 ( @stable_path_selector stable($feature:literal)) => { $crate::api::StablePathSelector::Feature($feature) };
172 ( @stable_path_selector $version:literal | stable($feature:literal)) => {
173 $crate::api::StablePathSelector::FeatureAndVersion {
174 feature: $feature,
175 version: $crate::api::MatrixVersion::from_lit(stringify!($version)),
176 }
177 };
178 ( @stable_path_selector $version:literal) => { $crate::api::StablePathSelector::Version($crate::api::MatrixVersion::from_lit(stringify!($version))) };
179 ( @optional_version ) => { None };
180 ( @optional_version $version:literal ) => { Some($crate::api::MatrixVersion::from_lit(stringify!($version))) }
181}
182
183pub trait Metadata: Sized {
185 const METHOD: Method;
187
188 const RATE_LIMITED: bool;
190
191 type Authentication: AuthScheme;
193
194 const HISTORY: VersionHistory;
196
197 fn empty_request_body<B>() -> B
202 where
203 B: Default + BufMut,
204 {
205 if Self::METHOD == Method::GET {
206 Default::default()
207 } else {
208 slice_to_buf(b"{}")
209 }
210 }
211
212 fn make_endpoint_url(
214 considering: &SupportedVersions,
215 base_url: &str,
216 path_args: &[&dyn Display],
217 query_string: &str,
218 ) -> Result<String, IntoHttpError> {
219 Self::HISTORY.make_endpoint_url(considering, base_url, path_args, query_string)
220 }
221
222 #[doc(hidden)]
226 fn _path_parameters() -> Vec<&'static str> {
227 Self::HISTORY._path_parameters()
228 }
229}
230
231#[derive(Clone, Debug, PartialEq, Eq)]
236#[allow(clippy::exhaustive_structs)]
237pub struct VersionHistory {
238 unstable_paths: &'static [(Option<&'static str>, &'static str)],
243
244 stable_paths: &'static [(StablePathSelector, &'static str)],
248
249 deprecated: Option<MatrixVersion>,
256
257 removed: Option<MatrixVersion>,
262}
263
264impl VersionHistory {
265 pub const fn new(
287 unstable_paths: &'static [(Option<&'static str>, &'static str)],
288 stable_paths: &'static [(StablePathSelector, &'static str)],
289 deprecated: Option<MatrixVersion>,
290 removed: Option<MatrixVersion>,
291 ) -> Self {
292 use konst::{iter, slice, string};
293
294 const fn check_path_is_valid(path: &'static str) {
295 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
296 match *path_b {
297 0x21..=0x7E => {},
298 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
299 }
300 });
301 }
302
303 const fn check_path_args_equal(first: &'static str, second: &'static str) {
304 let mut second_iter = string::split(second, "/").next();
305
306 iter::for_each!(first_s in string::split(first, "/") => {
307 if let Some(first_arg) = string::strip_prefix(first_s, ":") {
308 let second_next_arg: Option<&'static str> = loop {
309 let Some((second_s, second_n_iter)) = second_iter else {
310 break None;
311 };
312
313 let maybe_second_arg = string::strip_prefix(second_s, ":");
314
315 second_iter = second_n_iter.next();
316
317 if let Some(second_arg) = maybe_second_arg {
318 break Some(second_arg);
319 }
320 };
321
322 if let Some(second_next_arg) = second_next_arg {
323 if !string::eq_str(second_next_arg, first_arg) {
324 panic!("Path Arguments do not match");
325 }
326 } else {
327 panic!("Amount of Path Arguments do not match");
328 }
329 }
330 });
331
332 while let Some((second_s, second_n_iter)) = second_iter {
334 if string::starts_with(second_s, ":") {
335 panic!("Amount of Path Arguments do not match");
336 }
337 second_iter = second_n_iter.next();
338 }
339 }
340
341 let ref_path: &str = if let Some((_, s)) = unstable_paths.first() {
343 s
344 } else if let Some((_, s)) = stable_paths.first() {
345 s
346 } else {
347 panic!("No paths supplied")
348 };
349
350 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
351 check_path_is_valid(unstable_path.1);
352 check_path_args_equal(ref_path, unstable_path.1);
353 });
354
355 let mut prev_seen_version: Option<MatrixVersion> = None;
356
357 iter::for_each!(version_path in slice::iter(stable_paths) => {
358 check_path_is_valid(version_path.1);
359 check_path_args_equal(ref_path, version_path.1);
360
361 if let Some(current_version) = version_path.0.version() {
362 if let Some(prev_seen_version) = prev_seen_version {
363 let cmp_result = current_version.const_ord(&prev_seen_version);
364
365 if cmp_result.is_eq() {
366 panic!("Duplicate matrix version in stable_paths")
368 } else if cmp_result.is_lt() {
369 panic!("No ascending order in stable_paths")
371 }
372 }
373
374 prev_seen_version = Some(current_version);
375 }
376 });
377
378 if let Some(deprecated) = deprecated {
379 if let Some(prev_seen_version) = prev_seen_version {
380 let ord_result = prev_seen_version.const_ord(&deprecated);
381 if !deprecated.is_legacy() && ord_result.is_eq() {
382 panic!("deprecated version is equal to latest stable path version")
386 } else if ord_result.is_gt() {
387 panic!("deprecated version is older than latest stable path version")
389 }
390 } else {
391 panic!("Defined deprecated version while no stable path exists")
392 }
393 }
394
395 if let Some(removed) = removed {
396 if let Some(deprecated) = deprecated {
397 let ord_result = deprecated.const_ord(&removed);
398 if ord_result.is_eq() {
399 panic!("removed version is equal to deprecated version")
401 } else if ord_result.is_gt() {
402 panic!("removed version is older than deprecated version")
404 }
405 } else {
406 panic!("Defined removed version while no deprecated version exists")
407 }
408 }
409
410 VersionHistory { unstable_paths, stable_paths, deprecated, removed }
411 }
412
413 pub fn is_supported(&self, considering: &SupportedVersions) -> bool {
422 match self.versioning_decision_for(&considering.versions) {
423 VersioningDecision::Removed => false,
424 VersioningDecision::Version { .. } => true,
425 VersioningDecision::Feature => self.feature_path(&considering.features).is_some(),
426 }
427 }
428
429 fn select_path(&self, considering: &SupportedVersions) -> Result<&'static str, IntoHttpError> {
432 match self.versioning_decision_for(&considering.versions) {
433 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
434 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
435 )),
436 VersioningDecision::Version { any_deprecated, all_deprecated, any_removed } => {
437 if any_removed {
438 if all_deprecated {
439 warn!(
440 "endpoint is removed in some (and deprecated in ALL) \
441 of the following versions: {:?}",
442 considering.versions
443 );
444 } else if any_deprecated {
445 warn!(
446 "endpoint is removed (and deprecated) in some of the \
447 following versions: {:?}",
448 considering.versions
449 );
450 } else {
451 unreachable!("any_removed implies *_deprecated");
452 }
453 } else if all_deprecated {
454 warn!(
455 "endpoint is deprecated in ALL of the following versions: {:?}",
456 considering.versions
457 );
458 } else if any_deprecated {
459 warn!(
460 "endpoint is deprecated in some of the following versions: {:?}",
461 considering.versions
462 );
463 }
464
465 Ok(self
466 .version_path(&considering.versions)
467 .expect("VersioningDecision::Version implies that a version path exists"))
468 }
469 VersioningDecision::Feature => self
470 .feature_path(&considering.features)
471 .or_else(|| self.unstable())
472 .ok_or(IntoHttpError::NoUnstablePath),
473 }
474 }
475
476 fn extract_endpoint_path_segment_variable(segment: &str) -> Option<&str> {
490 if segment.starts_with(':') {
491 panic!(
492 "endpoint paths syntax has changed and segment variables must be wrapped by `{{}}`"
493 );
494 }
495
496 if let Some(var) = segment.strip_prefix('{').map(|s| {
497 s.strip_suffix('}')
498 .expect("endpoint path segment variable braces mismatch: missing ending `}`")
499 }) {
500 return Some(var);
501 }
502
503 if segment.ends_with('}') {
504 panic!("endpoint path segment variable braces mismatch: missing starting `{{`");
505 }
506
507 None
508 }
509
510 #[doc(hidden)]
514 pub fn _path_parameters(&self) -> Vec<&'static str> {
515 let path = self.all_paths().next().unwrap();
516 path.split('/').filter_map(Self::extract_endpoint_path_segment_variable).collect()
517 }
518
519 pub fn make_endpoint_url(
521 &self,
522 considering: &SupportedVersions,
523 base_url: &str,
524 path_args: &[&dyn Display],
525 query_string: &str,
526 ) -> Result<String, IntoHttpError> {
527 let path_with_placeholders = self.select_path(considering)?;
528
529 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
530 let mut segments = path_with_placeholders.split('/');
531 let mut path_args = path_args.iter();
532
533 let first_segment = segments.next().expect("split iterator is never empty");
534 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
535
536 for segment in segments {
537 if Self::extract_endpoint_path_segment_variable(segment).is_some() {
538 let arg = path_args
539 .next()
540 .expect("number of placeholders must match number of arguments")
541 .to_string();
542 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
543
544 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
545 } else {
546 res.reserve(segment.len() + 1);
547 res.push('/');
548 res.push_str(segment);
549 }
550 }
551
552 if !query_string.is_empty() {
553 res.push('?');
554 res.push_str(query_string);
555 }
556
557 Ok(res)
558 }
559
560 pub fn versioning_decision_for(
572 &self,
573 versions: &BTreeSet<MatrixVersion>,
574 ) -> VersioningDecision {
575 let is_superset_any =
576 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
577 let is_superset_all =
578 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
579
580 if self.removed.is_some_and(is_superset_all) {
582 return VersioningDecision::Removed;
583 }
584
585 if self.added_in().is_some_and(is_superset_any) {
587 let all_deprecated = self.deprecated.is_some_and(is_superset_all);
588
589 return VersioningDecision::Version {
590 any_deprecated: all_deprecated || self.deprecated.is_some_and(is_superset_any),
591 all_deprecated,
592 any_removed: self.removed.is_some_and(is_superset_any),
593 };
594 }
595
596 VersioningDecision::Feature
597 }
598
599 pub fn added_in(&self) -> Option<MatrixVersion> {
603 self.stable_paths.iter().find_map(|(v, _)| v.version())
604 }
605
606 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
608 self.deprecated
609 }
610
611 pub fn removed_in(&self) -> Option<MatrixVersion> {
613 self.removed
614 }
615
616 pub fn unstable(&self) -> Option<&'static str> {
618 self.unstable_paths.last().map(|(_, path)| *path)
619 }
620
621 pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
623 self.unstable_paths().map(|(_, path)| path).chain(self.stable_paths().map(|(_, path)| path))
624 }
625
626 pub fn unstable_paths(&self) -> impl Iterator<Item = (Option<&'static str>, &'static str)> {
628 self.unstable_paths.iter().copied()
629 }
630
631 pub fn stable_paths(&self) -> impl Iterator<Item = (StablePathSelector, &'static str)> {
633 self.stable_paths.iter().copied()
634 }
635
636 pub fn version_path(&self, versions: &BTreeSet<MatrixVersion>) -> Option<&'static str> {
648 let version_paths = self
649 .stable_paths
650 .iter()
651 .filter_map(|(selector, path)| selector.version().map(|version| (version, path)));
652
653 for (ver, path) in version_paths.rev() {
655 if versions.iter().any(|v| v.is_superset_of(ver)) {
657 return Some(path);
658 }
659 }
660
661 None
662 }
663
664 pub fn feature_path(&self, supported_features: &BTreeSet<FeatureFlag>) -> Option<&'static str> {
666 let unstable_feature_paths = self
667 .unstable_paths
668 .iter()
669 .filter_map(|(feature, path)| feature.map(|feature| (feature, path)));
670 let stable_feature_paths = self
671 .stable_paths
672 .iter()
673 .filter_map(|(selector, path)| selector.feature().map(|feature| (feature, path)));
674
675 for (feature, path) in unstable_feature_paths.chain(stable_feature_paths).rev() {
677 if supported_features.iter().any(|supported| supported.as_str() == feature) {
679 return Some(path);
680 }
681 }
682
683 None
684 }
685}
686
687#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
689#[allow(clippy::exhaustive_enums)]
690pub enum VersioningDecision {
691 Feature,
693
694 Version {
696 any_deprecated: bool,
698
699 all_deprecated: bool,
701
702 any_removed: bool,
704 },
705
706 Removed,
708}
709
710#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
731#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
732pub enum MatrixVersion {
733 V1_0,
745
746 V1_1,
750
751 V1_2,
755
756 V1_3,
760
761 V1_4,
765
766 V1_5,
770
771 V1_6,
775
776 V1_7,
780
781 V1_8,
785
786 V1_9,
790
791 V1_10,
795
796 V1_11,
800
801 V1_12,
805
806 V1_13,
810
811 V1_14,
815
816 V1_15,
820
821 V1_16,
825}
826
827impl TryFrom<&str> for MatrixVersion {
828 type Error = UnknownVersionError;
829
830 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
831 use MatrixVersion::*;
832
833 Ok(match value {
834 "r0.2.0" | "r0.2.1" | "r0.3.0" |
837 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
839 "v1.1" => V1_1,
840 "v1.2" => V1_2,
841 "v1.3" => V1_3,
842 "v1.4" => V1_4,
843 "v1.5" => V1_5,
844 "v1.6" => V1_6,
845 "v1.7" => V1_7,
846 "v1.8" => V1_8,
847 "v1.9" => V1_9,
848 "v1.10" => V1_10,
849 "v1.11" => V1_11,
850 "v1.12" => V1_12,
851 "v1.13" => V1_13,
852 "v1.14" => V1_14,
853 "v1.15" => V1_15,
854 "v1.16" => V1_16,
855 _ => return Err(UnknownVersionError),
856 })
857 }
858}
859
860impl FromStr for MatrixVersion {
861 type Err = UnknownVersionError;
862
863 fn from_str(s: &str) -> Result<Self, Self::Err> {
864 Self::try_from(s)
865 }
866}
867
868impl MatrixVersion {
869 pub fn is_superset_of(self, other: Self) -> bool {
879 self >= other
880 }
881
882 pub const fn as_str(self) -> Option<&'static str> {
889 let string = match self {
890 MatrixVersion::V1_0 => return None,
891 MatrixVersion::V1_1 => "v1.1",
892 MatrixVersion::V1_2 => "v1.2",
893 MatrixVersion::V1_3 => "v1.3",
894 MatrixVersion::V1_4 => "v1.4",
895 MatrixVersion::V1_5 => "v1.5",
896 MatrixVersion::V1_6 => "v1.6",
897 MatrixVersion::V1_7 => "v1.7",
898 MatrixVersion::V1_8 => "v1.8",
899 MatrixVersion::V1_9 => "v1.9",
900 MatrixVersion::V1_10 => "v1.10",
901 MatrixVersion::V1_11 => "v1.11",
902 MatrixVersion::V1_12 => "v1.12",
903 MatrixVersion::V1_13 => "v1.13",
904 MatrixVersion::V1_14 => "v1.14",
905 MatrixVersion::V1_15 => "v1.15",
906 MatrixVersion::V1_16 => "v1.16",
907 };
908
909 Some(string)
910 }
911
912 const fn into_parts(self) -> (u8, u8) {
914 match self {
915 MatrixVersion::V1_0 => (1, 0),
916 MatrixVersion::V1_1 => (1, 1),
917 MatrixVersion::V1_2 => (1, 2),
918 MatrixVersion::V1_3 => (1, 3),
919 MatrixVersion::V1_4 => (1, 4),
920 MatrixVersion::V1_5 => (1, 5),
921 MatrixVersion::V1_6 => (1, 6),
922 MatrixVersion::V1_7 => (1, 7),
923 MatrixVersion::V1_8 => (1, 8),
924 MatrixVersion::V1_9 => (1, 9),
925 MatrixVersion::V1_10 => (1, 10),
926 MatrixVersion::V1_11 => (1, 11),
927 MatrixVersion::V1_12 => (1, 12),
928 MatrixVersion::V1_13 => (1, 13),
929 MatrixVersion::V1_14 => (1, 14),
930 MatrixVersion::V1_15 => (1, 15),
931 MatrixVersion::V1_16 => (1, 16),
932 }
933 }
934
935 const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
937 match (major, minor) {
938 (1, 0) => Ok(MatrixVersion::V1_0),
939 (1, 1) => Ok(MatrixVersion::V1_1),
940 (1, 2) => Ok(MatrixVersion::V1_2),
941 (1, 3) => Ok(MatrixVersion::V1_3),
942 (1, 4) => Ok(MatrixVersion::V1_4),
943 (1, 5) => Ok(MatrixVersion::V1_5),
944 (1, 6) => Ok(MatrixVersion::V1_6),
945 (1, 7) => Ok(MatrixVersion::V1_7),
946 (1, 8) => Ok(MatrixVersion::V1_8),
947 (1, 9) => Ok(MatrixVersion::V1_9),
948 (1, 10) => Ok(MatrixVersion::V1_10),
949 (1, 11) => Ok(MatrixVersion::V1_11),
950 (1, 12) => Ok(MatrixVersion::V1_12),
951 (1, 13) => Ok(MatrixVersion::V1_13),
952 (1, 14) => Ok(MatrixVersion::V1_14),
953 (1, 15) => Ok(MatrixVersion::V1_15),
954 (1, 16) => Ok(MatrixVersion::V1_16),
955 _ => Err(UnknownVersionError),
956 }
957 }
958
959 #[doc(hidden)]
963 pub const fn from_lit(lit: &'static str) -> Self {
964 use konst::{option, primitive::parse_u8, result, string};
965
966 let major: u8;
967 let minor: u8;
968
969 let mut lit_iter = string::split(lit, ".").next();
970
971 {
972 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
975 "major version is not a valid number"
976 ));
977
978 lit_iter = checked_split.next();
979 }
980
981 match lit_iter {
982 Some((checked_second, checked_split)) => {
983 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
984 "minor version is not a valid number"
985 ));
986
987 lit_iter = checked_split.next();
988 }
989 None => panic!("could not find dot to denote second number"),
990 }
991
992 if lit_iter.is_some() {
993 panic!("version literal contains more than one dot")
994 }
995
996 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
997 "not a valid version literal"
998 ))
999 }
1000
1001 const fn const_ord(&self, other: &Self) -> Ordering {
1003 let self_parts = self.into_parts();
1004 let other_parts = other.into_parts();
1005
1006 use konst::primitive::cmp::cmp_u8;
1007
1008 let major_ord = cmp_u8(self_parts.0, other_parts.0);
1009 if major_ord.is_ne() {
1010 major_ord
1011 } else {
1012 cmp_u8(self_parts.1, other_parts.1)
1013 }
1014 }
1015
1016 const fn is_legacy(&self) -> bool {
1018 let self_parts = self.into_parts();
1019
1020 use konst::primitive::cmp::cmp_u8;
1021
1022 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
1023 }
1024
1025 pub fn default_room_version(&self) -> RoomVersionId {
1027 match self {
1028 MatrixVersion::V1_0
1030 | MatrixVersion::V1_1
1032 | MatrixVersion::V1_2 => RoomVersionId::V6,
1034 MatrixVersion::V1_3
1036 | MatrixVersion::V1_4
1038 | MatrixVersion::V1_5 => RoomVersionId::V9,
1040 MatrixVersion::V1_6
1042 | MatrixVersion::V1_7
1044 | MatrixVersion::V1_8
1046 | MatrixVersion::V1_9
1048 | MatrixVersion::V1_10
1050 | MatrixVersion::V1_11
1052 | MatrixVersion::V1_12
1054 | MatrixVersion::V1_13 => RoomVersionId::V10,
1056 | MatrixVersion::V1_14
1058 | MatrixVersion::V1_15 => RoomVersionId::V11,
1060 MatrixVersion::V1_16 => RoomVersionId::V12,
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1068#[allow(clippy::exhaustive_enums)]
1069pub enum StablePathSelector {
1070 Feature(&'static str),
1072
1073 Version(MatrixVersion),
1075
1076 FeatureAndVersion {
1078 feature: &'static str,
1080 version: MatrixVersion,
1082 },
1083}
1084
1085impl StablePathSelector {
1086 pub const fn feature(self) -> Option<&'static str> {
1088 match self {
1089 Self::Feature(feature) | Self::FeatureAndVersion { feature, .. } => Some(feature),
1090 _ => None,
1091 }
1092 }
1093
1094 pub const fn version(self) -> Option<MatrixVersion> {
1096 match self {
1097 Self::Version(version) | Self::FeatureAndVersion { version, .. } => Some(version),
1098 _ => None,
1099 }
1100 }
1101}
1102
1103impl From<MatrixVersion> for StablePathSelector {
1104 fn from(value: MatrixVersion) -> Self {
1105 Self::Version(value)
1106 }
1107}
1108
1109#[derive(Debug, Clone)]
1111#[allow(clippy::exhaustive_structs)]
1112pub struct SupportedVersions {
1113 pub versions: BTreeSet<MatrixVersion>,
1117
1118 pub features: BTreeSet<FeatureFlag>,
1123}
1124
1125impl SupportedVersions {
1126 pub fn from_parts(versions: &[String], unstable_features: &BTreeMap<String, bool>) -> Self {
1131 Self {
1132 versions: versions.iter().flat_map(|s| s.parse::<MatrixVersion>()).collect(),
1133 features: unstable_features
1134 .iter()
1135 .filter(|(_, enabled)| **enabled)
1136 .map(|(feature, _)| feature.as_str().into())
1137 .collect(),
1138 }
1139 }
1140}
1141
1142#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
1148#[derive(Clone, StringEnum, Hash)]
1149#[non_exhaustive]
1150pub enum FeatureFlag {
1151 #[ruma_enum(rename = "fi.mau.msc2246")]
1157 Msc2246,
1158
1159 #[ruma_enum(rename = "org.matrix.msc2432")]
1165 Msc2432,
1166
1167 #[ruma_enum(rename = "fi.mau.msc2659")]
1173 Msc2659,
1174
1175 #[ruma_enum(rename = "fi.mau.msc2659.stable")]
1181 Msc2659Stable,
1182
1183 #[cfg(feature = "unstable-msc2666")]
1189 #[ruma_enum(rename = "uk.half-shot.msc2666.query_mutual_rooms")]
1190 Msc2666,
1191
1192 #[ruma_enum(rename = "org.matrix.msc3030")]
1198 Msc3030,
1199
1200 #[ruma_enum(rename = "org.matrix.msc3882")]
1206 Msc3882,
1207
1208 #[ruma_enum(rename = "org.matrix.msc3916")]
1214 Msc3916,
1215
1216 #[ruma_enum(rename = "org.matrix.msc3916.stable")]
1222 Msc3916Stable,
1223
1224 #[cfg(feature = "unstable-msc4108")]
1230 #[ruma_enum(rename = "org.matrix.msc4108")]
1231 Msc4108,
1232
1233 #[cfg(feature = "unstable-msc4140")]
1239 #[ruma_enum(rename = "org.matrix.msc4140")]
1240 Msc4140,
1241
1242 #[cfg(feature = "unstable-msc4186")]
1248 #[ruma_enum(rename = "org.matrix.simplified_msc3575")]
1249 Msc4186,
1250
1251 #[doc(hidden)]
1252 _Custom(PrivOwnedStr),
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use std::collections::{BTreeMap, BTreeSet};
1258
1259 use assert_matches2::assert_matches;
1260
1261 use super::{
1262 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
1263 StablePathSelector, SupportedVersions, VersionHistory,
1264 };
1265 use crate::api::error::IntoHttpError;
1266
1267 fn stable_only_history(
1268 stable_paths: &'static [(StablePathSelector, &'static str)],
1269 ) -> VersionHistory {
1270 VersionHistory { unstable_paths: &[], stable_paths, deprecated: None, removed: None }
1271 }
1272
1273 fn version_only_supported(versions: &[MatrixVersion]) -> SupportedVersions {
1274 SupportedVersions {
1275 versions: versions.iter().copied().collect(),
1276 features: BTreeSet::new(),
1277 }
1278 }
1279
1280 #[test]
1283 fn make_simple_endpoint_url() {
1284 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s")]);
1285 let url = history
1286 .make_endpoint_url(&version_only_supported(&[V1_0]), "https://example.org", &[], "")
1287 .unwrap();
1288 assert_eq!(url, "https://example.org/s");
1289 }
1290
1291 #[test]
1292 fn make_endpoint_url_with_path_args() {
1293 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1294 let url = history
1295 .make_endpoint_url(
1296 &version_only_supported(&[V1_0]),
1297 "https://example.org",
1298 &[&"123"],
1299 "",
1300 )
1301 .unwrap();
1302 assert_eq!(url, "https://example.org/s/123");
1303 }
1304
1305 #[test]
1306 fn make_endpoint_url_with_path_args_with_dash() {
1307 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1308 let url = history
1309 .make_endpoint_url(
1310 &version_only_supported(&[V1_0]),
1311 "https://example.org",
1312 &[&"my-path"],
1313 "",
1314 )
1315 .unwrap();
1316 assert_eq!(url, "https://example.org/s/my-path");
1317 }
1318
1319 #[test]
1320 fn make_endpoint_url_with_path_args_with_reserved_char() {
1321 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1322 let url = history
1323 .make_endpoint_url(
1324 &version_only_supported(&[V1_0]),
1325 "https://example.org",
1326 &[&"#path"],
1327 "",
1328 )
1329 .unwrap();
1330 assert_eq!(url, "https://example.org/s/%23path");
1331 }
1332
1333 #[test]
1334 fn make_endpoint_url_with_query() {
1335 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/")]);
1336 let url = history
1337 .make_endpoint_url(
1338 &version_only_supported(&[V1_0]),
1339 "https://example.org",
1340 &[],
1341 "foo=bar",
1342 )
1343 .unwrap();
1344 assert_eq!(url, "https://example.org/s/?foo=bar");
1345 }
1346
1347 #[test]
1348 #[should_panic]
1349 fn make_endpoint_url_wrong_num_path_args() {
1350 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/{x}")]);
1351 _ = history.make_endpoint_url(
1352 &version_only_supported(&[V1_0]),
1353 "https://example.org",
1354 &[],
1355 "",
1356 );
1357 }
1358
1359 const EMPTY: VersionHistory =
1360 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
1361
1362 #[test]
1363 fn select_version() {
1364 let version_supported = version_only_supported(&[V1_0, V1_1]);
1365 let superset_supported = version_only_supported(&[V1_1]);
1366
1367 let hist =
1369 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/s")], ..EMPTY };
1370 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1371 assert!(hist.is_supported(&version_supported));
1372 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1373 assert!(hist.is_supported(&superset_supported));
1374
1375 let hist = VersionHistory {
1377 stable_paths: &[(
1378 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_0 },
1379 "/s",
1380 )],
1381 ..EMPTY
1382 };
1383 assert_matches!(hist.select_path(&version_supported), Ok("/s"));
1384 assert!(hist.is_supported(&version_supported));
1385 assert_matches!(hist.select_path(&superset_supported), Ok("/s"));
1386 assert!(hist.is_supported(&superset_supported));
1387
1388 let hist = VersionHistory {
1390 stable_paths: &[
1391 (StablePathSelector::Version(V1_0), "/s_v1"),
1392 (StablePathSelector::Version(V1_1), "/s_v2"),
1393 ],
1394 ..EMPTY
1395 };
1396 assert_matches!(hist.select_path(&version_supported), Ok("/s_v2"));
1397 assert!(hist.is_supported(&version_supported));
1398
1399 let unstable_supported = SupportedVersions {
1401 versions: [V1_0].into(),
1402 features: ["org.boo.unstable".into()].into(),
1403 };
1404 let hist = VersionHistory {
1405 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1406 stable_paths: &[(StablePathSelector::Version(V1_0), "/s")],
1407 ..EMPTY
1408 };
1409 assert_matches!(hist.select_path(&unstable_supported), Ok("/s"));
1410 assert!(hist.is_supported(&unstable_supported));
1411 }
1412
1413 #[test]
1414 fn select_stable_feature() {
1415 let supported = SupportedVersions {
1416 versions: [V1_1].into(),
1417 features: ["org.boo.unstable".into(), "org.boo.stable".into()].into(),
1418 };
1419
1420 let hist = VersionHistory {
1422 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1423 stable_paths: &[(StablePathSelector::Feature("org.boo.stable"), "/s")],
1424 ..EMPTY
1425 };
1426 assert_matches!(hist.select_path(&supported), Ok("/s"));
1427 assert!(hist.is_supported(&supported));
1428
1429 let hist = VersionHistory {
1431 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1432 stable_paths: &[(
1433 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1434 "/s",
1435 )],
1436 ..EMPTY
1437 };
1438 assert_matches!(hist.select_path(&supported), Ok("/s"));
1439 assert!(hist.is_supported(&supported));
1440 }
1441
1442 #[test]
1443 fn select_unstable_feature() {
1444 let supported = SupportedVersions {
1445 versions: [V1_1].into(),
1446 features: ["org.boo.unstable".into()].into(),
1447 };
1448
1449 let hist = VersionHistory {
1450 unstable_paths: &[(Some("org.boo.unstable"), "/u")],
1451 stable_paths: &[(
1452 StablePathSelector::FeatureAndVersion { feature: "org.boo.stable", version: V1_3 },
1453 "/s",
1454 )],
1455 ..EMPTY
1456 };
1457 assert_matches!(hist.select_path(&supported), Ok("/u"));
1458 assert!(hist.is_supported(&supported));
1459 }
1460
1461 #[test]
1462 fn select_unstable_fallback() {
1463 let supported = version_only_supported(&[V1_0]);
1464 let hist = VersionHistory { unstable_paths: &[(None, "/u")], ..EMPTY };
1465 assert_matches!(hist.select_path(&supported), Ok("/u"));
1466 assert!(!hist.is_supported(&supported));
1467 }
1468
1469 #[test]
1470 fn select_r0() {
1471 let supported = version_only_supported(&[V1_0]);
1472 let hist =
1473 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_0), "/r")], ..EMPTY };
1474 assert_matches!(hist.select_path(&supported), Ok("/r"));
1475 assert!(hist.is_supported(&supported));
1476 }
1477
1478 #[test]
1479 fn select_removed_err() {
1480 let supported = version_only_supported(&[V1_3]);
1481 let hist = VersionHistory {
1482 stable_paths: &[
1483 (StablePathSelector::Version(V1_0), "/r"),
1484 (StablePathSelector::Version(V1_1), "/s"),
1485 ],
1486 unstable_paths: &[(None, "/u")],
1487 deprecated: Some(V1_2),
1488 removed: Some(V1_3),
1489 };
1490 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::EndpointRemoved(V1_3)));
1491 assert!(!hist.is_supported(&supported));
1492 }
1493
1494 #[test]
1495 fn partially_removed_but_stable() {
1496 let supported = version_only_supported(&[V1_2]);
1497 let hist = VersionHistory {
1498 stable_paths: &[
1499 (StablePathSelector::Version(V1_0), "/r"),
1500 (StablePathSelector::Version(V1_1), "/s"),
1501 ],
1502 unstable_paths: &[],
1503 deprecated: Some(V1_2),
1504 removed: Some(V1_3),
1505 };
1506 assert_matches!(hist.select_path(&supported), Ok("/s"));
1507 assert!(hist.is_supported(&supported));
1508 }
1509
1510 #[test]
1511 fn no_unstable() {
1512 let supported = version_only_supported(&[V1_0]);
1513 let hist =
1514 VersionHistory { stable_paths: &[(StablePathSelector::Version(V1_1), "/s")], ..EMPTY };
1515 assert_matches!(hist.select_path(&supported), Err(IntoHttpError::NoUnstablePath));
1516 assert!(!hist.is_supported(&supported));
1517 }
1518
1519 #[test]
1520 fn version_literal() {
1521 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
1522
1523 assert_eq!(LIT, V1_0);
1524 }
1525
1526 #[test]
1527 fn parse_as_str_sanity() {
1528 let version = MatrixVersion::try_from("r0.5.0").unwrap();
1529 assert_eq!(version, V1_0);
1530 assert_eq!(version.as_str(), None);
1531
1532 let version = MatrixVersion::try_from("v1.1").unwrap();
1533 assert_eq!(version, V1_1);
1534 assert_eq!(version.as_str(), Some("v1.1"));
1535 }
1536
1537 #[test]
1538 fn supported_versions_from_parts() {
1539 let empty_features = BTreeMap::new();
1540
1541 let none = &[];
1542 let none_supported = SupportedVersions::from_parts(none, &empty_features);
1543 assert_eq!(none_supported.versions, BTreeSet::new());
1544 assert_eq!(none_supported.features, BTreeSet::new());
1545
1546 let single_known = &["r0.6.0".to_owned()];
1547 let single_known_supported = SupportedVersions::from_parts(single_known, &empty_features);
1548 assert_eq!(single_known_supported.versions, BTreeSet::from([V1_0]));
1549 assert_eq!(single_known_supported.features, BTreeSet::new());
1550
1551 let multiple_known = &["v1.1".to_owned(), "r0.6.0".to_owned(), "r0.6.1".to_owned()];
1552 let multiple_known_supported =
1553 SupportedVersions::from_parts(multiple_known, &empty_features);
1554 assert_eq!(multiple_known_supported.versions, BTreeSet::from([V1_0, V1_1]));
1555 assert_eq!(multiple_known_supported.features, BTreeSet::new());
1556
1557 let single_unknown = &["v0.0".to_owned()];
1558 let single_unknown_supported =
1559 SupportedVersions::from_parts(single_unknown, &empty_features);
1560 assert_eq!(single_unknown_supported.versions, BTreeSet::new());
1561 assert_eq!(single_unknown_supported.features, BTreeSet::new());
1562
1563 let mut features = BTreeMap::new();
1564 features.insert("org.bar.enabled_1".to_owned(), true);
1565 features.insert("org.bar.disabled".to_owned(), false);
1566 features.insert("org.bar.enabled_2".to_owned(), true);
1567
1568 let features_supported = SupportedVersions::from_parts(single_known, &features);
1569 assert_eq!(features_supported.versions, BTreeSet::from([V1_0]));
1570 assert_eq!(
1571 features_supported.features,
1572 ["org.bar.enabled_1".into(), "org.bar.enabled_2".into()].into()
1573 );
1574 }
1575
1576 #[test]
1577 fn supported_versions_from_parts_order() {
1578 let empty_features = BTreeMap::new();
1579
1580 let sorted = &[
1581 "r0.0.1".to_owned(),
1582 "r0.5.0".to_owned(),
1583 "r0.6.0".to_owned(),
1584 "r0.6.1".to_owned(),
1585 "v1.1".to_owned(),
1586 "v1.2".to_owned(),
1587 ];
1588 let sorted_supported = SupportedVersions::from_parts(sorted, &empty_features);
1589 assert_eq!(sorted_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1590
1591 let sorted_reverse = &[
1592 "v1.2".to_owned(),
1593 "v1.1".to_owned(),
1594 "r0.6.1".to_owned(),
1595 "r0.6.0".to_owned(),
1596 "r0.5.0".to_owned(),
1597 "r0.0.1".to_owned(),
1598 ];
1599 let sorted_reverse_supported =
1600 SupportedVersions::from_parts(sorted_reverse, &empty_features);
1601 assert_eq!(sorted_reverse_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1602
1603 let random_order = &[
1604 "v1.1".to_owned(),
1605 "r0.6.1".to_owned(),
1606 "r0.5.0".to_owned(),
1607 "r0.6.0".to_owned(),
1608 "r0.0.1".to_owned(),
1609 "v1.2".to_owned(),
1610 ];
1611 let random_order_supported = SupportedVersions::from_parts(random_order, &empty_features);
1612 assert_eq!(random_order_supported.versions, BTreeSet::from([V1_0, V1_1, V1_2]));
1613 }
1614
1615 #[test]
1616 #[should_panic]
1617 fn make_endpoint_url_with_path_args_old_syntax() {
1618 let history = stable_only_history(&[(StablePathSelector::Version(V1_0), "/s/:x")]);
1619 let url = history
1620 .make_endpoint_url(
1621 &version_only_supported(&[V1_0]),
1622 "https://example.org",
1623 &[&"123"],
1624 "",
1625 )
1626 .unwrap();
1627 assert_eq!(url, "https://example.org/s/123");
1628 }
1629}