events: Parse TagInfo::order as a f64 or a stringified f64

This commit is contained in:
Benjamin Bouvier 2023-07-06 14:34:43 +02:00 committed by GitHub
parent 4ac9e9a979
commit 2c8ece6bf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 5 deletions

View File

@ -4,6 +4,8 @@ Bug fixes:
- Set the predefined server-default `.m.rule.tombstone` push rule as enabled by default, as defined - Set the predefined server-default `.m.rule.tombstone` push rule as enabled by default, as defined
in the spec. in the spec.
- Parse `m.tag` `order` as a f64 value or a stringified f64 value, if the `compat-tag-info` feature
is enabled.
Breaking changes: Breaking changes:

View File

@ -69,6 +69,9 @@ compat-null = []
# mandatory. Deserialization will yield a default value like an empty string. # mandatory. Deserialization will yield a default value like an empty string.
compat-optional = [] compat-optional = []
# Allow TagInfo to contain a stringified floating-point value for the `order` field.
compat-tag-info = []
[dependencies] [dependencies]
base64 = { workspace = true } base64 = { workspace = true }
bytes = "1.0.1" bytes = "1.0.1"

View File

@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
use crate::{serde::deserialize_cow_str, PrivOwnedStr}; use crate::{serde::deserialize_cow_str, PrivOwnedStr};
#[cfg(feature = "compat-tag-info")]
use crate::serde::deserialize_as_optional_f64_or_string;
/// Map of tag names to tag info. /// Map of tag names to tag info.
pub type Tags = BTreeMap<TagName, TagInfo>; pub type Tags = BTreeMap<TagName, TagInfo>;
@ -167,11 +170,18 @@ impl Serialize for TagName {
} }
/// Information about a tag. /// Information about a tag.
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct TagInfo { pub struct TagInfo {
/// Value to use for lexicographically ordering rooms with this tag. /// Value to use for lexicographically ordering rooms with this tag.
///
/// If you activate the `compat-tag-info` feature, this field can be decoded as a stringified
/// floating-point value, instead of a number as it should be according to the specification.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "compat-tag-info",
serde(default, deserialize_with = "deserialize_as_optional_f64_or_string")
)]
pub order: Option<f64>, pub order: Option<f64>,
} }
@ -185,7 +195,7 @@ impl TagInfo {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use maplit::btreemap; use maplit::btreemap;
use serde_json::{json, to_value as to_json_value}; use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::{TagEventContent, TagInfo, TagName}; use super::{TagEventContent, TagInfo, TagName};
@ -215,6 +225,33 @@ mod tests {
); );
} }
#[test]
fn deserialize_tag_info() {
let json = json!({});
assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo::default());
let json = json!({ "order": null });
assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo::default());
let json = json!({ "order": 0.42 });
assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.42) });
#[cfg(feature = "compat-tag-info")]
{
let json = json!({ "order": "0.5" });
assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.5) });
let json = json!({ "order": ".5" });
assert_eq!(from_json_value::<TagInfo>(json).unwrap(), TagInfo { order: Some(0.5) });
}
#[cfg(not(feature = "compat-tag-info"))]
{
let json = json!({ "order": "0.5" });
assert!(from_json_value::<TagInfo>(json).is_err());
}
}
#[test] #[test]
fn display_name() { fn display_name() {
assert_eq!(TagName::Favorite.display_name(), "favourite"); assert_eq!(TagName::Favorite.display_name(), "favourite");

View File

@ -26,7 +26,8 @@ pub use self::{
cow::deserialize_cow_str, cow::deserialize_cow_str,
raw::Raw, raw::Raw,
strings::{ strings::{
btreemap_deserialize_v1_powerlevel_values, deserialize_v1_powerlevel, empty_string_as_none, btreemap_deserialize_v1_powerlevel_values, deserialize_as_f64_or_string,
deserialize_as_optional_f64_or_string, deserialize_v1_powerlevel, empty_string_as_none,
none_as_empty_string, none_as_empty_string,
}, },
}; };

View File

@ -49,6 +49,58 @@ where
} }
} }
/// Take either a floating point number or a string and deserialize to an floating-point number.
///
/// To be used like this:
/// `#[serde(deserialize_with = "deserialize_as_f64_or_string")]`
pub fn deserialize_as_f64_or_string<'de, D>(de: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
struct F64OrStringVisitor;
impl<'de> Visitor<'de> for F64OrStringVisitor {
type Value = f64;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a double or a string")
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v.into())
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse().map_err(E::custom)
}
}
de.deserialize_any(F64OrStringVisitor)
}
#[derive(Deserialize)]
struct F64OrStringWrapper(#[serde(deserialize_with = "deserialize_as_f64_or_string")] f64);
/// Deserializes an `Option<f64>` as encoded as a f64 or a string.
pub fn deserialize_as_optional_f64_or_string<'de, D>(
deserializer: D,
) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<F64OrStringWrapper>::deserialize(deserializer)?.map(|w| w.0))
}
/// Take either an integer number or a string and deserialize to an integer number. /// Take either an integer number or a string and deserialize to an integer number.
/// ///
/// To be used like this: /// To be used like this:
@ -160,7 +212,7 @@ where
type Value = BTreeMap<T, Int>; type Value = BTreeMap<T, Int>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map with integers or stings as values") formatter.write_str("a map with integers or strings as values")
} }
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> { fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {

View File

@ -98,6 +98,7 @@ compat = [
"compat-unset-avatar", "compat-unset-avatar",
"compat-get-3pids", "compat-get-3pids",
"compat-signature-id", "compat-signature-id",
"compat-tag-info",
] ]
# Don't validate the version part in `KeyId`. # Don't validate the version part in `KeyId`.
compat-key-id = ["ruma-common/compat-key-id"] compat-key-id = ["ruma-common/compat-key-id"]
@ -128,6 +129,8 @@ compat-get-3pids = ["ruma-client-api?/compat-get-3pids"]
compat-upload-signatures = ["ruma-client-api?/compat-upload-signatures"] compat-upload-signatures = ["ruma-client-api?/compat-upload-signatures"]
# Allow extra characters in signature IDs not allowed in the specification. # Allow extra characters in signature IDs not allowed in the specification.
compat-signature-id = ["ruma-signatures?/compat-signature-id"] compat-signature-id = ["ruma-signatures?/compat-signature-id"]
# Allow TagInfo to contain a stringified floating-point value for the `order` field.
compat-tag-info = ["ruma-common/compat-tag-info"]
# Specific compatibility for past ring public/private key documents. # Specific compatibility for past ring public/private key documents.
ring-compat = ["dep:ruma-signatures", "ruma-signatures?/ring-compat"] ring-compat = ["dep:ruma-signatures", "ruma-signatures?/ring-compat"]

View File

@ -211,7 +211,7 @@ impl CiTask {
fn test_common(&self) -> Result<()> { fn test_common(&self) -> Result<()> {
cmd!( cmd!(
"rustup run stable cargo test -p ruma-common "rustup run stable cargo test -p ruma-common
--features events,compat-empty-string-null,compat-user-id compat" --features events,compat-empty-string-null,compat-user-id,compat-tag-info compat"
) )
.run() .run()
.map_err(Into::into) .map_err(Into::into)