events: Parse TagInfo::order
as a f64 or a stringified f64
This commit is contained in:
parent
4ac9e9a979
commit
2c8ece6bf2
@ -4,6 +4,8 @@ Bug fixes:
|
||||
|
||||
- Set the predefined server-default `.m.rule.tombstone` push rule as enabled by default, as defined
|
||||
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:
|
||||
|
||||
|
@ -69,6 +69,9 @@ compat-null = []
|
||||
# mandatory. Deserialization will yield a default value like an empty string.
|
||||
compat-optional = []
|
||||
|
||||
# Allow TagInfo to contain a stringified floating-point value for the `order` field.
|
||||
compat-tag-info = []
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
bytes = "1.0.1"
|
||||
|
@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
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.
|
||||
pub type Tags = BTreeMap<TagName, TagInfo>;
|
||||
|
||||
@ -167,11 +170,18 @@ impl Serialize for TagName {
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct TagInfo {
|
||||
/// 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")]
|
||||
#[cfg_attr(
|
||||
feature = "compat-tag-info",
|
||||
serde(default, deserialize_with = "deserialize_as_optional_f64_or_string")
|
||||
)]
|
||||
pub order: Option<f64>,
|
||||
}
|
||||
|
||||
@ -185,7 +195,7 @@ impl TagInfo {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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};
|
||||
|
||||
@ -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]
|
||||
fn display_name() {
|
||||
assert_eq!(TagName::Favorite.display_name(), "favourite");
|
||||
|
@ -26,7 +26,8 @@ pub use self::{
|
||||
cow::deserialize_cow_str,
|
||||
raw::Raw,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
@ -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.
|
||||
///
|
||||
/// To be used like this:
|
||||
@ -160,7 +212,7 @@ where
|
||||
type Value = BTreeMap<T, Int>;
|
||||
|
||||
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> {
|
||||
|
@ -98,6 +98,7 @@ compat = [
|
||||
"compat-unset-avatar",
|
||||
"compat-get-3pids",
|
||||
"compat-signature-id",
|
||||
"compat-tag-info",
|
||||
]
|
||||
# Don't validate the version part in `KeyId`.
|
||||
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"]
|
||||
# Allow extra characters in signature IDs not allowed in the specification.
|
||||
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.
|
||||
ring-compat = ["dep:ruma-signatures", "ruma-signatures?/ring-compat"]
|
||||
|
@ -211,7 +211,7 @@ impl CiTask {
|
||||
fn test_common(&self) -> Result<()> {
|
||||
cmd!(
|
||||
"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()
|
||||
.map_err(Into::into)
|
||||
|
Loading…
x
Reference in New Issue
Block a user