Merge remote-tracking branch 'upstream/main' into conduwuit-changes

This commit is contained in:
strawberry 2024-05-21 21:31:00 -04:00
commit 042444dc1d
37 changed files with 596 additions and 613 deletions

View File

@ -4,7 +4,7 @@ env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
# 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:
push:

View File

@ -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-client = { version = "0.13.0", path = "crates/ruma-client" }
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-html = { version = "0.2.0", path = "crates/ruma-html" }
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"
thiserror = "1.0.37"
tracing = { version = "0.1.37", default-features = false, features = ["std"] }
url = { version = "2.5.0" }
web-time = "1.1.0"
[profile.dev]

View File

@ -1,5 +1,9 @@
# [unreleased]
Improvements:
- Add support for MSC4108 OIDC sign in and E2EE set up via QR code
# 0.18.0
Bug fixes:

View File

@ -50,6 +50,7 @@ unstable-msc3575 = []
unstable-msc3814 = []
unstable-msc3843 = []
unstable-msc3983 = []
unstable-msc4108 = []
unstable-msc4121 = []
[dependencies]
@ -67,6 +68,7 @@ serde = { workspace = true }
serde_html_form = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] }
web-time = { workspace = true }
[dev-dependencies]

View File

@ -22,8 +22,7 @@ pub fn system_time_to_http_date(
date_header::format(duration.as_secs(), &mut buffer)
.map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
Ok(http::HeaderValue::from_bytes(&buffer)
.expect("date_header should produce a valid header value"))
Ok(HeaderValue::from_bytes(&buffer).expect("date_header should produce a valid header value"))
}
/// Convert a header value representing a HTTP date to a `SystemTime`.

View File

@ -35,6 +35,8 @@ pub mod read_marker;
pub mod receipt;
pub mod redact;
pub mod relations;
#[cfg(feature = "unstable-msc4108")]
pub mod rendezvous;
pub mod room;
pub mod search;
pub mod server;

View File

@ -148,11 +148,11 @@ pub mod v3 {
}
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,
>::new(
path_args.iter().map(::std::convert::AsRef::as_ref),
path_args.iter().map(::std::convert::AsRef::as_ref)
))?;
let IncomingRequestQuery { before, after } =

View File

@ -0,0 +1,3 @@
//! Endpoints for managing rendezvous sessions.
pub mod create_rendezvous_session;

View File

@ -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)?)
}
}
}

View File

@ -145,7 +145,7 @@ impl<C: HttpClient> Client<C> {
.send_request(assign!(register::v3::Request::new(), { kind: RegistrationKind::Guest }))
.await?;
*self.0.access_token.lock().unwrap() = response.access_token.clone();
self.0.access_token.lock().unwrap().clone_from(&response.access_token);
Ok(response)
}
@ -169,7 +169,7 @@ impl<C: HttpClient> Client<C> {
}))
.await?;
*self.0.access_token.lock().unwrap() = response.access_token.clone();
self.0.access_token.lock().unwrap().clone_from(&response.access_token);
Ok(response)
}
@ -221,7 +221,7 @@ impl<C: HttpClient> Client<C> {
}))
.await?;
since = response.next_batch.clone();
since.clone_from(&response.next_batch);
yield response;
}
}

View File

@ -1,5 +1,10 @@
# [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
Bug fixes:

View File

@ -85,7 +85,7 @@ serde_json = { workspace = true, features = ["raw_value"] }
thiserror = { workspace = true }
time = "0.3.34"
tracing = { workspace = true, features = ["attributes"] }
url = "2.2.2"
url = { workspace = true }
uuid = { version = "1.0.0", optional = true, features = ["v4"] }
web-time = { workspace = true }
wildmatch = "2.0.0"

View File

@ -276,6 +276,20 @@ pub enum HeaderDeserializationError {
/// The given required header is missing.
#[error("missing header `{0}`")]
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.

View File

@ -58,10 +58,7 @@ impl<'de> Visitor<'de> for RoomNetworkVisitor {
while let Some((key, value)) = access.next_entry::<String, JsonValue>()? {
match key.as_str() {
"include_all_networks" => {
include_all_networks = match value.as_bool() {
Some(b) => b,
_ => false,
}
include_all_networks = value.as_bool().unwrap_or(false);
}
"third_party_instance_id" => {
third_party_instance_id = value.as_str().map(|v| v.to_owned());

View File

@ -153,7 +153,7 @@ impl<T> Raw<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")
}

View File

@ -79,7 +79,7 @@ impl IncomingRequest for Request {
B: AsRef<[u8]>,
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,
>::new(

View File

@ -1,5 +1,14 @@
# [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
Bug fixes:

View File

@ -1,6 +1,6 @@
[package]
name = "ruma-events"
version = "0.28.0"
version = "0.28.1"
description = "Serializable types for the events in the Matrix specification."
homepage = "https://ruma.dev/"
keywords = ["matrix", "chat", "messaging", "ruma"]
@ -72,7 +72,7 @@ serde = { workspace = true }
serde_json = { workspace = true, features = ["raw_value"] }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
url = "2.2.2"
url = { workspace = true }
wildmatch = "2.0.0"
# dev-dependencies can't be optional, so this is a regular dependency

View File

@ -141,7 +141,7 @@ impl<'de> Deserialize<'de> for JoinRule {
}
let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
.map_err(serde::de::Error::custom)?
.map_err(Error::custom)?
.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.
let rule_type = serde_json::from_str::<ExtractType<'_>>(json.get())
.map_err(serde::de::Error::custom)?
.rule_type;
let rule_type =
serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(Error::custom)?.rule_type;
match rule_type.as_deref() {
Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),

View File

@ -218,30 +218,12 @@ impl RoomMessageEventContent {
/// 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(
mut self,
self,
previous_message: &OriginalRoomMessageEvent,
is_reply: ReplyWithinThread,
add_mentions: AddMentions,
) -> Self {
if is_reply == ReplyWithinThread::Yes {
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
self.without_relation().make_for_thread(previous_message, is_reply, add_mentions)
}
/// Turns `self` into a [replacement] (or edit) for a given message.
@ -259,9 +241,9 @@ impl RoomMessageEventContent {
/// `original_message`.
#[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
/// `m.new_content` to keep the same mentions, but not into `content` to avoid repeated
/// notifications.
/// 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
///
@ -270,31 +252,11 @@ impl RoomMessageEventContent {
/// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
#[track_caller]
pub fn make_replacement(
mut self,
self,
metadata: impl Into<ReplacementMetadata>,
replied_to_message: Option<&OriginalRoomMessageEvent>,
) -> Self {
let metadata = metadata.into();
// 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
self.without_relation().make_replacement(metadata, replied_to_message)
}
/// Set the [mentions] of this event.
@ -308,6 +270,7 @@ impl RoomMessageEventContent {
/// used instead.
///
/// [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 {
if let Some(Relation::Replacement(replacement)) = &mut self.relates_to {
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
/// the values of `room`.
///
/// This is recommended over [`Self::set_mentions()`] to avoid to overwrite any mentions set
/// automatically by another method, like [`Self::make_reply_to()`]. However, this method has no
/// special support for replacements.
/// This should be called before methods that add a relation, like [`Self::make_reply_to()`] and
/// [`Self::make_replacement()`], for the mentions to be correctly set.
///
/// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
pub fn add_mentions(mut self, mentions: Mentions) -> Self {
@ -686,6 +648,8 @@ impl MessageType {
if let Some(formatted) = formatted {
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 {
*body = remove_plain_reply_fallback(body).to_owned();
}

View File

@ -4,10 +4,10 @@ use serde::{Deserialize, Serialize};
use super::{
AddMentions, ForwardThread, MessageType, OriginalRoomMessageEvent, Relation,
RoomMessageEventContent,
ReplacementMetadata, ReplyWithinThread, RoomMessageEventContent,
};
use crate::{
relation::{InReplyTo, Thread},
relation::{InReplyTo, Replacement, Thread},
room::message::{reply::OriginalEventData, FormattedBody},
AnySyncTimelineEvent, Mentions,
};
@ -209,6 +209,127 @@ impl RoomMessageEventContentWithoutRelation {
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.
///
/// 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 }
}
}
impl From<RoomMessageEventContentWithoutRelation> for RoomMessageEventContent {
fn from(value: RoomMessageEventContentWithoutRelation) -> Self {
let RoomMessageEventContentWithoutRelation { msgtype, mentions } = value;
Self { msgtype, relates_to: None, mentions }
}
}

View File

@ -1164,6 +1164,7 @@ fn video_msgtype_deserialization() {
}
#[test]
#[allow(deprecated)]
fn set_mentions() {
let mut content = RoomMessageEventContent::text_plain("you!");
let mentions = content.mentions.take();
@ -1176,7 +1177,42 @@ fn set_mentions() {
}
#[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 bob = owned_user_id!("@bob:localhost");
let original_message_json = json!({
@ -1201,19 +1237,12 @@ fn make_replacement_set_mentions() {
"This is <em>an edited</em> message.",
);
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();
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_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
assert!(replacement.new_content.mentions.is_none());
}
#[test]

View File

@ -6,13 +6,10 @@ use once_cell::sync::Lazy;
use proc_macro2::Span;
use serde::{de::IgnoredAny, Deserialize};
mod api_metadata;
mod attribute;
mod auth_scheme;
pub mod request;
pub mod response;
mod util;
mod version;
mod kw {
syn::custom_keyword!(error);

View File

@ -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);
}
}

View File

@ -53,9 +53,8 @@ impl Response {
}
ResponseFieldKind::Header(header_name) => {
let optional_header = match &field.ty {
syn::Type::Path(syn::TypePath {
path: syn::Path { segments, .. },
..
Type::Path(syn::TypePath {
path: syn::Path { segments, .. }, ..
}) if segments.last().unwrap().ident == "Option" => {
quote! {
#( #cfg_attrs )*

View File

@ -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))
}

View File

@ -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 });
}
}

View File

@ -1,5 +1,9 @@
# [unreleased]
# 0.10.1
Upgrade `ruma-events` to 0.28.1.
# 0.10.0
- Bump MSRV to 1.75

View File

@ -7,7 +7,7 @@ homepage = "https://ruma.dev/"
repository = "https://github.com/ruma/ruma"
readme = "README.md"
license = "MIT"
version = "0.10.0"
version = "0.10.1"
edition = "2021"
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-reqwest = ["client", "ruma-client?/reqwest"]
client-reqwest-native-tls = ["client", "ruma-client?/reqwest-native-tls"]
client-reqwest-native-tls-vendored = [
"client",
"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-native-roots = [
"client",
"ruma-client?/reqwest-rustls-native-roots",
]
client-reqwest-native-tls-alpn = ["client", "ruma-client?/reqwest-native-tls-alpn"]
client-reqwest-native-tls-vendored = ["client", "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-native-roots = ["client", "ruma-client?/reqwest-rustls-native-roots"]
appservice-api-c = [
"api",
@ -214,6 +203,7 @@ unstable-exhaustive-types = [
"ruma-federation-api?/unstable-exhaustive-types",
"ruma-identity-service-api?/unstable-exhaustive-types",
"ruma-push-gateway-api?/unstable-exhaustive-types",
"ruma-signatures?/unstable-exhaustive-types",
"ruma-state-res?/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-msc3983 = ["ruma-client-api?/unstable-msc3983"]
unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
unstable-pdu = ["ruma-events?/unstable-pdu"]
@ -327,6 +318,7 @@ __ci = [
"unstable-msc3956",
"unstable-msc3983",
"unstable-msc4075",
"unstable-msc4108",
"unstable-msc4121",
"unstable-msc4125",
]

View File

@ -5,7 +5,7 @@ edition = "2021"
publish = false
[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"
tokio = { version = "1.0.1", features = ["macros", "rt"] }

View File

@ -5,7 +5,7 @@ edition = "2021"
publish = false
[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.
# 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"] }

View File

@ -6,6 +6,6 @@ publish = false
[dependencies]
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-stream = { version = "0.1.1", default-features = false }

View File

@ -1,4 +1,4 @@
[toolchain]
# 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"]

View File

@ -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 semver::Version;
use serde::{de::IgnoredAny, Deserialize};
#[cfg(feature = "default")]
use toml_edit::{value, Document};
#[cfg(feature = "default")]
use xshell::{cmd, pushd, read_file, write_file};
use crate::{util::ask_yes_no, Metadata, Result};
@ -22,11 +27,51 @@ pub struct Package {
/// The package's manifest path.
pub manifest_path: PathBuf,
/// A map of the package dependencies.
/// A list of the package dependencies.
#[serde(default)]
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 {
/// Update the version of this crate.
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
@ -203,6 +248,7 @@ pub enum DependencyKind {
Build,
}
#[cfg(feature = "default")]
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
#[derive(Deserialize)]
struct CratesIoCrate {

View File

@ -1,15 +1,17 @@
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
#![allow(unused_qualifications)]
use std::path::PathBuf;
use std::path::Path;
use clap::{Args, Subcommand};
use xshell::pushd;
use crate::{cmd, Metadata, Result, NIGHTLY};
mod reexport_features;
mod spec_links;
use reexport_features::check_reexport_features;
use spec_links::check_spec_links;
const MSRV: &str = "1.75";
@ -66,6 +68,8 @@ pub enum CiCmd {
Dependencies,
/// Check spec links point to a recent version (lint)
SpecLinks,
/// Check all cargo features of sub-crates can be enabled from ruma (lint)
ReexportFeatures,
/// Check typos
Typos,
}
@ -75,18 +79,22 @@ pub struct CiTask {
/// Which command to run.
cmd: Option<CiCmd>,
/// The root of the workspace.
project_root: PathBuf,
/// The metadata of the workspace.
project_metadata: Metadata,
}
impl CiTask {
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
let project_root = Metadata::load()?.workspace_root;
Ok(Self { cmd, project_root })
let project_metadata = Metadata::load()?;
Ok(Self { cmd, project_metadata })
}
fn project_root(&self) -> &Path {
&self.project_metadata.workspace_root
}
pub(crate) fn run(self) -> Result<()> {
let _p = pushd(&self.project_root)?;
let _p = pushd(self.project_root())?;
match self.cmd {
Some(CiCmd::Msrv) => self.msrv()?,
@ -110,7 +118,8 @@ impl CiTask {
Some(CiCmd::ClippyAll) => self.clippy_all()?,
Some(CiCmd::Lint) => self.lint()?,
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()?,
None => {
self.msrv()
@ -229,7 +238,7 @@ impl CiTask {
cmd!(
"
rustup run {NIGHTLY} cargo check
--workspace --all-features -Z unstable-options -Z check-cfg
--workspace --all-features -Z unstable-options
"
)
.env(
@ -301,9 +310,11 @@ impl CiTask {
// Check dependencies being sorted
let dependencies_res = self.dependencies();
// 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.

View 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(())
}

View File

@ -15,9 +15,8 @@ use serde::Deserialize;
use serde_json::from_str as from_json_str;
// 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 ci;
mod doc;
@ -26,6 +25,7 @@ mod release;
#[cfg(feature = "default")]
mod util;
use cargo::Package;
use ci::{CiArgs, CiTask};
use doc::DocTask;
#[cfg(feature = "default")]
@ -70,8 +70,7 @@ fn main() -> Result<()> {
#[derive(Clone, Debug, Deserialize)]
struct Metadata {
pub workspace_root: PathBuf,
#[cfg(feature = "default")]
pub packages: Vec<cargo::Package>,
pub packages: Vec<Package>,
}
impl Metadata {
@ -80,6 +79,11 @@ impl Metadata {
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
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")]