Convert m.tag and m.typing to the new API.

This commit is contained in:
Jimmy Cuadra 2019-06-20 03:19:05 -07:00
parent 1f3e33c77d
commit 4be050b19a
9 changed files with 222 additions and 419 deletions

View File

@ -2,32 +2,32 @@
use std::collections::HashMap; use std::collections::HashMap;
use ruma_events_macros::ruma_event;
use ruma_identifiers::{RoomId, UserId}; use ruma_identifiers::{RoomId, UserId};
use serde::{Deserialize, Serialize};
event! { ruma_event! {
/// Informs the client about the rooms that are considered direct by a user. /// Informs the client about the rooms that are considered direct by a user.
pub struct DirectEvent(DirectEventContent) {} DirectEvent {
kind: Event,
event_type: Direct,
content_type_alias: {
/// The payload for `DirectEvent`.
///
/// A mapping of `UserId`s to a list of `RoomId`s which are considered *direct* for that
/// particular user.
HashMap<UserId, Vec<RoomId>>
},
}
} }
/// The payload of a `DirectEvent`.
///
/// A mapping of `UserId`'s to a collection of `RoomId`'s which are considered
/// *direct* for that particular user.
pub type DirectEventContent = HashMap<UserId, Vec<RoomId>>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use ruma_identifiers::{RoomId, UserId}; use ruma_identifiers::{RoomId, UserId};
use serde_json::{from_str, to_string}; use serde_json::to_string;
use crate::{ use super::{DirectEvent, DirectEventContent};
collections,
direct::{DirectEvent, DirectEventContent},
EventType,
};
#[test] #[test]
fn serialization() { fn serialization() {
@ -39,7 +39,6 @@ mod tests {
let event = DirectEvent { let event = DirectEvent {
content, content,
event_type: EventType::Direct,
}; };
assert_eq!( assert_eq!(
@ -70,33 +69,10 @@ mod tests {
rooms[1].to_string() rooms[1].to_string()
); );
let event = from_str::<DirectEvent>(&json_data).unwrap(); let event = DirectEvent::from_str(&json_data).unwrap();
assert_eq!(event.event_type, EventType::Direct);
let direct_rooms = event.content.get(&alice).unwrap(); let direct_rooms = event.content.get(&alice).unwrap();
assert!(direct_rooms.contains(&rooms[0])); assert!(direct_rooms.contains(&rooms[0]));
assert!(direct_rooms.contains(&rooms[1])); assert!(direct_rooms.contains(&rooms[1]));
match from_str::<collections::all::Event>(&json_data).unwrap() {
collections::all::Event::Direct(event) => {
assert_eq!(event.event_type, EventType::Direct);
let direct_rooms = event.content.get(&alice).unwrap();
assert!(direct_rooms.contains(&rooms[0]));
assert!(direct_rooms.contains(&rooms[1]));
}
_ => unreachable!(),
};
match from_str::<collections::only::Event>(&json_data).unwrap() {
collections::only::Event::Direct(event) => {
assert_eq!(event.event_type, EventType::Direct);
let direct_rooms = event.content.get(&alice).unwrap();
assert!(direct_rooms.contains(&rooms[0]));
assert!(direct_rooms.contains(&rooms[1]));
}
_ => unreachable!(),
};
} }
} }

View File

@ -1,84 +1,10 @@
//! Types for the *m.dummy* event. //! Types for the *m.dummy* event.
use std::{ use ruma_events_macros::ruma_event;
collections::HashMap,
fmt::{Formatter, Result as FmtResult},
};
use serde::{de::{Error, MapAccess, Visitor}, ser::{SerializeMap, SerializeStruct}, Deserialize, Deserializer, Serialize, Serializer}; use crate::Empty;
use crate::Event;
/// This event type is used to indicate new Olm sessions for end-to-end encryption.
///
/// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event.
///
/// The event does not have any content associated with it. The sending client is expected to
/// send a key share request shortly after this message, causing the receiving client to process
/// this *m.dummy* event as the most recent event and using the keyshare request to set up the
/// session. The keyshare request and *m.dummy* combination should result in the original
/// sending client receiving keys over the newly established session.
#[derive(Clone, Debug)]
pub struct DummyEvent {
/// The event's content.
pub content: DummyEventContent,
}
/// The payload for `DummyEvent`.
#[derive(Clone, Debug)]
pub struct DummyEventContent;
impl DummyEvent {
/// Attempt to create `Self` from parsing a string of JSON data.
pub fn from_str(json: &str) -> Result<Self, crate::InvalidEvent> {
serde_json::from_str::<raw::DummyEvent>(json)?;
Ok(Self {
content: DummyEventContent,
})
}
}
impl Serialize for DummyEvent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
let mut state = serializer.serialize_struct("DummyEvent", 2)?;
state.serialize_field("content", &self.content);
state.serialize_field("type", &self.event_type());
state.end()
}
}
impl crate::Event for DummyEvent {
/// The type of the event.
const EVENT_TYPE: crate::EventType = crate::EventType::Dummy;
/// The type of this event's `content` field.
type Content = DummyEventContent;
/// The event's content.
fn content(&self) -> &Self::Content {
&self.content
}
}
// This is necessary because the content is represented in JSON as an empty object.
impl Serialize for DummyEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
serializer.serialize_map(Some(0))?.end()
}
}
mod raw {
use super::*;
ruma_event! {
/// This event type is used to indicate new Olm sessions for end-to-end encryption. /// This event type is used to indicate new Olm sessions for end-to-end encryption.
/// ///
/// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event. /// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event.
@ -88,51 +14,24 @@ mod raw {
/// this *m.dummy* event as the most recent event and using the keyshare request to set up the /// this *m.dummy* event as the most recent event and using the keyshare request to set up the
/// session. The keyshare request and *m.dummy* combination should result in the original /// session. The keyshare request and *m.dummy* combination should result in the original
/// sending client receiving keys over the newly established session. /// sending client receiving keys over the newly established session.
#[derive(Clone, Debug, Deserialize)] DummyEvent {
pub struct DummyEvent { kind: Event,
/// The event's content. event_type: Dummy,
pub content: DummyEventContent, content_type_alias: {
} /// The payload for `DummyEvent`.
Empty
/// The payload for `DummyEvent`.
#[derive(Clone, Debug)]
pub struct DummyEventContent;
impl<'de> Deserialize<'de> for DummyEventContent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct EmptyMapVisitor;
impl <'de> Visitor<'de> for EmptyMapVisitor {
type Value = DummyEventContent;
fn expecting(&self, f: &mut Formatter) -> FmtResult {
write!(f, "an object/map")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>
{
Ok(DummyEventContent)
}
}
deserializer.deserialize_map(EmptyMapVisitor)
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{DummyEvent, DummyEventContent}; use super::{DummyEvent, Empty};
#[test] #[test]
fn serialization() { fn serialization() {
let dummy_event = DummyEvent { let dummy_event = DummyEvent {
content: DummyEventContent, content: Empty,
}; };
let actual = serde_json::to_string(&dummy_event).unwrap(); let actual = serde_json::to_string(&dummy_event).unwrap();

View File

@ -3,25 +3,100 @@
use std::collections::HashMap; use std::collections::HashMap;
use ruma_identifiers::UserId; use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
event! { use crate::{Empty, Event};
/// A list of users to ignore.
pub struct IgnoredUserListEvent(IgnoredUserListEventContent) {} /// A list of users to ignore.
#[derive(Clone, Debug, PartialEq)]
pub struct IgnoredUserListEvent {
/// The event's content.
pub content: IgnoredUserListEventContent,
} }
/// The payload of an `IgnoredUserListEvent`. /// The payload for `IgnoredUserListEvent`.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq)]
pub struct IgnoredUserListEventContent { pub struct IgnoredUserListEventContent {
/// A list of users to ignore. /// A list of users to ignore.
/// pub ignored_users: Vec<UserId>,
/// The values in the hash map are not meaningful. They are used to generate an empty JSON }
/// object to support the odd structure used by the Matrix specification:
/// impl IgnoredUserListEvent {
/// ```text /// Attempt to create `Self` from parsing a string of JSON data.
/// "@someone:example.org": {} pub fn from_str(json: &str) -> Result<Self, crate::InvalidEvent> {
/// ``` let raw = serde_json::from_str::<raw::IgnoredUserListEvent>(json)?;
pub ignored_users: HashMap<UserId, HashMap<(), ()>>,
Ok(Self {
content: IgnoredUserListEventContent {
ignored_users: raw.content.ignored_users.keys().cloned().collect(),
},
})
}
}
impl Serialize for IgnoredUserListEvent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
let mut state = serializer.serialize_struct("IgnoredUserListEvent", 2)?;
state.serialize_field("content", &self.content);
state.serialize_field("type", &self.event_type());
state.end()
}
}
impl crate::Event for IgnoredUserListEvent {
/// The type of the event.
const EVENT_TYPE: crate::EventType = crate::EventType::IgnoredUserList;
/// The type of this event's `content` field.
type Content = IgnoredUserListEventContent;
/// The event's content.
fn content(&self) -> &Self::Content {
&self.content
}
}
impl Serialize for IgnoredUserListEventContent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
let mut map = HashMap::new();
for user_id in &self.ignored_users {
map.insert(user_id.clone(), Empty);
}
let raw = raw::IgnoredUserListEventContent {
ignored_users: map,
};
raw.serialize(serializer)
}
}
mod raw {
use crate::Empty;
use super::*;
/// A list of users to ignore.
#[derive(Clone, Debug, Deserialize)]
pub struct IgnoredUserListEvent {
/// The event's content.
pub content: IgnoredUserListEventContent,
}
/// The payload for `IgnoredUserListEvent`.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct IgnoredUserListEventContent {
/// A list of users to ignore.
pub ignored_users: HashMap<UserId, Empty>,
}
} }
#[cfg(test)] #[cfg(test)]
@ -30,40 +105,33 @@ mod tests {
use ruma_identifiers::UserId; use ruma_identifiers::UserId;
use super::IgnoredUserListEventContent; use super::{IgnoredUserListEvent, IgnoredUserListEventContent};
#[test] #[test]
fn serialize_to_empty_json_object() { fn serialization() {
let mut ignored_user_list_event_content = IgnoredUserListEventContent { let ignored_user_list_event = IgnoredUserListEvent {
ignored_users: HashMap::new(), content: IgnoredUserListEventContent {
ignored_users: vec![UserId::try_from("@carl:example.com").unwrap()],
},
}; };
let user_id = UserId::try_from("@carl:example.com").unwrap(); let json = serde_json::to_string(&ignored_user_list_event).unwrap();
ignored_user_list_event_content assert_eq!(json, r#"{"content":{"ignored_users":{"@carl:example.com":{}}},"type":"m.ignored_user_list"}"#);
.ignored_users
.insert(user_id, HashMap::new());
let json = serde_json::to_string(&ignored_user_list_event_content).unwrap();
assert_eq!(json, r#"{"ignored_users":{"@carl:example.com":{}}}"#);
} }
#[test] #[test]
fn deserialize_from_empty_json_object() { fn deserialization() {
let json = r#"{"ignored_users":{"@carl:example.com":{}}}"#; let json = r#"{"content":{"ignored_users":{"@carl:example.com":{}}},"type":"m.ignored_user_list"}"#;
let ignored_user_list_event_content: IgnoredUserListEventContent = let actual = IgnoredUserListEvent::from_str(json).unwrap();
serde_json::from_str(&json).unwrap();
let mut expected = IgnoredUserListEventContent { let expected = IgnoredUserListEvent {
ignored_users: HashMap::new(), content: IgnoredUserListEventContent {
ignored_users: vec![UserId::try_from("@carl:example.com").unwrap()],
},
}; };
let user_id = UserId::try_from("@carl:example.com").unwrap(); assert_eq!(actual, expected);
expected.ignored_users.insert(user_id, HashMap::new());
assert_eq!(ignored_user_list_event_content, expected);
} }
} }

View File

@ -107,7 +107,8 @@ use std::{
use js_int::UInt; use js_int::UInt;
use ruma_identifiers::{EventId, RoomId, UserId}; use ruma_identifiers::{EventId, RoomId, UserId};
use serde::{ use serde::{
de::{Error as SerdeError, IntoDeserializer, Visitor}, de::{Error as SerdeError, IntoDeserializer, MapAccess, Visitor},
ser::SerializeMap,
Deserialize, Deserializer, Serialize, Serializer, Deserialize, Deserializer, Serialize, Serializer,
}; };
use serde_json::Value; use serde_json::Value;
@ -121,22 +122,22 @@ mod macros;
// pub mod all; // pub mod all;
// pub mod only; // pub mod only;
// } // }
// pub mod direct; pub mod direct;
pub mod dummy; pub mod dummy;
pub mod forwarded_room_key; pub mod forwarded_room_key;
// pub mod fully_read; // pub mod fully_read;
// pub mod ignored_user_list; pub mod ignored_user_list;
// pub mod key; // pub mod key;
pub mod presence; pub mod presence;
// pub mod push_rules; // pub mod push_rules;
// pub mod receipt; pub mod receipt;
pub mod room; pub mod room;
// pub mod room_key; // pub mod room_key;
pub mod room_key_request; pub mod room_key_request;
pub mod sticker; pub mod sticker;
// pub mod stripped; // pub mod stripped;
// pub mod tag; pub mod tag;
// pub mod typing; pub mod typing;
/// An event that is malformed or otherwise invalid. /// An event that is malformed or otherwise invalid.
/// ///
@ -212,6 +213,48 @@ impl Display for FromStrError {
impl Error for FromStrError {} impl Error for FromStrError {}
/// A meaningless value that serializes to an empty JSON object.
///
/// This type is used in a few places where the Matrix specification requires an empty JSON object,
/// but it's wasteful to represent it as a `HashMap` in Rust code.
#[derive(Clone, Debug, PartialEq)]
pub struct Empty;
impl Serialize for Empty {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
serializer.serialize_map(Some(0))?.end()
}
}
impl<'de> Deserialize<'de> for Empty {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct EmptyMapVisitor;
impl <'de> Visitor<'de> for EmptyMapVisitor {
type Value = Empty;
fn expecting(&self, f: &mut Formatter) -> FmtResult {
write!(f, "an object/map")
}
fn visit_map<A>(self, _map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>
{
Ok(Empty)
}
}
deserializer.deserialize_map(EmptyMapVisitor)
}
}
/// The type of an event. /// The type of an event.
#[derive(Clone, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum EventType { pub enum EventType {

View File

@ -23,193 +23,3 @@ macro_rules! impl_enum {
} }
} }
} }
macro_rules! event {
( $(#[$attr:meta])*
pub struct $name:ident($content_type:ty) {
$(
$(#[$field_attr:meta])*
pub $field_name:ident: $field_type:ty
),*
}
) => {
$(#[$attr])*
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct $name {
/// The event's content.
pub content: $content_type,
/// The type of the event.
#[serde(rename = "type")]
pub event_type: $crate::EventType,
$(
$(#[$field_attr])*
pub $field_name: $field_type
),*
}
impl_event!($name, $content_type);
}
}
macro_rules! impl_event {
($name:ident, $content_type:ty) => {
impl $crate::Event for $name {
type Content = $content_type;
fn content(&self) -> &<$name as $crate::Event>::Content {
&self.content
}
fn event_type(&self) -> &$crate::EventType {
&self.event_type
}
}
};
}
macro_rules! room_event {
( $(#[$attr:meta])*
pub struct $name:ident($content_type:ty) {
$(
$(#[$field_attr:meta])*
pub $field_name:ident: $field_type:ty
),*
}
) => {
$(#[$attr])*
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct $name {
/// The event's content.
pub content: $content_type,
/// The unique identifier for the event.
pub event_id: ::ruma_identifiers::EventId,
/// The type of the event.
#[serde(rename = "type")]
pub event_type: $crate::EventType,
/// Timestamp (milliseconds since the UNIX epoch) on originating homeserver when this
/// event was sent.
pub origin_server_ts: UInt,
/// The unique identifier for the room associated with this event.
#[serde(skip_serializing_if="Option::is_none")]
pub room_id: Option<::ruma_identifiers::RoomId>,
/// Additional key-value pairs not signed by the homeserver.
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned: Option<::serde_json::Value>,
/// The unique identifier for the user who sent this event.
pub sender: ::ruma_identifiers::UserId,
$(
$(#[$field_attr])*
pub $field_name: $field_type
),*
}
impl_room_event!($name, $content_type);
}
}
macro_rules! impl_room_event {
($name:ident, $content_type:ty) => {
impl_event!($name, $content_type);
impl $crate::RoomEvent for $name {
fn event_id(&self) -> &::ruma_identifiers::EventId {
&self.event_id
}
fn origin_server_ts(&self) -> UInt {
self.origin_server_ts
}
fn room_id(&self) -> Option<&::ruma_identifiers::RoomId> {
self.room_id.as_ref()
}
fn unsigned(&self) -> Option<&::serde_json::Value> {
self.unsigned.as_ref()
}
fn sender(&self) -> &::ruma_identifiers::UserId {
&self.sender
}
}
};
}
macro_rules! state_event {
( $(#[$attr:meta])*
pub struct $name:ident($content_type:ty) {
$(
$(#[$field_attr:meta])*
pub $field_name:ident: $field_type:ty
),*
}
) => {
$(#[$attr])*
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct $name {
/// The event's content.
pub content: $content_type,
/// The unique identifier for the event.
pub event_id: ::ruma_identifiers::EventId,
/// The type of the event.
#[serde(rename = "type")]
pub event_type: $crate::EventType,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: UInt,
/// The previous content for this state key, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub prev_content: Option<$content_type>,
/// The unique identifier for the room associated with this event.
#[serde(skip_serializing_if="Option::is_none")]
pub room_id: Option<::ruma_identifiers::RoomId>,
/// A key that determines which piece of room state the event represents.
pub state_key: String,
/// Additional key-value pairs not signed by the homeserver.
#[serde(skip_serializing_if = "Option::is_none")]
pub unsigned: Option<::serde_json::Value>,
/// The unique identifier for the user associated with this event.
pub sender: ::ruma_identifiers::UserId,
$(
$(#[$field_attr])*
pub $field_name: $field_type
),*
}
impl_state_event!($name, $content_type);
}
}
macro_rules! impl_state_event {
($name:ident, $content_type:ty) => {
impl_room_event!($name, $content_type);
impl $crate::StateEvent for $name {
fn prev_content(&self) -> Option<&Self::Content> {
self.prev_content.as_ref()
}
fn state_key(&self) -> &str {
&self.state_key
}
}
};
}

View File

@ -77,10 +77,9 @@ mod tests {
use js_int::UInt; use js_int::UInt;
use ruma_identifiers::UserId; use ruma_identifiers::UserId;
use serde_json::{from_str, to_string}; use serde_json::to_string;
use super::{PresenceEvent, PresenceEventContent, PresenceState}; use super::{PresenceEvent, PresenceEventContent, PresenceState};
use crate::EventType;
/// Test serialization and deserialization of example m.presence event from the spec /// Test serialization and deserialization of example m.presence event from the spec
/// https://github.com/turt2live/matrix-doc/blob/master/event-schemas/examples/m.presence /// https://github.com/turt2live/matrix-doc/blob/master/event-schemas/examples/m.presence

View File

@ -3,23 +3,29 @@
use std::collections::HashMap; use std::collections::HashMap;
use js_int::UInt; use js_int::UInt;
use ruma_events_macros::ruma_event;
use ruma_identifiers::{EventId, RoomId, UserId}; use ruma_identifiers::{EventId, RoomId, UserId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
event! { ruma_event! {
/// Informs the client of new receipts. /// Informs the client of new receipts.
pub struct ReceiptEvent(ReceiptEventContent) { ReceiptEvent {
/// The unique identifier for the room associated with this event. kind: Event,
pub room_id: RoomId event_type: Receipt,
fields: {
/// The unique identifier for the room associated with this event.
pub room_id: RoomId,
},
content_type_alias: {
/// The payload for `ReceiptEvent`.
///
/// A mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of
/// the event being acknowledged and *not* an ID for the receipt itself.
HashMap<EventId, Receipts>
},
} }
} }
/// The payload of a `ReceiptEvent`.
///
/// A mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of
/// the event being acknowledged and *not* an ID for the receipt itself.
pub type ReceiptEventContent = HashMap<EventId, Receipts>;
/// A collection of receipts. /// A collection of receipts.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Receipts { pub struct Receipts {

View File

@ -2,18 +2,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use ruma_events_macros::ruma_event;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
event! { ruma_event! {
/// Informs the client of tags on a room. /// Informs the client of tags on a room.
pub struct TagEvent(TagEventContent) {} TagEvent {
} kind: Event,
event_type: Tag,
/// The payload of a `TagEvent`. content: {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] /// A map of tag names to tag info.
pub struct TagEventContent { pub tags: HashMap<String, TagInfo>,
/// A map of tag names to tag info. },
pub tags: HashMap<String, TagInfo>, }
} }
/// Information about a tag. /// Information about a tag.

View File

@ -1,19 +1,20 @@
//! Types for the *m.typing* event. //! Types for the *m.typing* event.
use ruma_events_macros::ruma_event;
use ruma_identifiers::{RoomId, UserId}; use ruma_identifiers::{RoomId, UserId};
use serde::{Deserialize, Serialize};
event! { ruma_event! {
/// Informs the client of the list of users currently typing. /// Informs the client of the list of users currently typing.
pub struct TypingEvent(TypingEventContent) { TypingEvent {
/// The unique identifier for the room associated with this event. kind: Event,
pub room_id: RoomId event_type: Typing,
fields: {
/// The unique identifier for the room associated with this event.
pub room_id: RoomId,
},
content: {
/// The list of user IDs typing in this room, if any.
pub user_ids: Vec<UserId>,
},
} }
} }
/// The payload of a `TypingEvent`.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TypingEventContent {
/// The list of user IDs typing in this room, if any.
pub user_ids: Vec<UserId>,
}