Merge remote-tracking branch 'upstream/main' into conduwuit-changes
This commit is contained in:
		
						commit
						042444dc1d
					
				
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @ -4,7 +4,7 @@ env: | |||||||
|   CARGO_TERM_COLOR: always |   CARGO_TERM_COLOR: always | ||||||
|   CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse |   CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse | ||||||
|   # Keep in sync with version in `rust-toolchain.toml` and `xtask/src/ci.rs` |   # Keep in sync with version in `rust-toolchain.toml` and `xtask/src/ci.rs` | ||||||
|   NIGHTLY: nightly-2024-02-14 |   NIGHTLY: nightly-2024-05-09 | ||||||
| 
 | 
 | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ ruma-appservice-api = { version = "0.10.0", path = "crates/ruma-appservice-api" | |||||||
| ruma-common = { version = "0.13.0", path = "crates/ruma-common" } | ruma-common = { version = "0.13.0", path = "crates/ruma-common" } | ||||||
| ruma-client = { version = "0.13.0", path = "crates/ruma-client" } | ruma-client = { version = "0.13.0", path = "crates/ruma-client" } | ||||||
| ruma-client-api = { version = "0.18.0", path = "crates/ruma-client-api" } | ruma-client-api = { version = "0.18.0", path = "crates/ruma-client-api" } | ||||||
| ruma-events = { version = "0.28.0", path = "crates/ruma-events" } | ruma-events = { version = "0.28.1", path = "crates/ruma-events" } | ||||||
| ruma-federation-api = { version = "0.9.0", path = "crates/ruma-federation-api" } | ruma-federation-api = { version = "0.9.0", path = "crates/ruma-federation-api" } | ||||||
| ruma-html = { version = "0.2.0", path = "crates/ruma-html" } | ruma-html = { version = "0.2.0", path = "crates/ruma-html" } | ||||||
| ruma-identifiers-validation = { version = "0.9.5", path = "crates/ruma-identifiers-validation" } | ruma-identifiers-validation = { version = "0.9.5", path = "crates/ruma-identifiers-validation" } | ||||||
| @ -35,6 +35,7 @@ serde_html_form = "0.2.0" | |||||||
| serde_json = "1.0.87" | serde_json = "1.0.87" | ||||||
| thiserror = "1.0.37" | thiserror = "1.0.37" | ||||||
| tracing = { version = "0.1.37", default-features = false, features = ["std"] } | tracing = { version = "0.1.37", default-features = false, features = ["std"] } | ||||||
|  | url = { version = "2.5.0" } | ||||||
| web-time = "1.1.0" | web-time = "1.1.0" | ||||||
| 
 | 
 | ||||||
| [profile.dev] | [profile.dev] | ||||||
|  | |||||||
| @ -1,5 +1,9 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | Improvements: | ||||||
|  | 
 | ||||||
|  | - Add support for MSC4108 OIDC sign in and E2EE set up via QR code | ||||||
|  | 
 | ||||||
| # 0.18.0 | # 0.18.0 | ||||||
| 
 | 
 | ||||||
| Bug fixes: | Bug fixes: | ||||||
|  | |||||||
| @ -50,6 +50,7 @@ unstable-msc3575 = [] | |||||||
| unstable-msc3814 = [] | unstable-msc3814 = [] | ||||||
| unstable-msc3843 = [] | unstable-msc3843 = [] | ||||||
| unstable-msc3983 = [] | unstable-msc3983 = [] | ||||||
|  | unstable-msc4108 = [] | ||||||
| unstable-msc4121 = [] | unstable-msc4121 = [] | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| @ -67,6 +68,7 @@ serde = { workspace = true } | |||||||
| serde_html_form = { workspace = true } | serde_html_form = { workspace = true } | ||||||
| serde_json = { workspace = true } | serde_json = { workspace = true } | ||||||
| thiserror = { workspace = true } | thiserror = { workspace = true } | ||||||
|  | url = { workspace = true, features = ["serde"] } | ||||||
| web-time = { workspace = true } | web-time = { workspace = true } | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
|  | |||||||
| @ -22,8 +22,7 @@ pub fn system_time_to_http_date( | |||||||
|     date_header::format(duration.as_secs(), &mut buffer) |     date_header::format(duration.as_secs(), &mut buffer) | ||||||
|         .map_err(|_| HeaderSerializationError::InvalidHttpDate)?; |         .map_err(|_| HeaderSerializationError::InvalidHttpDate)?; | ||||||
| 
 | 
 | ||||||
|     Ok(http::HeaderValue::from_bytes(&buffer) |     Ok(HeaderValue::from_bytes(&buffer).expect("date_header should produce a valid header value")) | ||||||
|         .expect("date_header should produce a valid header value")) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Convert a header value representing a HTTP date to a `SystemTime`.
 | /// Convert a header value representing a HTTP date to a `SystemTime`.
 | ||||||
|  | |||||||
| @ -35,6 +35,8 @@ pub mod read_marker; | |||||||
| pub mod receipt; | pub mod receipt; | ||||||
| pub mod redact; | pub mod redact; | ||||||
| pub mod relations; | pub mod relations; | ||||||
|  | #[cfg(feature = "unstable-msc4108")] | ||||||
|  | pub mod rendezvous; | ||||||
| pub mod room; | pub mod room; | ||||||
| pub mod search; | pub mod search; | ||||||
| pub mod server; | pub mod server; | ||||||
|  | |||||||
| @ -148,11 +148,11 @@ pub mod v3 { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             let (scope, kind, rule_id): (RuleScope, RuleKind, String) = |             let (scope, kind, rule_id): (RuleScope, RuleKind, String) = | ||||||
|                 serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::< |                 Deserialize::deserialize(serde::de::value::SeqDeserializer::< | ||||||
|                     _, |                     _, | ||||||
|                     serde::de::value::Error, |                     serde::de::value::Error, | ||||||
|                 >::new( |                 >::new( | ||||||
|                     path_args.iter().map(::std::convert::AsRef::as_ref), |                     path_args.iter().map(::std::convert::AsRef::as_ref) | ||||||
|                 ))?; |                 ))?; | ||||||
| 
 | 
 | ||||||
|             let IncomingRequestQuery { before, after } = |             let IncomingRequestQuery { before, after } = | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								crates/ruma-client-api/src/rendezvous.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								crates/ruma-client-api/src/rendezvous.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | //! Endpoints for managing rendezvous sessions.
 | ||||||
|  | 
 | ||||||
|  | pub mod create_rendezvous_session; | ||||||
| @ -0,0 +1,207 @@ | |||||||
|  | //! `POST /_matrix/client/*/rendezvous/`
 | ||||||
|  | //!
 | ||||||
|  | //! Create a rendezvous session.
 | ||||||
|  | 
 | ||||||
|  | pub mod unstable { | ||||||
|  |     //! `msc4108` ([MSC])
 | ||||||
|  |     //!
 | ||||||
|  |     //! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
 | ||||||
|  | 
 | ||||||
|  |     use http::{ | ||||||
|  |         header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, EXPIRES, LAST_MODIFIED}, | ||||||
|  |         HeaderName, | ||||||
|  |     }; | ||||||
|  |     #[cfg(feature = "client")] | ||||||
|  |     use ruma_common::api::error::FromHttpResponseError; | ||||||
|  |     use ruma_common::{ | ||||||
|  |         api::{error::HeaderDeserializationError, Metadata}, | ||||||
|  |         metadata, | ||||||
|  |     }; | ||||||
|  |     use serde::{Deserialize, Serialize}; | ||||||
|  |     use url::Url; | ||||||
|  |     use web_time::SystemTime; | ||||||
|  | 
 | ||||||
|  |     const METADATA: Metadata = metadata! { | ||||||
|  |         method: POST, | ||||||
|  |         rate_limited: true, | ||||||
|  |         authentication: None, | ||||||
|  |         history: { | ||||||
|  |             unstable => "/_matrix/client/unstable/org.matrix.msc4108/rendezvous", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /// Request type for the `POST` `rendezvous` endpoint.
 | ||||||
|  |     #[derive(Debug, Default, Clone)] | ||||||
|  |     #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  |     pub struct Request { | ||||||
|  |         /// Any data up to maximum size allowed by the server.
 | ||||||
|  |         pub content: String, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(feature = "client")] | ||||||
|  |     impl ruma_common::api::OutgoingRequest for Request { | ||||||
|  |         type EndpointError = crate::Error; | ||||||
|  |         type IncomingResponse = Response; | ||||||
|  |         const METADATA: Metadata = METADATA; | ||||||
|  | 
 | ||||||
|  |         fn try_into_http_request<T: Default + bytes::BufMut>( | ||||||
|  |             self, | ||||||
|  |             base_url: &str, | ||||||
|  |             _: ruma_common::api::SendAccessToken<'_>, | ||||||
|  |             considering_versions: &'_ [ruma_common::api::MatrixVersion], | ||||||
|  |         ) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> { | ||||||
|  |             let url = METADATA.make_endpoint_url(considering_versions, base_url, &[], "")?; | ||||||
|  |             let body = self.content.as_bytes(); | ||||||
|  |             let content_length = body.len(); | ||||||
|  | 
 | ||||||
|  |             Ok(http::Request::builder() | ||||||
|  |                 .method(METADATA.method) | ||||||
|  |                 .uri(url) | ||||||
|  |                 .header(CONTENT_TYPE, "text/plain") | ||||||
|  |                 .header(CONTENT_LENGTH, content_length) | ||||||
|  |                 .body(ruma_common::serde::slice_to_buf(body))?) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(feature = "server")] | ||||||
|  |     impl ruma_common::api::IncomingRequest for Request { | ||||||
|  |         type EndpointError = crate::Error; | ||||||
|  |         type OutgoingResponse = Response; | ||||||
|  |         const METADATA: Metadata = METADATA; | ||||||
|  | 
 | ||||||
|  |         fn try_from_http_request<B, S>( | ||||||
|  |             request: http::Request<B>, | ||||||
|  |             _path_args: &[S], | ||||||
|  |         ) -> Result<Self, ruma_common::api::error::FromHttpRequestError> | ||||||
|  |         where | ||||||
|  |             B: AsRef<[u8]>, | ||||||
|  |             S: AsRef<str>, | ||||||
|  |         { | ||||||
|  |             const EXPECTED_CONTENT_TYPE: &str = "text/plain"; | ||||||
|  | 
 | ||||||
|  |             use ruma_common::api::error::DeserializationError; | ||||||
|  | 
 | ||||||
|  |             let content_type = request | ||||||
|  |                 .headers() | ||||||
|  |                 .get(CONTENT_TYPE) | ||||||
|  |                 .ok_or(HeaderDeserializationError::MissingHeader(CONTENT_TYPE.to_string()))?; | ||||||
|  | 
 | ||||||
|  |             let content_type = content_type.to_str()?; | ||||||
|  | 
 | ||||||
|  |             if content_type != EXPECTED_CONTENT_TYPE { | ||||||
|  |                 Err(HeaderDeserializationError::InvalidHeaderValue { | ||||||
|  |                     header: CONTENT_TYPE.to_string(), | ||||||
|  |                     expected: EXPECTED_CONTENT_TYPE.to_owned(), | ||||||
|  |                     unexpected: content_type.to_owned(), | ||||||
|  |                 } | ||||||
|  |                 .into()) | ||||||
|  |             } else { | ||||||
|  |                 let body = request.into_body().as_ref().to_vec(); | ||||||
|  |                 let content = String::from_utf8(body) | ||||||
|  |                     .map_err(|e| DeserializationError::Utf8(e.utf8_error()))?; | ||||||
|  | 
 | ||||||
|  |                 Ok(Self { content }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl Request { | ||||||
|  |         /// Creates a new `Request` with the given content.
 | ||||||
|  |         pub fn new(content: String) -> Self { | ||||||
|  |             Self { content } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Response type for the `POST` `rendezvous` endpoint.
 | ||||||
|  |     #[derive(Debug, Clone)] | ||||||
|  |     #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] | ||||||
|  |     pub struct Response { | ||||||
|  |         /// The absolute URL of the rendezvous session.
 | ||||||
|  |         pub url: Url, | ||||||
|  | 
 | ||||||
|  |         /// ETag for the current payload at the rendezvous session as
 | ||||||
|  |         /// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.etag).
 | ||||||
|  |         pub etag: String, | ||||||
|  | 
 | ||||||
|  |         /// The expiry time of the rendezvous as per
 | ||||||
|  |         /// [RFC7234](https://httpwg.org/specs/rfc7234.html#header.expires).
 | ||||||
|  |         pub expires: SystemTime, | ||||||
|  | 
 | ||||||
|  |         /// The last modified date of the payload as
 | ||||||
|  |         /// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.last-modified)
 | ||||||
|  |         pub last_modified: SystemTime, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[derive(Serialize, Deserialize)] | ||||||
|  |     struct ResponseBody { | ||||||
|  |         url: Url, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(feature = "client")] | ||||||
|  |     impl ruma_common::api::IncomingResponse for Response { | ||||||
|  |         type EndpointError = crate::Error; | ||||||
|  | 
 | ||||||
|  |         fn try_from_http_response<T: AsRef<[u8]>>( | ||||||
|  |             response: http::Response<T>, | ||||||
|  |         ) -> Result<Self, FromHttpResponseError<Self::EndpointError>> { | ||||||
|  |             use ruma_common::api::EndpointError; | ||||||
|  | 
 | ||||||
|  |             if response.status().as_u16() >= 400 { | ||||||
|  |                 return Err(FromHttpResponseError::Server( | ||||||
|  |                     Self::EndpointError::from_http_response(response), | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let get_date = |header: HeaderName| -> Result<SystemTime, FromHttpResponseError<Self::EndpointError>> { | ||||||
|  |                 let date = response | ||||||
|  |                     .headers() | ||||||
|  |                     .get(&header) | ||||||
|  |                     .ok_or_else(|| HeaderDeserializationError::MissingHeader(header.to_string()))?; | ||||||
|  | 
 | ||||||
|  |                 let date = crate::http_headers::http_date_to_system_time(date)?; | ||||||
|  | 
 | ||||||
|  |                 Ok(date) | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             let etag = response | ||||||
|  |                 .headers() | ||||||
|  |                 .get(ETAG) | ||||||
|  |                 .ok_or(HeaderDeserializationError::MissingHeader(ETAG.to_string()))? | ||||||
|  |                 .to_str()? | ||||||
|  |                 .to_owned(); | ||||||
|  |             let expires = get_date(EXPIRES)?; | ||||||
|  |             let last_modified = get_date(LAST_MODIFIED)?; | ||||||
|  | 
 | ||||||
|  |             let body = response.into_body(); | ||||||
|  |             let body: ResponseBody = serde_json::from_slice(body.as_ref())?; | ||||||
|  | 
 | ||||||
|  |             Ok(Self { url: body.url, etag, expires, last_modified }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(feature = "server")] | ||||||
|  |     impl ruma_common::api::OutgoingResponse for Response { | ||||||
|  |         fn try_into_http_response<T: Default + bytes::BufMut>( | ||||||
|  |             self, | ||||||
|  |         ) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> { | ||||||
|  |             use http::header::{CACHE_CONTROL, PRAGMA}; | ||||||
|  | 
 | ||||||
|  |             let body = ResponseBody { url: self.url.clone() }; | ||||||
|  |             let body = serde_json::to_vec(&body)?; | ||||||
|  |             let body = ruma_common::serde::slice_to_buf(&body); | ||||||
|  | 
 | ||||||
|  |             let expires = crate::http_headers::system_time_to_http_date(&self.expires)?; | ||||||
|  |             let last_modified = crate::http_headers::system_time_to_http_date(&self.last_modified)?; | ||||||
|  | 
 | ||||||
|  |             Ok(http::Response::builder() | ||||||
|  |                 .status(http::StatusCode::OK) | ||||||
|  |                 .header(CONTENT_TYPE, "application/json") | ||||||
|  |                 .header(PRAGMA, "no-cache") | ||||||
|  |                 .header(CACHE_CONTROL, "no-store") | ||||||
|  |                 .header(ETAG, self.etag) | ||||||
|  |                 .header(EXPIRES, expires) | ||||||
|  |                 .header(LAST_MODIFIED, last_modified) | ||||||
|  |                 .body(body)?) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -145,7 +145,7 @@ impl<C: HttpClient> Client<C> { | |||||||
|             .send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest })) |             .send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest })) | ||||||
|             .await?; |             .await?; | ||||||
| 
 | 
 | ||||||
|         *self.0.access_token.lock().unwrap() = response.access_token.clone(); |         self.0.access_token.lock().unwrap().clone_from(&response.access_token); | ||||||
| 
 | 
 | ||||||
|         Ok(response) |         Ok(response) | ||||||
|     } |     } | ||||||
| @ -169,7 +169,7 @@ impl<C: HttpClient> Client<C> { | |||||||
|             })) |             })) | ||||||
|             .await?; |             .await?; | ||||||
| 
 | 
 | ||||||
|         *self.0.access_token.lock().unwrap() = response.access_token.clone(); |         self.0.access_token.lock().unwrap().clone_from(&response.access_token); | ||||||
| 
 | 
 | ||||||
|         Ok(response) |         Ok(response) | ||||||
|     } |     } | ||||||
| @ -221,7 +221,7 @@ impl<C: HttpClient> Client<C> { | |||||||
|                     })) |                     })) | ||||||
|                     .await?; |                     .await?; | ||||||
| 
 | 
 | ||||||
|                 since = response.next_batch.clone(); |                 since.clone_from(&response.next_batch); | ||||||
|                 yield response; |                 yield response; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -1,5 +1,10 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | Improvements: | ||||||
|  | 
 | ||||||
|  | - Add the `InvalidHeaderValue` variant to the `DeserializationError` struct, for | ||||||
|  |   cases where we receive a HTTP header with an unexpected value. | ||||||
|  | 
 | ||||||
| # 0.13.0 | # 0.13.0 | ||||||
| 
 | 
 | ||||||
| Bug fixes: | Bug fixes: | ||||||
|  | |||||||
| @ -85,7 +85,7 @@ serde_json = { workspace = true, features = ["raw_value"] } | |||||||
| thiserror = { workspace = true } | thiserror = { workspace = true } | ||||||
| time = "0.3.34" | time = "0.3.34" | ||||||
| tracing = { workspace = true, features = ["attributes"] } | tracing = { workspace = true, features = ["attributes"] } | ||||||
| url = "2.2.2" | url = { workspace = true } | ||||||
| uuid = { version = "1.0.0", optional = true, features = ["v4"] } | uuid = { version = "1.0.0", optional = true, features = ["v4"] } | ||||||
| web-time = { workspace = true } | web-time = { workspace = true } | ||||||
| wildmatch = "2.0.0" | wildmatch = "2.0.0" | ||||||
|  | |||||||
| @ -276,6 +276,20 @@ pub enum HeaderDeserializationError { | |||||||
|     /// The given required header is missing.
 |     /// The given required header is missing.
 | ||||||
|     #[error("missing header `{0}`")] |     #[error("missing header `{0}`")] | ||||||
|     MissingHeader(String), |     MissingHeader(String), | ||||||
|  | 
 | ||||||
|  |     /// A header was received with a unexpected value.
 | ||||||
|  |     #[error(
 | ||||||
|  |         "The {header} header was received with an unexpected value, \ | ||||||
|  |          expected {expected}, received {unexpected}" | ||||||
|  |     )] | ||||||
|  |     InvalidHeaderValue { | ||||||
|  |         /// The name of the header containing the invalid value.
 | ||||||
|  |         header: String, | ||||||
|  |         /// The value the header should have been set to.
 | ||||||
|  |         expected: String, | ||||||
|  |         /// The value we instead received and rejected.
 | ||||||
|  |         unexpected: String, | ||||||
|  |     }, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// An error that happens when Ruma cannot understand a Matrix version.
 | /// An error that happens when Ruma cannot understand a Matrix version.
 | ||||||
|  | |||||||
| @ -58,10 +58,7 @@ impl<'de> Visitor<'de> for RoomNetworkVisitor { | |||||||
|         while let Some((key, value)) = access.next_entry::<String, JsonValue>()? { |         while let Some((key, value)) = access.next_entry::<String, JsonValue>()? { | ||||||
|             match key.as_str() { |             match key.as_str() { | ||||||
|                 "include_all_networks" => { |                 "include_all_networks" => { | ||||||
|                     include_all_networks = match value.as_bool() { |                     include_all_networks = value.as_bool().unwrap_or(false); | ||||||
|                         Some(b) => b, |  | ||||||
|                         _ => false, |  | ||||||
|                     } |  | ||||||
|                 } |                 } | ||||||
|                 "third_party_instance_id" => { |                 "third_party_instance_id" => { | ||||||
|                     third_party_instance_id = value.as_str().map(|v| v.to_owned()); |                     third_party_instance_id = value.as_str().map(|v| v.to_owned()); | ||||||
|  | |||||||
| @ -153,7 +153,7 @@ impl<T> Raw<T> { | |||||||
|         { |         { | ||||||
|             type Value = Option<T>; |             type Value = Option<T>; | ||||||
| 
 | 
 | ||||||
|             fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> std::fmt::Result { |             fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
|                 formatter.write_str("a string") |                 formatter.write_str("a string") | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -79,7 +79,7 @@ impl IncomingRequest for Request { | |||||||
|         B: AsRef<[u8]>, |         B: AsRef<[u8]>, | ||||||
|         S: AsRef<str>, |         S: AsRef<str>, | ||||||
|     { |     { | ||||||
|         let (room_alias,) = serde::Deserialize::deserialize(serde::de::value::SeqDeserializer::< |         let (room_alias,) = Deserialize::deserialize(serde::de::value::SeqDeserializer::< | ||||||
|             _, |             _, | ||||||
|             serde::de::value::Error, |             serde::de::value::Error, | ||||||
|         >::new( |         >::new( | ||||||
|  | |||||||
| @ -1,5 +1,14 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | # 0.28.1 | ||||||
|  | 
 | ||||||
|  | Improvements: | ||||||
|  | 
 | ||||||
|  | - Implement `make_for_thread` and `make_replacement` for | ||||||
|  |   `RoomMessageEventContentWithoutRelation` | ||||||
|  | - `RoomMessageEventContent::set_mentions` is deprecated and replaced by | ||||||
|  |   `add_mentions` that should be called before `make_replacement`. | ||||||
|  | 
 | ||||||
| # 0.28.0 | # 0.28.0 | ||||||
| 
 | 
 | ||||||
| Bug fixes: | Bug fixes: | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "ruma-events" | name = "ruma-events" | ||||||
| version = "0.28.0" | version = "0.28.1" | ||||||
| description = "Serializable types for the events in the Matrix specification." | description = "Serializable types for the events in the Matrix specification." | ||||||
| homepage = "https://ruma.dev/" | homepage = "https://ruma.dev/" | ||||||
| keywords = ["matrix", "chat", "messaging", "ruma"] | keywords = ["matrix", "chat", "messaging", "ruma"] | ||||||
| @ -72,7 +72,7 @@ serde = { workspace = true } | |||||||
| serde_json = { workspace = true, features = ["raw_value"] } | serde_json = { workspace = true, features = ["raw_value"] } | ||||||
| thiserror = { workspace = true } | thiserror = { workspace = true } | ||||||
| tracing = { workspace = true, features = ["attributes"] } | tracing = { workspace = true, features = ["attributes"] } | ||||||
| url = "2.2.2" | url = { workspace = true } | ||||||
| wildmatch = "2.0.0" | wildmatch = "2.0.0" | ||||||
| 
 | 
 | ||||||
| # dev-dependencies can't be optional, so this is a regular dependency | # dev-dependencies can't be optional, so this is a regular dependency | ||||||
|  | |||||||
| @ -141,7 +141,7 @@ impl<'de> Deserialize<'de> for JoinRule { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get()) |         let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get()) | ||||||
|             .map_err(serde::de::Error::custom)? |             .map_err(Error::custom)? | ||||||
|             .join_rule |             .join_rule | ||||||
|             .ok_or_else(|| D::Error::missing_field("join_rule"))?; |             .ok_or_else(|| D::Error::missing_field("join_rule"))?; | ||||||
| 
 | 
 | ||||||
| @ -238,9 +238,8 @@ impl<'de> Deserialize<'de> for AllowRule { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get the value of `type` if present.
 |         // Get the value of `type` if present.
 | ||||||
|         let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get()) |         let rule_type = | ||||||
|             .map_err(serde::de::Error::custom)? |             serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(Error::custom)?.rule_type; | ||||||
|             .rule_type; |  | ||||||
| 
 | 
 | ||||||
|         match rule_type.as_deref() { |         match rule_type.as_deref() { | ||||||
|             Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership), |             Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership), | ||||||
|  | |||||||
| @ -218,30 +218,12 @@ impl RoomMessageEventContent { | |||||||
|     /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
 |     /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
 | ||||||
|     /// other than HTML.
 |     /// other than HTML.
 | ||||||
|     pub fn make_for_thread( |     pub fn make_for_thread( | ||||||
|         mut self, |         self, | ||||||
|         previous_message: &OriginalRoomMessageEvent, |         previous_message: &OriginalRoomMessageEvent, | ||||||
|         is_reply: ReplyWithinThread, |         is_reply: ReplyWithinThread, | ||||||
|         add_mentions: AddMentions, |         add_mentions: AddMentions, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         if is_reply == ReplyWithinThread::Yes { |         self.without_relation().make_for_thread(previous_message, is_reply, add_mentions) | ||||||
|             self = self.make_reply_to(previous_message, ForwardThread::No, add_mentions); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) = |  | ||||||
|             &previous_message.content.relates_to |  | ||||||
|         { |  | ||||||
|             event_id.clone() |  | ||||||
|         } else { |  | ||||||
|             previous_message.event_id.clone() |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         self.relates_to = Some(Relation::Thread(Thread { |  | ||||||
|             event_id: thread_root, |  | ||||||
|             in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }), |  | ||||||
|             is_falling_back: is_reply == ReplyWithinThread::No, |  | ||||||
|         })); |  | ||||||
| 
 |  | ||||||
|         self |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Turns `self` into a [replacement] (or edit) for a given message.
 |     /// Turns `self` into a [replacement] (or edit) for a given message.
 | ||||||
| @ -259,9 +241,9 @@ impl RoomMessageEventContent { | |||||||
|     /// `original_message`.
 |     /// `original_message`.
 | ||||||
|     #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] |     #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] | ||||||
|     ///
 |     ///
 | ||||||
|     /// If the message that is replaced contains [`Mentions`], they are copied into
 |     /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
 | ||||||
|     /// `m.new_content` to keep the same mentions, but not into `content` to avoid repeated
 |     /// mentions, but the ones in `content` are filtered with the ones in the
 | ||||||
|     /// notifications.
 |     /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// # Panics
 |     /// # Panics
 | ||||||
|     ///
 |     ///
 | ||||||
| @ -270,31 +252,11 @@ impl RoomMessageEventContent { | |||||||
|     /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 |     /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 | ||||||
|     #[track_caller] |     #[track_caller] | ||||||
|     pub fn make_replacement( |     pub fn make_replacement( | ||||||
|         mut self, |         self, | ||||||
|         metadata: impl Into<ReplacementMetadata>, |         metadata: impl Into<ReplacementMetadata>, | ||||||
|         replied_to_message: Option<&OriginalRoomMessageEvent>, |         replied_to_message: Option<&OriginalRoomMessageEvent>, | ||||||
|     ) -> Self { |     ) -> Self { | ||||||
|         let metadata = metadata.into(); |         self.without_relation().make_replacement(metadata, replied_to_message) | ||||||
| 
 |  | ||||||
|         // Prepare relates_to with the untouched msgtype.
 |  | ||||||
|         let relates_to = Relation::Replacement(Replacement { |  | ||||||
|             event_id: metadata.event_id, |  | ||||||
|             new_content: RoomMessageEventContentWithoutRelation { |  | ||||||
|                 msgtype: self.msgtype.clone(), |  | ||||||
|                 mentions: metadata.mentions, |  | ||||||
|             }, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         self.msgtype.make_replacement_body(); |  | ||||||
| 
 |  | ||||||
|         // Add reply fallback if needed.
 |  | ||||||
|         if let Some(original_message) = replied_to_message { |  | ||||||
|             self = self.make_reply_to(original_message, ForwardThread::No, AddMentions::No); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         self.relates_to = Some(relates_to); |  | ||||||
| 
 |  | ||||||
|         self |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Set the [mentions] of this event.
 |     /// Set the [mentions] of this event.
 | ||||||
| @ -308,6 +270,7 @@ impl RoomMessageEventContent { | |||||||
|     /// used instead.
 |     /// used instead.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
 |     /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
 | ||||||
|  |     #[deprecated = "Call add_mentions before adding the relation instead."] | ||||||
|     pub fn set_mentions(mut self, mentions: Mentions) -> Self { |     pub fn set_mentions(mut self, mentions: Mentions) -> Self { | ||||||
|         if let Some(Relation::Replacement(replacement)) = &mut self.relates_to { |         if let Some(Relation::Replacement(replacement)) = &mut self.relates_to { | ||||||
|             let old_mentions = &replacement.new_content.mentions; |             let old_mentions = &replacement.new_content.mentions; | ||||||
| @ -344,9 +307,8 @@ impl RoomMessageEventContent { | |||||||
|     /// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
 |     /// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
 | ||||||
|     /// the values of `room`.
 |     /// the values of `room`.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// This is recommended over [`Self::set_mentions()`] to avoid to overwrite any mentions set
 |     /// This should be called before methods that add a relation, like [`Self::make_reply_to()`] and
 | ||||||
|     /// automatically by another method, like [`Self::make_reply_to()`]. However, this method has no
 |     /// [`Self::make_replacement()`], for the mentions to be correctly set.
 | ||||||
|     /// special support for replacements.
 |  | ||||||
|     ///
 |     ///
 | ||||||
|     /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
 |     /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
 | ||||||
|     pub fn add_mentions(mut self, mentions: Mentions) -> Self { |     pub fn add_mentions(mut self, mentions: Mentions) -> Self { | ||||||
| @ -686,6 +648,8 @@ impl MessageType { | |||||||
|             if let Some(formatted) = formatted { |             if let Some(formatted) = formatted { | ||||||
|                 formatted.sanitize_html(mode, remove_reply_fallback); |                 formatted.sanitize_html(mode, remove_reply_fallback); | ||||||
|             } |             } | ||||||
|  |             // This is a false positive, see <https://github.com/rust-lang/rust-clippy/issues/12444>
 | ||||||
|  |             #[allow(clippy::assigning_clones)] | ||||||
|             if remove_reply_fallback == RemoveReplyFallback::Yes { |             if remove_reply_fallback == RemoveReplyFallback::Yes { | ||||||
|                 *body = remove_plain_reply_fallback(body).to_owned(); |                 *body = remove_plain_reply_fallback(body).to_owned(); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -4,10 +4,10 @@ use serde::{Deserialize, Serialize}; | |||||||
| 
 | 
 | ||||||
| use super::{ | use super::{ | ||||||
|     AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation, |     AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation, | ||||||
|     RoomMessageEventContent, |     ReplacementMetadata, ReplyWithinThread, RoomMessageEventContent, | ||||||
| }; | }; | ||||||
| use crate::{ | use crate::{ | ||||||
|     relation::{InReplyTo, Thread}, |     relation::{InReplyTo, Replacement, Thread}, | ||||||
|     room::message::{reply::OriginalEventData, FormattedBody}, |     room::message::{reply::OriginalEventData, FormattedBody}, | ||||||
|     AnySyncTimelineEvent, Mentions, |     AnySyncTimelineEvent, Mentions, | ||||||
| }; | }; | ||||||
| @ -209,6 +209,127 @@ impl RoomMessageEventContentWithoutRelation { | |||||||
|         self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions) |         self.make_reply_tweaks(original_event_id, original_thread_id, sender_for_mentions) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Turns `self` into a new message for a thread, that is optionally a reply.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
 | ||||||
|  |     /// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
 | ||||||
|  |     /// created.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
 | ||||||
|  |     /// for the main text and prepends a quoted version of `previous_message`. Also sets the
 | ||||||
|  |     /// `in_reply_to` field inside `relates_to`.
 | ||||||
|  |     #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] | ||||||
|  |     ///
 | ||||||
|  |     /// # Panics
 | ||||||
|  |     ///
 | ||||||
|  |     /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
 | ||||||
|  |     /// other than HTML.
 | ||||||
|  |     pub fn make_for_thread( | ||||||
|  |         self, | ||||||
|  |         previous_message: &OriginalRoomMessageEvent, | ||||||
|  |         is_reply: ReplyWithinThread, | ||||||
|  |         add_mentions: AddMentions, | ||||||
|  |     ) -> RoomMessageEventContent { | ||||||
|  |         let mut content = if is_reply == ReplyWithinThread::Yes { | ||||||
|  |             self.make_reply_to(previous_message, ForwardThread::No, add_mentions) | ||||||
|  |         } else { | ||||||
|  |             self.into() | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) = | ||||||
|  |             &previous_message.content.relates_to | ||||||
|  |         { | ||||||
|  |             event_id.clone() | ||||||
|  |         } else { | ||||||
|  |             previous_message.event_id.clone() | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         content.relates_to = Some(Relation::Thread(Thread { | ||||||
|  |             event_id: thread_root, | ||||||
|  |             in_reply_to: Some(InReplyTo { event_id: previous_message.event_id.clone() }), | ||||||
|  |             is_falling_back: is_reply == ReplyWithinThread::No, | ||||||
|  |         })); | ||||||
|  | 
 | ||||||
|  |         content | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Turns `self` into a [replacement] (or edit) for a given message.
 | ||||||
|  |     ///
 | ||||||
|  |     /// The first argument after `self` can be `&OriginalRoomMessageEvent` or
 | ||||||
|  |     /// `&OriginalSyncRoomMessageEvent` if you don't want to create `ReplacementMetadata` separately
 | ||||||
|  |     /// before calling this function.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
 | ||||||
|  |     /// a fallback.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If the message that is replaced is a reply to another message, the latter should also be
 | ||||||
|  |     /// provided to be able to generate a rich reply fallback that takes the `body` /
 | ||||||
|  |     /// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
 | ||||||
|  |     /// `original_message`.
 | ||||||
|  |     #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))] | ||||||
|  |     ///
 | ||||||
|  |     /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
 | ||||||
|  |     /// mentions, but the ones in `content` are filtered with the ones in the
 | ||||||
|  |     /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Panics
 | ||||||
|  |     ///
 | ||||||
|  |     /// Panics if `self` has a `formatted_body` with a format other than HTML.
 | ||||||
|  |     ///
 | ||||||
|  |     /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
 | ||||||
|  |     #[track_caller] | ||||||
|  |     pub fn make_replacement( | ||||||
|  |         mut self, | ||||||
|  |         metadata: impl Into<ReplacementMetadata>, | ||||||
|  |         replied_to_message: Option<&OriginalRoomMessageEvent>, | ||||||
|  |     ) -> RoomMessageEventContent { | ||||||
|  |         let metadata = metadata.into(); | ||||||
|  | 
 | ||||||
|  |         let mentions = self.mentions.take(); | ||||||
|  | 
 | ||||||
|  |         // Only set mentions that were not there before.
 | ||||||
|  |         if let Some(mentions) = &mentions { | ||||||
|  |             let new_mentions = metadata.mentions.map(|old_mentions| { | ||||||
|  |                 let mut new_mentions = Mentions::new(); | ||||||
|  | 
 | ||||||
|  |                 new_mentions.user_ids = mentions | ||||||
|  |                     .user_ids | ||||||
|  |                     .iter() | ||||||
|  |                     .filter(|u| !old_mentions.user_ids.contains(*u)) | ||||||
|  |                     .cloned() | ||||||
|  |                     .collect(); | ||||||
|  | 
 | ||||||
|  |                 new_mentions.room = mentions.room && !old_mentions.room; | ||||||
|  | 
 | ||||||
|  |                 new_mentions | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             self.mentions = new_mentions; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Prepare relates_to with the untouched msgtype.
 | ||||||
|  |         let relates_to = Relation::Replacement(Replacement { | ||||||
|  |             event_id: metadata.event_id, | ||||||
|  |             new_content: RoomMessageEventContentWithoutRelation { | ||||||
|  |                 msgtype: self.msgtype.clone(), | ||||||
|  |                 mentions, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         self.msgtype.make_replacement_body(); | ||||||
|  | 
 | ||||||
|  |         // Add reply fallback if needed.
 | ||||||
|  |         let mut content = if let Some(original_message) = replied_to_message { | ||||||
|  |             self.make_reply_to(original_message, ForwardThread::No, AddMentions::No) | ||||||
|  |         } else { | ||||||
|  |             self.into() | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         content.relates_to = Some(relates_to); | ||||||
|  | 
 | ||||||
|  |         content | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Add the given [mentions] to this event.
 |     /// Add the given [mentions] to this event.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
 |     /// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
 | ||||||
| @ -253,3 +374,10 @@ impl From<RoomMessageEventContent> for RoomMessageEventContentWithoutRelation { | |||||||
|         Self { msgtype, mentions } |         Self { msgtype, mentions } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | impl From<RoomMessageEventContentWithoutRelation> for RoomMessageEventContent { | ||||||
|  |     fn from(value: RoomMessageEventContentWithoutRelation) -> Self { | ||||||
|  |         let RoomMessageEventContentWithoutRelation { msgtype, mentions } = value; | ||||||
|  |         Self { msgtype, relates_to: None, mentions } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1164,6 +1164,7 @@ fn video_msgtype_deserialization() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
|  | #[allow(deprecated)] | ||||||
| fn set_mentions() { | fn set_mentions() { | ||||||
|     let mut content = RoomMessageEventContent::text_plain("you!"); |     let mut content = RoomMessageEventContent::text_plain("you!"); | ||||||
|     let mentions = content.mentions.take(); |     let mentions = content.mentions.take(); | ||||||
| @ -1176,7 +1177,42 @@ fn set_mentions() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
| fn make_replacement_set_mentions() { | fn add_mentions_then_make_replacement() { | ||||||
|  |     let alice = owned_user_id!("@alice:localhost"); | ||||||
|  |     let bob = owned_user_id!("@bob:localhost"); | ||||||
|  |     let original_message_json = json!({ | ||||||
|  |         "content": { | ||||||
|  |             "body": "Hello, World!", | ||||||
|  |             "msgtype": "m.text", | ||||||
|  |             "m.mentions": { | ||||||
|  |                 "user_ids": [alice], | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "event_id": "$143273582443PhrSn", | ||||||
|  |         "origin_server_ts": 134_829_848, | ||||||
|  |         "room_id": "!roomid:notareal.hs", | ||||||
|  |         "sender": "@user:notareal.hs", | ||||||
|  |         "type": "m.room.message", | ||||||
|  |     }); | ||||||
|  |     let original_message: OriginalSyncRoomMessageEvent = | ||||||
|  |         from_json_value(original_message_json).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let mut content = RoomMessageEventContent::text_html( | ||||||
|  |         "This is _an edited_ message.", | ||||||
|  |         "This is <em>an edited</em> message.", | ||||||
|  |     ); | ||||||
|  |     content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()])); | ||||||
|  |     content = content.make_replacement(&original_message, None); | ||||||
|  | 
 | ||||||
|  |     let mentions = content.mentions.unwrap(); | ||||||
|  |     assert_eq!(mentions.user_ids, [bob.clone()].into()); | ||||||
|  |     assert_matches!(content.relates_to, Some(Relation::Replacement(replacement))); | ||||||
|  |     let mentions = replacement.new_content.mentions.unwrap(); | ||||||
|  |     assert_eq!(mentions.user_ids, [alice, bob].into()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn make_replacement_then_add_mentions() { | ||||||
|     let alice = owned_user_id!("@alice:localhost"); |     let alice = owned_user_id!("@alice:localhost"); | ||||||
|     let bob = owned_user_id!("@bob:localhost"); |     let bob = owned_user_id!("@bob:localhost"); | ||||||
|     let original_message_json = json!({ |     let original_message_json = json!({ | ||||||
| @ -1201,19 +1237,12 @@ fn make_replacement_set_mentions() { | |||||||
|         "This is <em>an edited</em> message.", |         "This is <em>an edited</em> message.", | ||||||
|     ); |     ); | ||||||
|     content = content.make_replacement(&original_message, None); |     content = content.make_replacement(&original_message, None); | ||||||
|     let content_clone = content.clone(); |     content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()])); | ||||||
| 
 | 
 | ||||||
|     assert_matches!(content.mentions, None); |  | ||||||
|     assert_matches!(content.relates_to, Some(Relation::Replacement(replacement))); |  | ||||||
|     let mentions = replacement.new_content.mentions.unwrap(); |  | ||||||
|     assert_eq!(mentions.user_ids, [alice.clone()].into()); |  | ||||||
| 
 |  | ||||||
|     content = content_clone.set_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()])); |  | ||||||
|     let mentions = content.mentions.unwrap(); |     let mentions = content.mentions.unwrap(); | ||||||
|     assert_eq!(mentions.user_ids, [bob.clone()].into()); |  | ||||||
|     assert_matches!(content.relates_to, Some(Relation::Replacement(replacement))); |  | ||||||
|     let mentions = replacement.new_content.mentions.unwrap(); |  | ||||||
|     assert_eq!(mentions.user_ids, [alice, bob].into()); |     assert_eq!(mentions.user_ids, [alice, bob].into()); | ||||||
|  |     assert_matches!(content.relates_to, Some(Relation::Replacement(replacement))); | ||||||
|  |     assert!(replacement.new_content.mentions.is_none()); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
|  | |||||||
| @ -6,13 +6,10 @@ use once_cell::sync::Lazy; | |||||||
| use proc_macro2::Span; | use proc_macro2::Span; | ||||||
| use serde::{de::IgnoredAny, Deserialize}; | use serde::{de::IgnoredAny, Deserialize}; | ||||||
| 
 | 
 | ||||||
| mod api_metadata; |  | ||||||
| mod attribute; | mod attribute; | ||||||
| mod auth_scheme; | mod auth_scheme; | ||||||
| pub mod request; | pub mod request; | ||||||
| pub mod response; | pub mod response; | ||||||
| mod util; |  | ||||||
| mod version; |  | ||||||
| 
 | 
 | ||||||
| mod kw { | mod kw { | ||||||
|     syn::custom_keyword!(error); |     syn::custom_keyword!(error); | ||||||
|  | |||||||
| @ -1,419 +0,0 @@ | |||||||
| //! Details of the `metadata` section of the procedural macro.
 |  | ||||||
| 
 |  | ||||||
| use proc_macro2::TokenStream; |  | ||||||
| use quote::{quote, ToTokens}; |  | ||||||
| use syn::{ |  | ||||||
|     braced, |  | ||||||
|     parse::{Parse, ParseStream}, |  | ||||||
|     Ident, LitBool, LitStr, Token, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| use super::{auth_scheme::AuthScheme, util, version::MatrixVersionLiteral}; |  | ||||||
| 
 |  | ||||||
| mod kw { |  | ||||||
|     syn::custom_keyword!(metadata); |  | ||||||
|     syn::custom_keyword!(description); |  | ||||||
|     syn::custom_keyword!(method); |  | ||||||
|     syn::custom_keyword!(name); |  | ||||||
|     syn::custom_keyword!(unstable_path); |  | ||||||
|     syn::custom_keyword!(r0_path); |  | ||||||
|     syn::custom_keyword!(stable_path); |  | ||||||
|     syn::custom_keyword!(rate_limited); |  | ||||||
|     syn::custom_keyword!(authentication); |  | ||||||
|     syn::custom_keyword!(added); |  | ||||||
|     syn::custom_keyword!(deprecated); |  | ||||||
|     syn::custom_keyword!(removed); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// The result of processing the `metadata` section of the macro.
 |  | ||||||
| pub struct Metadata { |  | ||||||
|     /// The description field.
 |  | ||||||
|     pub description: LitStr, |  | ||||||
| 
 |  | ||||||
|     /// The method field.
 |  | ||||||
|     pub method: Ident, |  | ||||||
| 
 |  | ||||||
|     /// The name field.
 |  | ||||||
|     pub name: LitStr, |  | ||||||
| 
 |  | ||||||
|     /// The rate_limited field.
 |  | ||||||
|     pub rate_limited: LitBool, |  | ||||||
| 
 |  | ||||||
|     /// The authentication field.
 |  | ||||||
|     pub authentication: AuthScheme, |  | ||||||
| 
 |  | ||||||
|     /// The version history field.
 |  | ||||||
|     pub history: History, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn set_field<T: ToTokens>(field: &mut Option<T>, value: T) -> syn::Result<()> { |  | ||||||
|     match field { |  | ||||||
|         Some(existing_value) => { |  | ||||||
|             let mut error = syn::Error::new_spanned(value, "duplicate field assignment"); |  | ||||||
|             error.combine(syn::Error::new_spanned(existing_value, "first one here")); |  | ||||||
|             Err(error) |  | ||||||
|         } |  | ||||||
|         None => { |  | ||||||
|             *field = Some(value); |  | ||||||
|             Ok(()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for Metadata { |  | ||||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |  | ||||||
|         let metadata_kw: kw::metadata = input.parse()?; |  | ||||||
|         let _: Token![:] = input.parse()?; |  | ||||||
| 
 |  | ||||||
|         let field_values; |  | ||||||
|         braced!(field_values in input); |  | ||||||
| 
 |  | ||||||
|         let field_values = field_values.parse_terminated(FieldValue::parse, Token![,])?; |  | ||||||
| 
 |  | ||||||
|         let mut description = None; |  | ||||||
|         let mut method = None; |  | ||||||
|         let mut name = None; |  | ||||||
|         let mut unstable_path = None; |  | ||||||
|         let mut r0_path = None; |  | ||||||
|         let mut stable_path = None; |  | ||||||
|         let mut rate_limited = None; |  | ||||||
|         let mut authentication = None; |  | ||||||
|         let mut added = None; |  | ||||||
|         let mut deprecated = None; |  | ||||||
|         let mut removed = None; |  | ||||||
| 
 |  | ||||||
|         for field_value in field_values { |  | ||||||
|             match field_value { |  | ||||||
|                 FieldValue::Description(d) => set_field(&mut description, d)?, |  | ||||||
|                 FieldValue::Method(m) => set_field(&mut method, m)?, |  | ||||||
|                 FieldValue::Name(n) => set_field(&mut name, n)?, |  | ||||||
|                 FieldValue::UnstablePath(p) => set_field(&mut unstable_path, p)?, |  | ||||||
|                 FieldValue::R0Path(p) => set_field(&mut r0_path, p)?, |  | ||||||
|                 FieldValue::StablePath(p) => set_field(&mut stable_path, p)?, |  | ||||||
|                 FieldValue::RateLimited(rl) => set_field(&mut rate_limited, rl)?, |  | ||||||
|                 FieldValue::Authentication(a) => set_field(&mut authentication, a)?, |  | ||||||
|                 FieldValue::Added(v) => set_field(&mut added, v)?, |  | ||||||
|                 FieldValue::Deprecated(v) => set_field(&mut deprecated, v)?, |  | ||||||
|                 FieldValue::Removed(v) => set_field(&mut removed, v)?, |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let missing_field = |  | ||||||
|             |name| syn::Error::new_spanned(metadata_kw, format!("missing field `{name}`")); |  | ||||||
| 
 |  | ||||||
|         // Construct the History object.
 |  | ||||||
|         let history = { |  | ||||||
|             let stable_or_r0 = stable_path.as_ref().or(r0_path.as_ref()); |  | ||||||
| 
 |  | ||||||
|             if let Some(path) = stable_or_r0 { |  | ||||||
|                 if added.is_none() { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         path, |  | ||||||
|                         "stable path was defined, while `added` version was not defined", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if let Some(deprecated) = &deprecated { |  | ||||||
|                 if added.is_none() { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         deprecated, |  | ||||||
|                         "deprecated version is defined while added version is not defined", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Note: It is possible that Matrix will remove endpoints in a single version, while
 |  | ||||||
|             // not having a deprecation version inbetween, but that would not be allowed by their
 |  | ||||||
|             // own deprecation policy, so lets just assume  there's always a deprecation version
 |  | ||||||
|             // before a removal one.
 |  | ||||||
|             //
 |  | ||||||
|             // If Matrix does so anyways, we can just alter this.
 |  | ||||||
|             if let Some(removed) = &removed { |  | ||||||
|                 if deprecated.is_none() { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         removed, |  | ||||||
|                         "removed version is defined while deprecated version is not defined", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if let Some(added) = &added { |  | ||||||
|                 if stable_or_r0.is_none() { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         added, |  | ||||||
|                         "added version is defined, but no stable or r0 path exists", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if let Some(r0) = &r0_path { |  | ||||||
|                 let added = |  | ||||||
|                     added.as_ref().expect("we error if r0 or stable is defined without added"); |  | ||||||
| 
 |  | ||||||
|                 if added.major.get() == 1 && added.minor > 0 { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         r0, |  | ||||||
|                         "r0 defined while added version is newer than v1.0", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if stable_path.is_none() { |  | ||||||
|                     return Err(syn::Error::new_spanned(r0, "r0 defined without stable path")); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if !r0.value().contains("/r0/") { |  | ||||||
|                     return Err(syn::Error::new_spanned(r0, "r0 endpoint does not contain /r0/")); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if let Some(stable) = &stable_path { |  | ||||||
|                 if stable.value().contains("/r0/") { |  | ||||||
|                     return Err(syn::Error::new_spanned( |  | ||||||
|                         stable, |  | ||||||
|                         "stable endpoint contains /r0/ (did you make a copy-paste error?)", |  | ||||||
|                     )); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if unstable_path.is_none() && r0_path.is_none() && stable_path.is_none() { |  | ||||||
|                 return Err(syn::Error::new_spanned( |  | ||||||
|                     metadata_kw, |  | ||||||
|                     "need to define one of [r0_path, stable_path, unstable_path]", |  | ||||||
|                 )); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             History::construct(deprecated, removed, unstable_path, r0_path, stable_path.zip(added)) |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         Ok(Self { |  | ||||||
|             description: description.ok_or_else(|| missing_field("description"))?, |  | ||||||
|             method: method.ok_or_else(|| missing_field("method"))?, |  | ||||||
|             name: name.ok_or_else(|| missing_field("name"))?, |  | ||||||
|             rate_limited: rate_limited.ok_or_else(|| missing_field("rate_limited"))?, |  | ||||||
|             authentication: authentication.ok_or_else(|| missing_field("authentication"))?, |  | ||||||
|             history, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| enum Field { |  | ||||||
|     Description, |  | ||||||
|     Method, |  | ||||||
|     Name, |  | ||||||
|     UnstablePath, |  | ||||||
|     R0Path, |  | ||||||
|     StablePath, |  | ||||||
|     RateLimited, |  | ||||||
|     Authentication, |  | ||||||
|     Added, |  | ||||||
|     Deprecated, |  | ||||||
|     Removed, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for Field { |  | ||||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |  | ||||||
|         let lookahead = input.lookahead1(); |  | ||||||
| 
 |  | ||||||
|         if lookahead.peek(kw::description) { |  | ||||||
|             let _: kw::description = input.parse()?; |  | ||||||
|             Ok(Self::Description) |  | ||||||
|         } else if lookahead.peek(kw::method) { |  | ||||||
|             let _: kw::method = input.parse()?; |  | ||||||
|             Ok(Self::Method) |  | ||||||
|         } else if lookahead.peek(kw::name) { |  | ||||||
|             let _: kw::name = input.parse()?; |  | ||||||
|             Ok(Self::Name) |  | ||||||
|         } else if lookahead.peek(kw::unstable_path) { |  | ||||||
|             let _: kw::unstable_path = input.parse()?; |  | ||||||
|             Ok(Self::UnstablePath) |  | ||||||
|         } else if lookahead.peek(kw::r0_path) { |  | ||||||
|             let _: kw::r0_path = input.parse()?; |  | ||||||
|             Ok(Self::R0Path) |  | ||||||
|         } else if lookahead.peek(kw::stable_path) { |  | ||||||
|             let _: kw::stable_path = input.parse()?; |  | ||||||
|             Ok(Self::StablePath) |  | ||||||
|         } else if lookahead.peek(kw::rate_limited) { |  | ||||||
|             let _: kw::rate_limited = input.parse()?; |  | ||||||
|             Ok(Self::RateLimited) |  | ||||||
|         } else if lookahead.peek(kw::authentication) { |  | ||||||
|             let _: kw::authentication = input.parse()?; |  | ||||||
|             Ok(Self::Authentication) |  | ||||||
|         } else if lookahead.peek(kw::added) { |  | ||||||
|             let _: kw::added = input.parse()?; |  | ||||||
|             Ok(Self::Added) |  | ||||||
|         } else if lookahead.peek(kw::deprecated) { |  | ||||||
|             let _: kw::deprecated = input.parse()?; |  | ||||||
|             Ok(Self::Deprecated) |  | ||||||
|         } else if lookahead.peek(kw::removed) { |  | ||||||
|             let _: kw::removed = input.parse()?; |  | ||||||
|             Ok(Self::Removed) |  | ||||||
|         } else { |  | ||||||
|             Err(lookahead.error()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| enum FieldValue { |  | ||||||
|     Description(LitStr), |  | ||||||
|     Method(Ident), |  | ||||||
|     Name(LitStr), |  | ||||||
|     UnstablePath(EndpointPath), |  | ||||||
|     R0Path(EndpointPath), |  | ||||||
|     StablePath(EndpointPath), |  | ||||||
|     RateLimited(LitBool), |  | ||||||
|     Authentication(AuthScheme), |  | ||||||
|     Added(MatrixVersionLiteral), |  | ||||||
|     Deprecated(MatrixVersionLiteral), |  | ||||||
|     Removed(MatrixVersionLiteral), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for FieldValue { |  | ||||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |  | ||||||
|         let field: Field = input.parse()?; |  | ||||||
|         let _: Token![:] = input.parse()?; |  | ||||||
| 
 |  | ||||||
|         Ok(match field { |  | ||||||
|             Field::Description => Self::Description(input.parse()?), |  | ||||||
|             Field::Method => Self::Method(input.parse()?), |  | ||||||
|             Field::Name => Self::Name(input.parse()?), |  | ||||||
|             Field::UnstablePath => Self::UnstablePath(input.parse()?), |  | ||||||
|             Field::R0Path => Self::R0Path(input.parse()?), |  | ||||||
|             Field::StablePath => Self::StablePath(input.parse()?), |  | ||||||
|             Field::RateLimited => Self::RateLimited(input.parse()?), |  | ||||||
|             Field::Authentication => Self::Authentication(input.parse()?), |  | ||||||
|             Field::Added => Self::Added(input.parse()?), |  | ||||||
|             Field::Deprecated => Self::Deprecated(input.parse()?), |  | ||||||
|             Field::Removed => Self::Removed(input.parse()?), |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub struct History { |  | ||||||
|     pub(super) entries: Vec<HistoryEntry>, |  | ||||||
|     misc: MiscVersioning, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl History { |  | ||||||
|     // TODO(j0j0): remove after codebase conversion is complete
 |  | ||||||
|     /// Construct a History object from legacy parts.
 |  | ||||||
|     pub fn construct( |  | ||||||
|         deprecated: Option<MatrixVersionLiteral>, |  | ||||||
|         removed: Option<MatrixVersionLiteral>, |  | ||||||
|         unstable_path: Option<EndpointPath>, |  | ||||||
|         r0_path: Option<EndpointPath>, |  | ||||||
|         stable_path_and_version: Option<(EndpointPath, MatrixVersionLiteral)>, |  | ||||||
|     ) -> Self { |  | ||||||
|         // Unfortunately can't `use` associated constants
 |  | ||||||
|         const V1_0: MatrixVersionLiteral = MatrixVersionLiteral::V1_0; |  | ||||||
| 
 |  | ||||||
|         let unstable = unstable_path.map(|path| HistoryEntry::Unstable { path }); |  | ||||||
|         let r0 = r0_path.map(|path| HistoryEntry::Stable { path, version: V1_0 }); |  | ||||||
|         let stable = stable_path_and_version.map(|(path, mut version)| { |  | ||||||
|             // If added in 1.0 as r0, the new stable path must be from 1.1
 |  | ||||||
|             if r0.is_some() && version == V1_0 { |  | ||||||
|                 version = MatrixVersionLiteral::V1_1; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             HistoryEntry::Stable { path, version } |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         let misc = match (deprecated, removed) { |  | ||||||
|             (None, None) => MiscVersioning::None, |  | ||||||
|             (Some(deprecated), None) => MiscVersioning::Deprecated(deprecated), |  | ||||||
|             (Some(deprecated), Some(removed)) => MiscVersioning::Removed { deprecated, removed }, |  | ||||||
| 
 |  | ||||||
|             (None, Some(_)) => unreachable!("removed implies deprecated"), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let entries = [unstable, r0, stable].into_iter().flatten().collect(); |  | ||||||
| 
 |  | ||||||
|         History { entries, misc } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| pub enum MiscVersioning { |  | ||||||
|     None, |  | ||||||
|     Deprecated(MatrixVersionLiteral), |  | ||||||
|     Removed { deprecated: MatrixVersionLiteral, removed: MatrixVersionLiteral }, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl ToTokens for History { |  | ||||||
|     fn to_tokens(&self, tokens: &mut TokenStream) { |  | ||||||
|         fn endpointpath_to_pathdata_ts(endpoint: &EndpointPath) -> String { |  | ||||||
|             endpoint.value() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let unstable = self.entries.iter().filter_map(|e| match e { |  | ||||||
|             HistoryEntry::Unstable { path } => Some(endpointpath_to_pathdata_ts(path)), |  | ||||||
|             _ => None, |  | ||||||
|         }); |  | ||||||
|         let versioned = self.entries.iter().filter_map(|e| match e { |  | ||||||
|             HistoryEntry::Stable { path, version } => { |  | ||||||
|                 let path = endpointpath_to_pathdata_ts(path); |  | ||||||
|                 Some(quote! {( #version, #path )}) |  | ||||||
|             } |  | ||||||
|             _ => None, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         let (deprecated, removed) = match &self.misc { |  | ||||||
|             MiscVersioning::None => (None, None), |  | ||||||
|             MiscVersioning::Deprecated(deprecated) => (Some(deprecated), None), |  | ||||||
|             MiscVersioning::Removed { deprecated, removed } => (Some(deprecated), Some(removed)), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         let deprecated = util::map_option_literal(&deprecated); |  | ||||||
|         let removed = util::map_option_literal(&removed); |  | ||||||
| 
 |  | ||||||
|         tokens.extend(quote! { |  | ||||||
|             ::ruma_common::api::VersionHistory::new( |  | ||||||
|                 &[ #(#unstable),* ], |  | ||||||
|                 &[ #(#versioned),* ], |  | ||||||
|                 #deprecated, |  | ||||||
|                 #removed, |  | ||||||
|             ) |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, PartialEq)] |  | ||||||
| // Unused variants will be constructed when the macro input is updated
 |  | ||||||
| #[allow(dead_code)] |  | ||||||
| pub enum HistoryEntry { |  | ||||||
|     Unstable { path: EndpointPath }, |  | ||||||
|     Stable { version: MatrixVersionLiteral, path: EndpointPath }, |  | ||||||
|     Deprecated { version: MatrixVersionLiteral }, |  | ||||||
|     Removed { version: MatrixVersionLiteral }, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Clone, Debug, PartialEq)] |  | ||||||
| pub struct EndpointPath(LitStr); |  | ||||||
| 
 |  | ||||||
| impl EndpointPath { |  | ||||||
|     pub fn value(&self) -> String { |  | ||||||
|         self.0.value() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for EndpointPath { |  | ||||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { |  | ||||||
|         let path: LitStr = input.parse()?; |  | ||||||
| 
 |  | ||||||
|         if util::is_valid_endpoint_path(&path.value()) { |  | ||||||
|             Ok(Self(path)) |  | ||||||
|         } else { |  | ||||||
|             Err(syn::Error::new_spanned( |  | ||||||
|                 &path, |  | ||||||
|                 "path may only contain printable ASCII characters with no spaces", |  | ||||||
|             )) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl ToTokens for EndpointPath { |  | ||||||
|     fn to_tokens(&self, tokens: &mut TokenStream) { |  | ||||||
|         self.0.to_tokens(tokens); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -53,9 +53,8 @@ impl Response { | |||||||
|                     } |                     } | ||||||
|                     ResponseFieldKind::Header(header_name) => { |                     ResponseFieldKind::Header(header_name) => { | ||||||
|                         let optional_header = match &field.ty { |                         let optional_header = match &field.ty { | ||||||
|                             syn::Type::Path(syn::TypePath { |                             Type::Path(syn::TypePath { | ||||||
|                                 path: syn::Path { segments, .. }, |                                 path: syn::Path { segments, .. }, .. | ||||||
|                                 .. |  | ||||||
|                             }) if segments.last().unwrap().ident == "Option" => { |                             }) if segments.last().unwrap().ident == "Option" => { | ||||||
|                                 quote! { |                                 quote! { | ||||||
|                                     #( #cfg_attrs )* |                                     #( #cfg_attrs )* | ||||||
|  | |||||||
| @ -1,15 +0,0 @@ | |||||||
| //! Functions to aid the `Api::to_tokens` method.
 |  | ||||||
| 
 |  | ||||||
| use proc_macro2::TokenStream; |  | ||||||
| use quote::{quote, ToTokens}; |  | ||||||
| 
 |  | ||||||
| pub fn map_option_literal<T: ToTokens>(ver: &Option<T>) -> TokenStream { |  | ||||||
|     match ver { |  | ||||||
|         Some(v) => quote! { ::std::option::Option::Some(#v) }, |  | ||||||
|         None => quote! { ::std::option::Option::None }, |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub fn is_valid_endpoint_path(string: &str) -> bool { |  | ||||||
|     string.as_bytes().iter().all(|b| (0x21..=0x7E).contains(b)) |  | ||||||
| } |  | ||||||
| @ -1,53 +0,0 @@ | |||||||
| use std::num::NonZeroU8; |  | ||||||
| 
 |  | ||||||
| use proc_macro2::TokenStream; |  | ||||||
| use quote::{format_ident, quote, ToTokens}; |  | ||||||
| use syn::{parse::Parse, Error, LitFloat}; |  | ||||||
| 
 |  | ||||||
| #[derive(Clone, Debug, PartialEq)] |  | ||||||
| pub struct MatrixVersionLiteral { |  | ||||||
|     pub(crate) major: NonZeroU8, |  | ||||||
|     pub(crate) minor: u8, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const ONE: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) }; |  | ||||||
| 
 |  | ||||||
| impl MatrixVersionLiteral { |  | ||||||
|     pub const V1_0: Self = Self { major: ONE, minor: 0 }; |  | ||||||
|     pub const V1_1: Self = Self { major: ONE, minor: 1 }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for MatrixVersionLiteral { |  | ||||||
|     fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> { |  | ||||||
|         let fl: LitFloat = input.parse()?; |  | ||||||
| 
 |  | ||||||
|         if !fl.suffix().is_empty() { |  | ||||||
|             return Err(Error::new_spanned( |  | ||||||
|                 fl, |  | ||||||
|                 "matrix version has to be only two positive numbers separated by a `.`", |  | ||||||
|             )); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let ver_vec: Vec<String> = fl.to_string().split('.').map(&str::to_owned).collect(); |  | ||||||
| 
 |  | ||||||
|         let ver: [String; 2] = ver_vec.try_into().map_err(|_| { |  | ||||||
|             Error::new_spanned(&fl, "did not contain only both an X and Y value like X.Y") |  | ||||||
|         })?; |  | ||||||
| 
 |  | ||||||
|         let major: NonZeroU8 = ver[0].parse().map_err(|e| { |  | ||||||
|             Error::new_spanned(&fl, format!("major number failed to parse as >0 number: {e}")) |  | ||||||
|         })?; |  | ||||||
|         let minor: u8 = ver[1] |  | ||||||
|             .parse() |  | ||||||
|             .map_err(|e| Error::new_spanned(&fl, format!("minor number failed to parse: {e}")))?; |  | ||||||
| 
 |  | ||||||
|         Ok(Self { major, minor }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl ToTokens for MatrixVersionLiteral { |  | ||||||
|     fn to_tokens(&self, tokens: &mut TokenStream) { |  | ||||||
|         let variant = format_ident!("V{}_{}", u8::from(self.major), self.minor); |  | ||||||
|         tokens.extend(quote! { ::ruma_common::api::MatrixVersion::#variant }); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,5 +1,9 @@ | |||||||
| # [unreleased] | # [unreleased] | ||||||
| 
 | 
 | ||||||
|  | # 0.10.1 | ||||||
|  | 
 | ||||||
|  | Upgrade `ruma-events` to 0.28.1. | ||||||
|  | 
 | ||||||
| # 0.10.0 | # 0.10.0 | ||||||
| 
 | 
 | ||||||
| - Bump MSRV to 1.75 | - Bump MSRV to 1.75 | ||||||
|  | |||||||
| @ -7,7 +7,7 @@ homepage = "https://ruma.dev/" | |||||||
| repository = "https://github.com/ruma/ruma" | repository = "https://github.com/ruma/ruma" | ||||||
| readme = "README.md" | readme = "README.md" | ||||||
| license = "MIT" | license = "MIT" | ||||||
| version = "0.10.0" | version = "0.10.1" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| rust-version = { workspace = true } | rust-version = { workspace = true } | ||||||
| 
 | 
 | ||||||
| @ -30,22 +30,11 @@ client-hyper = ["client", "ruma-client?/hyper"] | |||||||
| client-hyper-native-tls = ["client", "ruma-client?/hyper-native-tls"] | client-hyper-native-tls = ["client", "ruma-client?/hyper-native-tls"] | ||||||
| client-reqwest = ["client", "ruma-client?/reqwest"] | client-reqwest = ["client", "ruma-client?/reqwest"] | ||||||
| client-reqwest-native-tls = ["client", "ruma-client?/reqwest-native-tls"] | client-reqwest-native-tls = ["client", "ruma-client?/reqwest-native-tls"] | ||||||
| client-reqwest-native-tls-vendored = [ | client-reqwest-native-tls-alpn = ["client", "ruma-client?/reqwest-native-tls-alpn"] | ||||||
|     "client", | client-reqwest-native-tls-vendored = ["client", "ruma-client?/reqwest-native-tls-vendored"] | ||||||
|     "ruma-client?/reqwest-native-tls-vendored", | client-reqwest-rustls-manual-roots = ["client", "ruma-client?/reqwest-rustls-manual-roots"] | ||||||
| ] | client-reqwest-rustls-webpki-roots = ["client", "ruma-client?/reqwest-rustls-webpki-roots"] | ||||||
| client-reqwest-rustls-manual-roots = [ | client-reqwest-rustls-native-roots = ["client", "ruma-client?/reqwest-rustls-native-roots"] | ||||||
|     "client", |  | ||||||
|     "ruma-client?/reqwest-rustls-manual-roots", |  | ||||||
| ] |  | ||||||
| client-reqwest-rustls-webpki-roots = [ |  | ||||||
|     "client", |  | ||||||
|     "ruma-client?/reqwest-rustls-webpki-roots", |  | ||||||
| ] |  | ||||||
| client-reqwest-rustls-native-roots = [ |  | ||||||
|     "client", |  | ||||||
|     "ruma-client?/reqwest-rustls-native-roots", |  | ||||||
| ] |  | ||||||
| 
 | 
 | ||||||
| appservice-api-c = [ | appservice-api-c = [ | ||||||
|     "api", |     "api", | ||||||
| @ -214,6 +203,7 @@ unstable-exhaustive-types = [ | |||||||
|     "ruma-federation-api?/unstable-exhaustive-types", |     "ruma-federation-api?/unstable-exhaustive-types", | ||||||
|     "ruma-identity-service-api?/unstable-exhaustive-types", |     "ruma-identity-service-api?/unstable-exhaustive-types", | ||||||
|     "ruma-push-gateway-api?/unstable-exhaustive-types", |     "ruma-push-gateway-api?/unstable-exhaustive-types", | ||||||
|  |     "ruma-signatures?/unstable-exhaustive-types", | ||||||
|     "ruma-state-res?/unstable-exhaustive-types", |     "ruma-state-res?/unstable-exhaustive-types", | ||||||
|     "ruma-events?/unstable-exhaustive-types", |     "ruma-events?/unstable-exhaustive-types", | ||||||
| ] | ] | ||||||
| @ -274,6 +264,7 @@ unstable-msc3955 = ["ruma-events?/unstable-msc3955"] | |||||||
| unstable-msc3956 = ["ruma-events?/unstable-msc3956"] | unstable-msc3956 = ["ruma-events?/unstable-msc3956"] | ||||||
| unstable-msc3983 = ["ruma-client-api?/unstable-msc3983"] | unstable-msc3983 = ["ruma-client-api?/unstable-msc3983"] | ||||||
| unstable-msc4075 = ["ruma-events?/unstable-msc4075"] | unstable-msc4075 = ["ruma-events?/unstable-msc4075"] | ||||||
|  | unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"] | ||||||
| unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] | unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"] | ||||||
| unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] | unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"] | ||||||
| unstable-pdu = ["ruma-events?/unstable-pdu"] | unstable-pdu = ["ruma-events?/unstable-pdu"] | ||||||
| @ -327,6 +318,7 @@ __ci = [ | |||||||
|     "unstable-msc3956", |     "unstable-msc3956", | ||||||
|     "unstable-msc3983", |     "unstable-msc3983", | ||||||
|     "unstable-msc4075", |     "unstable-msc4075", | ||||||
|  |     "unstable-msc4108", | ||||||
|     "unstable-msc4121", |     "unstable-msc4121", | ||||||
|     "unstable-msc4125", |     "unstable-msc4125", | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ edition = "2021" | |||||||
| publish = false | publish = false | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } | ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } | ||||||
| 
 | 
 | ||||||
| anyhow = "1.0.37" | anyhow = "1.0.37" | ||||||
| tokio = { version = "1.0.1", features = ["macros", "rt"] } | tokio = { version = "1.0.1", features = ["macros", "rt"] } | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ edition = "2021" | |||||||
| publish = false | publish = false | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } | ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } | ||||||
| # For building locally: use the git dependencies below. | # For building locally: use the git dependencies below. | ||||||
| # Browse the source at this revision here: https://github.com/ruma/ruma/tree/f161c8117c706fc52089999e1f406cf34276ec9d | # Browse the source at this revision here: https://github.com/ruma/ruma/tree/f161c8117c706fc52089999e1f406cf34276ec9d | ||||||
| # ruma = { git = "https://github.com/ruma/ruma", rev = "f161c8117c706fc52089999e1f406cf34276ec9d", features = ["client-api-c", "client", "client-hyper-native-tls", "events"] } | # ruma = { git = "https://github.com/ruma/ruma", rev = "f161c8117c706fc52089999e1f406cf34276ec9d", features = ["client-api-c", "client", "client-hyper-native-tls", "events"] } | ||||||
|  | |||||||
| @ -6,6 +6,6 @@ publish = false | |||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| anyhow = "1.0.37" | anyhow = "1.0.37" | ||||||
| ruma = { version = "0.10.0", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls"] } | ruma = { version = "0.10.1", path = "../../crates/ruma", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls"] } | ||||||
| tokio = { version = "1.0.1", features = ["macros", "rt"] } | tokio = { version = "1.0.1", features = ["macros", "rt"] } | ||||||
| tokio-stream = { version = "0.1.1", default-features = false } | tokio-stream = { version = "0.1.1", default-features = false } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| [toolchain] | [toolchain] | ||||||
| # Keep in sync with version in `xtask/src/ci.rs` and `.github/workflows/ci.yml` | # Keep in sync with version in `xtask/src/ci.rs` and `.github/workflows/ci.yml` | ||||||
| channel = "nightly-2024-02-14" | channel = "nightly-2024-05-09" | ||||||
| components = ["rustfmt", "clippy"] | components = ["rustfmt", "clippy"] | ||||||
|  | |||||||
| @ -1,9 +1,14 @@ | |||||||
| use std::path::PathBuf; | #![allow(clippy::disallowed_types)] | ||||||
| 
 | 
 | ||||||
|  | use std::{collections::HashMap, path::PathBuf}; | ||||||
|  | 
 | ||||||
|  | #[cfg(feature = "default")] | ||||||
| use reqwest::blocking::Client; | use reqwest::blocking::Client; | ||||||
| use semver::Version; | use semver::Version; | ||||||
| use serde::{de::IgnoredAny, Deserialize}; | use serde::{de::IgnoredAny, Deserialize}; | ||||||
|  | #[cfg(feature = "default")] | ||||||
| use toml_edit::{value, Document}; | use toml_edit::{value, Document}; | ||||||
|  | #[cfg(feature = "default")] | ||||||
| use xshell::{cmd, pushd, read_file, write_file}; | use xshell::{cmd, pushd, read_file, write_file}; | ||||||
| 
 | 
 | ||||||
| use crate::{util::ask_yes_no, Metadata, Result}; | use crate::{util::ask_yes_no, Metadata, Result}; | ||||||
| @ -22,11 +27,51 @@ pub struct Package { | |||||||
|     /// The package's manifest path.
 |     /// The package's manifest path.
 | ||||||
|     pub manifest_path: PathBuf, |     pub manifest_path: PathBuf, | ||||||
| 
 | 
 | ||||||
|     /// A map of the package dependencies.
 |     /// A list of the package dependencies.
 | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     pub dependencies: Vec<Dependency>, |     pub dependencies: Vec<Dependency>, | ||||||
|  | 
 | ||||||
|  |     /// A map of the package features.
 | ||||||
|  |     #[serde(default)] | ||||||
|  |     pub features: HashMap<String, Vec<String>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl Package { | ||||||
|  |     /// Whether this package has a way to enable the given feature from the given package.
 | ||||||
|  |     pub fn can_enable_feature(&self, package_name: &str, feature_name: &str) -> bool { | ||||||
|  |         for activated_feature in self.features.values().flatten() { | ||||||
|  |             // Remove optional `dep:` at the start.
 | ||||||
|  |             let remaining = activated_feature.trim_start_matches("dep:"); | ||||||
|  | 
 | ||||||
|  |             // Check that we have the package name.
 | ||||||
|  |             let Some(remaining) = remaining.strip_prefix(package_name) else { | ||||||
|  |                 continue; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             if remaining.is_empty() { | ||||||
|  |                 // The feature only enables the dependency.
 | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Remove optional `?`.
 | ||||||
|  |             let remaining = remaining.trim_start_matches('?'); | ||||||
|  | 
 | ||||||
|  |             let Some(remaining) = remaining.strip_prefix('/') else { | ||||||
|  |                 // This is another package name starting with the same string.
 | ||||||
|  |                 continue; | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             // Finally, only the feature name is remaining.
 | ||||||
|  |             if remaining == feature_name { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(feature = "default")] | ||||||
| impl Package { | impl Package { | ||||||
|     /// Update the version of this crate.
 |     /// Update the version of this crate.
 | ||||||
|     pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> { |     pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> { | ||||||
| @ -203,6 +248,7 @@ pub enum DependencyKind { | |||||||
|     Build, |     Build, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[cfg(feature = "default")] | ||||||
| /// A crate from the `GET /crates/{crate}` endpoint of crates.io.
 | /// A crate from the `GET /crates/{crate}` endpoint of crates.io.
 | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| struct CratesIoCrate { | struct CratesIoCrate { | ||||||
|  | |||||||
| @ -1,15 +1,17 @@ | |||||||
| // Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
 | // Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
 | ||||||
| #![allow(unused_qualifications)] | #![allow(unused_qualifications)] | ||||||
| 
 | 
 | ||||||
| use std::path::PathBuf; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| use clap::{Args, Subcommand}; | use clap::{Args, Subcommand}; | ||||||
| use xshell::pushd; | use xshell::pushd; | ||||||
| 
 | 
 | ||||||
| use crate::{cmd, Metadata, Result, NIGHTLY}; | use crate::{cmd, Metadata, Result, NIGHTLY}; | ||||||
| 
 | 
 | ||||||
|  | mod reexport_features; | ||||||
| mod spec_links; | mod spec_links; | ||||||
| 
 | 
 | ||||||
|  | use reexport_features::check_reexport_features; | ||||||
| use spec_links::check_spec_links; | use spec_links::check_spec_links; | ||||||
| 
 | 
 | ||||||
| const MSRV: &str = "1.75"; | const MSRV: &str = "1.75"; | ||||||
| @ -66,6 +68,8 @@ pub enum CiCmd { | |||||||
|     Dependencies, |     Dependencies, | ||||||
|     /// Check spec links point to a recent version (lint)
 |     /// Check spec links point to a recent version (lint)
 | ||||||
|     SpecLinks, |     SpecLinks, | ||||||
|  |     /// Check all cargo features of sub-crates can be enabled from ruma (lint)
 | ||||||
|  |     ReexportFeatures, | ||||||
|     /// Check typos
 |     /// Check typos
 | ||||||
|     Typos, |     Typos, | ||||||
| } | } | ||||||
| @ -75,18 +79,22 @@ pub struct CiTask { | |||||||
|     /// Which command to run.
 |     /// Which command to run.
 | ||||||
|     cmd: Option<CiCmd>, |     cmd: Option<CiCmd>, | ||||||
| 
 | 
 | ||||||
|     /// The root of the workspace.
 |     /// The metadata of the workspace.
 | ||||||
|     project_root: PathBuf, |     project_metadata: Metadata, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl CiTask { | impl CiTask { | ||||||
|     pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> { |     pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> { | ||||||
|         let project_root = Metadata::load()?.workspace_root; |         let project_metadata = Metadata::load()?; | ||||||
|         Ok(Self { cmd, project_root }) |         Ok(Self { cmd, project_metadata }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn project_root(&self) -> &Path { | ||||||
|  |         &self.project_metadata.workspace_root | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub(crate) fn run(self) -> Result<()> { |     pub(crate) fn run(self) -> Result<()> { | ||||||
|         let _p = pushd(&self.project_root)?; |         let _p = pushd(self.project_root())?; | ||||||
| 
 | 
 | ||||||
|         match self.cmd { |         match self.cmd { | ||||||
|             Some(CiCmd::Msrv) => self.msrv()?, |             Some(CiCmd::Msrv) => self.msrv()?, | ||||||
| @ -110,7 +118,8 @@ impl CiTask { | |||||||
|             Some(CiCmd::ClippyAll) => self.clippy_all()?, |             Some(CiCmd::ClippyAll) => self.clippy_all()?, | ||||||
|             Some(CiCmd::Lint) => self.lint()?, |             Some(CiCmd::Lint) => self.lint()?, | ||||||
|             Some(CiCmd::Dependencies) => self.dependencies()?, |             Some(CiCmd::Dependencies) => self.dependencies()?, | ||||||
|             Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root.join("crates"))?, |             Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root().join("crates"))?, | ||||||
|  |             Some(CiCmd::ReexportFeatures) => check_reexport_features(&self.project_metadata)?, | ||||||
|             Some(CiCmd::Typos) => self.typos()?, |             Some(CiCmd::Typos) => self.typos()?, | ||||||
|             None => { |             None => { | ||||||
|                 self.msrv() |                 self.msrv() | ||||||
| @ -229,7 +238,7 @@ impl CiTask { | |||||||
|         cmd!( |         cmd!( | ||||||
|             " |             " | ||||||
|             rustup run {NIGHTLY} cargo check |             rustup run {NIGHTLY} cargo check | ||||||
|                 --workspace --all-features -Z unstable-options -Z check-cfg |                 --workspace --all-features -Z unstable-options | ||||||
|             " |             " | ||||||
|         ) |         ) | ||||||
|         .env( |         .env( | ||||||
| @ -301,9 +310,11 @@ impl CiTask { | |||||||
|         // Check dependencies being sorted
 |         // Check dependencies being sorted
 | ||||||
|         let dependencies_res = self.dependencies(); |         let dependencies_res = self.dependencies(); | ||||||
|         // Check that all links point to the same version of the spec
 |         // Check that all links point to the same version of the spec
 | ||||||
|         let spec_links_res = check_spec_links(&self.project_root.join("crates")); |         let spec_links_res = check_spec_links(&self.project_root().join("crates")); | ||||||
|  |         // Check that all cargo features of sub-crates can be enabled from ruma.
 | ||||||
|  |         let reexport_features_res = check_reexport_features(&self.project_metadata); | ||||||
| 
 | 
 | ||||||
|         dependencies_res.and(spec_links_res) |         dependencies_res.and(spec_links_res).and(reexport_features_res) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Check the sorting of dependencies with the nightly version.
 |     /// Check the sorting of dependencies with the nightly version.
 | ||||||
|  | |||||||
							
								
								
									
										54
									
								
								xtask/src/ci/reexport_features.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								xtask/src/ci/reexport_features.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | use crate::{Metadata, Result}; | ||||||
|  | 
 | ||||||
|  | /// Check that the ruma crate allows to enable all the features of the other ruma-* crates.
 | ||||||
|  | ///
 | ||||||
|  | /// For simplicity, this function assumes that:
 | ||||||
|  | ///
 | ||||||
|  | /// - Those dependencies are not renamed.
 | ||||||
|  | /// - ruma does not use `default-features = false` on those dependencies.
 | ||||||
|  | ///
 | ||||||
|  | /// This does not check if all features are re-exported individually, as that is not always wanted.
 | ||||||
|  | pub(crate) fn check_reexport_features(metadata: &Metadata) -> Result<()> { | ||||||
|  |     println!("Checking all features can be enabled from ruma…"); | ||||||
|  |     let mut n_errors = 0; | ||||||
|  | 
 | ||||||
|  |     let Some(ruma) = metadata.find_package("ruma") else { | ||||||
|  |         return Err("ruma package not found in workspace".into()); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     for package in ruma.dependencies.iter().filter_map(|dep| metadata.find_package(&dep.name)) { | ||||||
|  |         println!("Checking features of {}…", package.name); | ||||||
|  | 
 | ||||||
|  |         // Exclude ruma and xtask.
 | ||||||
|  |         if !package.name.starts_with("ruma-") { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Filter features that are enabled by other features of the same package.
 | ||||||
|  |         let features = package.features.keys().filter(|feature_name| { | ||||||
|  |             !package.features.values().flatten().any(|activated_feature| { | ||||||
|  |                 activated_feature.trim_start_matches("dep:") == *feature_name | ||||||
|  |             }) | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         for feature_name in features { | ||||||
|  |             // Let's assume that ruma never has `default-features = false`.
 | ||||||
|  |             if feature_name == "default" { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if !ruma.can_enable_feature(&package.name, feature_name) { | ||||||
|  |                 println!(r#"  Missing feature "{}/{feature_name}""#, package.name); | ||||||
|  |                 n_errors += 1; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if n_errors > 0 { | ||||||
|  |         // Visual aid to separate the end error message.
 | ||||||
|  |         println!(); | ||||||
|  |         return Err(format!("Found {n_errors} missing features").into()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
| @ -15,9 +15,8 @@ use serde::Deserialize; | |||||||
| use serde_json::from_str as from_json_str; | use serde_json::from_str as from_json_str; | ||||||
| 
 | 
 | ||||||
| // Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
 | // Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
 | ||||||
| const NIGHTLY: &str = "nightly-2024-02-14"; | const NIGHTLY: &str = "nightly-2024-05-09"; | ||||||
| 
 | 
 | ||||||
| #[cfg(feature = "default")] |  | ||||||
| mod cargo; | mod cargo; | ||||||
| mod ci; | mod ci; | ||||||
| mod doc; | mod doc; | ||||||
| @ -26,6 +25,7 @@ mod release; | |||||||
| #[cfg(feature = "default")] | #[cfg(feature = "default")] | ||||||
| mod util; | mod util; | ||||||
| 
 | 
 | ||||||
|  | use cargo::Package; | ||||||
| use ci::{CiArgs, CiTask}; | use ci::{CiArgs, CiTask}; | ||||||
| use doc::DocTask; | use doc::DocTask; | ||||||
| #[cfg(feature = "default")] | #[cfg(feature = "default")] | ||||||
| @ -70,8 +70,7 @@ fn main() -> Result<()> { | |||||||
| #[derive(Clone, Debug, Deserialize)] | #[derive(Clone, Debug, Deserialize)] | ||||||
| struct Metadata { | struct Metadata { | ||||||
|     pub workspace_root: PathBuf, |     pub workspace_root: PathBuf, | ||||||
|     #[cfg(feature = "default")] |     pub packages: Vec<Package>, | ||||||
|     pub packages: Vec<cargo::Package>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Metadata { | impl Metadata { | ||||||
| @ -80,6 +79,11 @@ impl Metadata { | |||||||
|         let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?; |         let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?; | ||||||
|         Ok(from_json_str(&metadata_json)?) |         Ok(from_json_str(&metadata_json)?) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Find the package with the given name.
 | ||||||
|  |     pub fn find_package(&self, name: &str) -> Option<&Package> { | ||||||
|  |         self.packages.iter().find(|p| p.name == name) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(feature = "default")] | #[cfg(feature = "default")] | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user