1use std::{
2 cmp::Ordering,
3 fmt::{Display, Write},
4 str::FromStr,
5};
6
7use bytes::BufMut;
8use http::{
9 header::{self, HeaderName, HeaderValue},
10 Method,
11};
12use percent_encoding::utf8_percent_encode;
13use tracing::warn;
14
15use super::{
16 error::{IntoHttpError, UnknownVersionError},
17 AuthScheme, SendAccessToken,
18};
19use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, RoomVersionId};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23#[allow(clippy::exhaustive_structs)]
24pub struct Metadata {
25 pub method: Method,
27
28 pub rate_limited: bool,
30
31 pub authentication: AuthScheme,
33
34 pub history: VersionHistory,
36}
37
38impl Metadata {
39 pub fn empty_request_body<B>(&self) -> B
44 where
45 B: Default + BufMut,
46 {
47 if self.method == Method::GET {
48 Default::default()
49 } else {
50 slice_to_buf(b"{}")
51 }
52 }
53
54 pub fn authorization_header(
60 &self,
61 access_token: SendAccessToken<'_>,
62 ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
63 Ok(match self.authentication {
64 AuthScheme::None => match access_token.get_not_required_for_endpoint() {
65 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
66 None => None,
67 },
68
69 AuthScheme::AccessToken => {
70 let token = access_token
71 .get_required_for_endpoint()
72 .ok_or(IntoHttpError::NeedsAuthentication)?;
73
74 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
75 }
76
77 AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
78 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
79 None => None,
80 },
81
82 AuthScheme::AppserviceToken => {
83 let token = access_token
84 .get_required_for_appservice()
85 .ok_or(IntoHttpError::NeedsAuthentication)?;
86
87 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
88 }
89
90 AuthScheme::AppserviceTokenOptional => match access_token.get_required_for_appservice()
91 {
92 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
93 None => None,
94 },
95
96 AuthScheme::ServerSignatures => None,
97 })
98 }
99
100 pub fn make_endpoint_url(
102 &self,
103 versions: &[MatrixVersion],
104 base_url: &str,
105 path_args: &[&dyn Display],
106 query_string: &str,
107 ) -> Result<String, IntoHttpError> {
108 let path_with_placeholders = self.history.select_path(versions)?;
109
110 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
111 let mut segments = path_with_placeholders.split('/');
112 let mut path_args = path_args.iter();
113
114 let first_segment = segments.next().expect("split iterator is never empty");
115 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
116
117 for segment in segments {
118 if segment.starts_with(':') {
119 let arg = path_args
120 .next()
121 .expect("number of placeholders must match number of arguments")
122 .to_string();
123 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
124
125 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
126 } else {
127 res.reserve(segment.len() + 1);
128 res.push('/');
129 res.push_str(segment);
130 }
131 }
132
133 if !query_string.is_empty() {
134 res.push('?');
135 res.push_str(query_string);
136 }
137
138 Ok(res)
139 }
140
141 #[doc(hidden)]
143 pub fn _path_parameters(&self) -> Vec<&'static str> {
144 let path = self.history.all_paths().next().unwrap();
145 path.split('/').filter_map(|segment| segment.strip_prefix(':')).collect()
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
154#[allow(clippy::exhaustive_structs)]
155pub struct VersionHistory {
156 unstable_paths: &'static [&'static str],
160
161 stable_paths: &'static [(MatrixVersion, &'static str)],
165
166 deprecated: Option<MatrixVersion>,
173
174 removed: Option<MatrixVersion>,
179}
180
181impl VersionHistory {
182 pub const fn new(
195 unstable_paths: &'static [&'static str],
196 stable_paths: &'static [(MatrixVersion, &'static str)],
197 deprecated: Option<MatrixVersion>,
198 removed: Option<MatrixVersion>,
199 ) -> Self {
200 use konst::{iter, slice, string};
201
202 const fn check_path_is_valid(path: &'static str) {
203 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
204 match *path_b {
205 0x21..=0x7E => {},
206 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
207 }
208 });
209 }
210
211 const fn check_path_args_equal(first: &'static str, second: &'static str) {
212 let mut second_iter = string::split(second, "/").next();
213
214 iter::for_each!(first_s in string::split(first, "/") => {
215 if let Some(first_arg) = string::strip_prefix(first_s, ":") {
216 let second_next_arg: Option<&'static str> = loop {
217 let (second_s, second_n_iter) = match second_iter {
218 Some(tuple) => tuple,
219 None => break None,
220 };
221
222 let maybe_second_arg = string::strip_prefix(second_s, ":");
223
224 second_iter = second_n_iter.next();
225
226 if let Some(second_arg) = maybe_second_arg {
227 break Some(second_arg);
228 }
229 };
230
231 if let Some(second_next_arg) = second_next_arg {
232 if !string::eq_str(second_next_arg, first_arg) {
233 panic!("Path Arguments do not match");
234 }
235 } else {
236 panic!("Amount of Path Arguments do not match");
237 }
238 }
239 });
240
241 while let Some((second_s, second_n_iter)) = second_iter {
243 if string::starts_with(second_s, ":") {
244 panic!("Amount of Path Arguments do not match");
245 }
246 second_iter = second_n_iter.next();
247 }
248 }
249
250 let ref_path: &str = if let Some(s) = unstable_paths.first() {
252 s
253 } else if let Some((_, s)) = stable_paths.first() {
254 s
255 } else {
256 panic!("No paths supplied")
257 };
258
259 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
260 check_path_is_valid(unstable_path);
261 check_path_args_equal(ref_path, unstable_path);
262 });
263
264 let mut prev_seen_version: Option<MatrixVersion> = None;
265
266 iter::for_each!(stable_path in slice::iter(stable_paths) => {
267 check_path_is_valid(stable_path.1);
268 check_path_args_equal(ref_path, stable_path.1);
269
270 let current_version = stable_path.0;
271
272 if let Some(prev_seen_version) = prev_seen_version {
273 let cmp_result = current_version.const_ord(&prev_seen_version);
274
275 if cmp_result.is_eq() {
276 panic!("Duplicate matrix version in stable_paths")
278 } else if cmp_result.is_lt() {
279 panic!("No ascending order in stable_paths")
281 }
282 }
283
284 prev_seen_version = Some(current_version);
285 });
286
287 if let Some(deprecated) = deprecated {
288 if let Some(prev_seen_version) = prev_seen_version {
289 let ord_result = prev_seen_version.const_ord(&deprecated);
290 if !deprecated.is_legacy() && ord_result.is_eq() {
291 panic!("deprecated version is equal to latest stable path version")
295 } else if ord_result.is_gt() {
296 panic!("deprecated version is older than latest stable path version")
298 }
299 } else {
300 panic!("Defined deprecated version while no stable path exists")
301 }
302 }
303
304 if let Some(removed) = removed {
305 if let Some(deprecated) = deprecated {
306 let ord_result = deprecated.const_ord(&removed);
307 if ord_result.is_eq() {
308 panic!("removed version is equal to deprecated version")
310 } else if ord_result.is_gt() {
311 panic!("removed version is older than deprecated version")
313 }
314 } else {
315 panic!("Defined removed version while no deprecated version exists")
316 }
317 }
318
319 VersionHistory { unstable_paths, stable_paths, deprecated, removed }
320 }
321
322 fn select_path(&self, versions: &[MatrixVersion]) -> Result<&'static str, IntoHttpError> {
324 match self.versioning_decision_for(versions) {
325 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
326 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
327 )),
328 VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => {
329 if any_removed {
330 if all_deprecated {
331 warn!(
332 "endpoint is removed in some (and deprecated in ALL) \
333 of the following versions: {versions:?}",
334 );
335 } else if any_deprecated {
336 warn!(
337 "endpoint is removed (and deprecated) in some of the \
338 following versions: {versions:?}",
339 );
340 } else {
341 unreachable!("any_removed implies *_deprecated");
342 }
343 } else if all_deprecated {
344 warn!(
345 "endpoint is deprecated in ALL of the following versions: \
346 {versions:?}",
347 );
348 } else if any_deprecated {
349 warn!(
350 "endpoint is deprecated in some of the following versions: \
351 {versions:?}",
352 );
353 }
354
355 Ok(self
356 .stable_endpoint_for(versions)
357 .expect("VersioningDecision::Stable implies that a stable path exists"))
358 }
359 VersioningDecision::Unstable => self.unstable().ok_or(IntoHttpError::NoUnstablePath),
360 }
361 }
362
363 pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision {
374 let greater_or_equal_any =
375 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
376 let greater_or_equal_all =
377 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
378
379 if self.removed.is_some_and(greater_or_equal_all) {
381 return VersioningDecision::Removed;
382 }
383
384 if self.added_in().is_some_and(greater_or_equal_any) {
386 let all_deprecated = self.deprecated.is_some_and(greater_or_equal_all);
387
388 return VersioningDecision::Stable {
389 any_deprecated: all_deprecated || self.deprecated.is_some_and(greater_or_equal_any),
390 all_deprecated,
391 any_removed: self.removed.is_some_and(greater_or_equal_any),
392 };
393 }
394
395 VersioningDecision::Unstable
396 }
397
398 pub fn added_in(&self) -> Option<MatrixVersion> {
402 self.stable_paths.first().map(|(v, _)| *v)
403 }
404
405 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
407 self.deprecated
408 }
409
410 pub fn removed_in(&self) -> Option<MatrixVersion> {
412 self.removed
413 }
414
415 pub fn unstable(&self) -> Option<&'static str> {
417 self.unstable_paths.last().copied()
418 }
419
420 pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
422 self.unstable_paths().chain(self.stable_paths().map(|(_, path)| path))
423 }
424
425 pub fn unstable_paths(&self) -> impl Iterator<Item = &'static str> {
427 self.unstable_paths.iter().copied()
428 }
429
430 pub fn stable_paths(&self) -> impl Iterator<Item = (MatrixVersion, &'static str)> {
432 self.stable_paths.iter().map(|(version, data)| (*version, *data))
433 }
434
435 pub fn stable_endpoint_for(&self, versions: &[MatrixVersion]) -> Option<&'static str> {
447 for (ver, path) in self.stable_paths.iter().rev() {
449 if versions.iter().any(|v| v.is_superset_of(*ver)) {
451 return Some(path);
452 }
453 }
454
455 None
456 }
457}
458
459#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
461#[allow(clippy::exhaustive_enums)]
462pub enum VersioningDecision {
463 Unstable,
465
466 Stable {
468 any_deprecated: bool,
470
471 all_deprecated: bool,
473
474 any_removed: bool,
476 },
477
478 Removed,
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
503#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
504pub enum MatrixVersion {
505 V1_0,
517
518 V1_1,
522
523 V1_2,
527
528 V1_3,
532
533 V1_4,
537
538 V1_5,
542
543 V1_6,
547
548 V1_7,
552
553 V1_8,
557
558 V1_9,
562
563 V1_10,
567
568 V1_11,
572
573 V1_12,
577
578 V1_13,
582}
583
584impl TryFrom<&str> for MatrixVersion {
585 type Error = UnknownVersionError;
586
587 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
588 use MatrixVersion::*;
589
590 Ok(match value {
591 "r0.2.0" | "r0.2.1" | "r0.3.0" |
594 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
596 "v1.1" => V1_1,
597 "v1.2" => V1_2,
598 "v1.3" => V1_3,
599 "v1.4" => V1_4,
600 "v1.5" => V1_5,
601 "v1.6" => V1_6,
602 "v1.7" => V1_7,
603 "v1.8" => V1_8,
604 "v1.9" => V1_9,
605 "v1.10" => V1_10,
606 "v1.11" => V1_11,
607 "v1.12" => V1_12,
608 "v1.13" => V1_13,
609 _ => return Err(UnknownVersionError),
610 })
611 }
612}
613
614impl FromStr for MatrixVersion {
615 type Err = UnknownVersionError;
616
617 fn from_str(s: &str) -> Result<Self, Self::Err> {
618 Self::try_from(s)
619 }
620}
621
622impl MatrixVersion {
623 pub fn is_superset_of(self, other: Self) -> bool {
633 self >= other
634 }
635
636 pub const fn as_str(self) -> Option<&'static str> {
643 let string = match self {
644 MatrixVersion::V1_0 => return None,
645 MatrixVersion::V1_1 => "v1.1",
646 MatrixVersion::V1_2 => "v1.2",
647 MatrixVersion::V1_3 => "v1.3",
648 MatrixVersion::V1_4 => "v1.4",
649 MatrixVersion::V1_5 => "v1.5",
650 MatrixVersion::V1_6 => "v1.6",
651 MatrixVersion::V1_7 => "v1.7",
652 MatrixVersion::V1_8 => "v1.8",
653 MatrixVersion::V1_9 => "v1.9",
654 MatrixVersion::V1_10 => "v1.10",
655 MatrixVersion::V1_11 => "v1.11",
656 MatrixVersion::V1_12 => "v1.12",
657 MatrixVersion::V1_13 => "v1.13",
658 };
659
660 Some(string)
661 }
662
663 const fn into_parts(self) -> (u8, u8) {
665 match self {
666 MatrixVersion::V1_0 => (1, 0),
667 MatrixVersion::V1_1 => (1, 1),
668 MatrixVersion::V1_2 => (1, 2),
669 MatrixVersion::V1_3 => (1, 3),
670 MatrixVersion::V1_4 => (1, 4),
671 MatrixVersion::V1_5 => (1, 5),
672 MatrixVersion::V1_6 => (1, 6),
673 MatrixVersion::V1_7 => (1, 7),
674 MatrixVersion::V1_8 => (1, 8),
675 MatrixVersion::V1_9 => (1, 9),
676 MatrixVersion::V1_10 => (1, 10),
677 MatrixVersion::V1_11 => (1, 11),
678 MatrixVersion::V1_12 => (1, 12),
679 MatrixVersion::V1_13 => (1, 13),
680 }
681 }
682
683 const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
685 match (major, minor) {
686 (1, 0) => Ok(MatrixVersion::V1_0),
687 (1, 1) => Ok(MatrixVersion::V1_1),
688 (1, 2) => Ok(MatrixVersion::V1_2),
689 (1, 3) => Ok(MatrixVersion::V1_3),
690 (1, 4) => Ok(MatrixVersion::V1_4),
691 (1, 5) => Ok(MatrixVersion::V1_5),
692 (1, 6) => Ok(MatrixVersion::V1_6),
693 (1, 7) => Ok(MatrixVersion::V1_7),
694 (1, 8) => Ok(MatrixVersion::V1_8),
695 (1, 9) => Ok(MatrixVersion::V1_9),
696 (1, 10) => Ok(MatrixVersion::V1_10),
697 (1, 11) => Ok(MatrixVersion::V1_11),
698 (1, 12) => Ok(MatrixVersion::V1_12),
699 (1, 13) => Ok(MatrixVersion::V1_13),
700 _ => Err(UnknownVersionError),
701 }
702 }
703
704 #[doc(hidden)]
708 pub const fn from_lit(lit: &'static str) -> Self {
709 use konst::{option, primitive::parse_u8, result, string};
710
711 let major: u8;
712 let minor: u8;
713
714 let mut lit_iter = string::split(lit, ".").next();
715
716 {
717 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
720 "major version is not a valid number"
721 ));
722
723 lit_iter = checked_split.next();
724 }
725
726 match lit_iter {
727 Some((checked_second, checked_split)) => {
728 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
729 "minor version is not a valid number"
730 ));
731
732 lit_iter = checked_split.next();
733 }
734 None => panic!("could not find dot to denote second number"),
735 }
736
737 if lit_iter.is_some() {
738 panic!("version literal contains more than one dot")
739 }
740
741 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
742 "not a valid version literal"
743 ))
744 }
745
746 const fn const_ord(&self, other: &Self) -> Ordering {
748 let self_parts = self.into_parts();
749 let other_parts = other.into_parts();
750
751 use konst::primitive::cmp::cmp_u8;
752
753 let major_ord = cmp_u8(self_parts.0, other_parts.0);
754 if major_ord.is_ne() {
755 major_ord
756 } else {
757 cmp_u8(self_parts.1, other_parts.1)
758 }
759 }
760
761 const fn is_legacy(&self) -> bool {
763 let self_parts = self.into_parts();
764
765 use konst::primitive::cmp::cmp_u8;
766
767 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
768 }
769
770 pub fn default_room_version(&self) -> RoomVersionId {
772 match self {
773 MatrixVersion::V1_0
775 | MatrixVersion::V1_1
777 | MatrixVersion::V1_2 => RoomVersionId::V6,
779 MatrixVersion::V1_3
781 | MatrixVersion::V1_4
783 | MatrixVersion::V1_5 => RoomVersionId::V9,
785 MatrixVersion::V1_6
787 | MatrixVersion::V1_7
789 | MatrixVersion::V1_8
791 | MatrixVersion::V1_9
793 | MatrixVersion::V1_10
795 | MatrixVersion::V1_11
797 | MatrixVersion::V1_12
799 | MatrixVersion::V1_13 => RoomVersionId::V10,
801 }
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use assert_matches2::assert_matches;
808 use http::Method;
809
810 use super::{
811 AuthScheme,
812 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
813 Metadata, VersionHistory,
814 };
815 use crate::api::error::IntoHttpError;
816
817 fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
818 Metadata {
819 method: Method::GET,
820 rate_limited: false,
821 authentication: AuthScheme::None,
822 history: VersionHistory {
823 unstable_paths: &[],
824 stable_paths,
825 deprecated: None,
826 removed: None,
827 },
828 }
829 }
830
831 #[test]
834 fn make_simple_endpoint_url() {
835 let meta = stable_only_metadata(&[(V1_0, "/s")]);
836 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
837 assert_eq!(url, "https://example.org/s");
838 }
839
840 #[test]
841 fn make_endpoint_url_with_path_args() {
842 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
843 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
844 assert_eq!(url, "https://example.org/s/123");
845 }
846
847 #[test]
848 fn make_endpoint_url_with_path_args_with_dash() {
849 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
850 let url =
851 meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
852 assert_eq!(url, "https://example.org/s/my-path");
853 }
854
855 #[test]
856 fn make_endpoint_url_with_path_args_with_reserved_char() {
857 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
858 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
859 assert_eq!(url, "https://example.org/s/%23path");
860 }
861
862 #[test]
863 fn make_endpoint_url_with_query() {
864 let meta = stable_only_metadata(&[(V1_0, "/s/")]);
865 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
866 assert_eq!(url, "https://example.org/s/?foo=bar");
867 }
868
869 #[test]
870 #[should_panic]
871 fn make_endpoint_url_wrong_num_path_args() {
872 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
873 _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
874 }
875
876 const EMPTY: VersionHistory =
877 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
878
879 #[test]
880 fn select_latest_stable() {
881 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
882 assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
883 }
884
885 #[test]
886 fn select_unstable() {
887 let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
888 assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
889 }
890
891 #[test]
892 fn select_r0() {
893 let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
894 assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
895 }
896
897 #[test]
898 fn select_removed_err() {
899 let hist = VersionHistory {
900 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
901 unstable_paths: &["/u"],
902 deprecated: Some(V1_2),
903 removed: Some(V1_3),
904 };
905 assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
906 }
907
908 #[test]
909 fn partially_removed_but_stable() {
910 let hist = VersionHistory {
911 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
912 unstable_paths: &[],
913 deprecated: Some(V1_2),
914 removed: Some(V1_3),
915 };
916 assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
917 }
918
919 #[test]
920 fn no_unstable() {
921 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
922 assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
923 }
924
925 #[test]
926 fn version_literal() {
927 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
928
929 assert_eq!(LIT, V1_0);
930 }
931
932 #[test]
933 fn parse_as_str_sanity() {
934 let version = MatrixVersion::try_from("r0.5.0").unwrap();
935 assert_eq!(version, V1_0);
936 assert_eq!(version.as_str(), None);
937
938 let version = MatrixVersion::try_from("v1.1").unwrap();
939 assert_eq!(version, V1_1);
940 assert_eq!(version.as_str(), Some("v1.1"));
941 }
942}