use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::{
endpoints::{location::position_xyz_object, shop::ShopKind},
error::{Error, Result},
model::{
location::PositionXYZ, named_uuid::NamedUuid, timestamp::Timestamp,
},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EventKind {
NewDay,
NationCreated,
NationDeleted,
NationRenamed,
NationKingChanged,
NationMerged,
TownCreated,
TownDeleted,
TownRenamed,
TownMayorChanged,
TownMerged,
TownRuined,
TownReclaimed,
TownJoinedNation,
TownLeftNation,
ResidentJoinedTown,
ResidentLeftTown,
ShopSoldItem,
ShopBoughtItem,
ShopOutOfStock,
ShopOutOfSpace,
ShopOutOfGold,
}
impl EventKind {
pub const fn all() -> &'static [Self] {
&[
Self::NewDay,
Self::NationCreated,
Self::NationDeleted,
Self::NationRenamed,
Self::NationKingChanged,
Self::NationMerged,
Self::TownCreated,
Self::TownDeleted,
Self::TownRenamed,
Self::TownMayorChanged,
Self::TownMerged,
Self::TownRuined,
Self::TownReclaimed,
Self::TownJoinedNation,
Self::TownLeftNation,
Self::ResidentJoinedTown,
Self::ResidentLeftTown,
Self::ShopSoldItem,
Self::ShopBoughtItem,
Self::ShopOutOfStock,
Self::ShopOutOfSpace,
Self::ShopOutOfGold,
]
}
pub const fn as_str(self) -> &'static str {
match self {
Self::NewDay => "NewDay",
Self::NationCreated => "NationCreated",
Self::NationDeleted => "NationDeleted",
Self::NationRenamed => "NationRenamed",
Self::NationKingChanged => "NationKingChanged",
Self::NationMerged => "NationMerged",
Self::TownCreated => "TownCreated",
Self::TownDeleted => "TownDeleted",
Self::TownRenamed => "TownRenamed",
Self::TownMayorChanged => "TownMayorChanged",
Self::TownMerged => "TownMerged",
Self::TownRuined => "TownRuined",
Self::TownReclaimed => "TownReclaimed",
Self::TownJoinedNation => "TownJoinedNation",
Self::TownLeftNation => "TownLeftNation",
Self::ResidentJoinedTown => "ResidentJoinedTown",
Self::ResidentLeftTown => "ResidentLeftTown",
Self::ShopSoldItem => "ShopSoldItem",
Self::ShopBoughtItem => "ShopBoughtItem",
Self::ShopOutOfStock => "ShopOutOfStock",
Self::ShopOutOfSpace => "ShopOutOfSpace",
Self::ShopOutOfGold => "ShopOutOfGold",
}
}
}
impl fmt::Display for EventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for EventKind {
type Err = Error;
fn from_str(value: &str) -> Result<Self> {
match value {
"NewDay" => Ok(Self::NewDay),
"NationCreated" => Ok(Self::NationCreated),
"NationDeleted" => Ok(Self::NationDeleted),
"NationRenamed" => Ok(Self::NationRenamed),
"NationKingChanged" => Ok(Self::NationKingChanged),
"NationMerged" => Ok(Self::NationMerged),
"TownCreated" => Ok(Self::TownCreated),
"TownDeleted" => Ok(Self::TownDeleted),
"TownRenamed" => Ok(Self::TownRenamed),
"TownMayorChanged" => Ok(Self::TownMayorChanged),
"TownMerged" => Ok(Self::TownMerged),
"TownRuined" => Ok(Self::TownRuined),
"TownReclaimed" => Ok(Self::TownReclaimed),
"TownJoinedNation" => Ok(Self::TownJoinedNation),
"TownLeftNation" => Ok(Self::TownLeftNation),
"ResidentJoinedTown" => Ok(Self::ResidentJoinedTown),
"ResidentLeftTown" => Ok(Self::ResidentLeftTown),
"ShopSoldItem" => Ok(Self::ShopSoldItem),
"ShopBoughtItem" => Ok(Self::ShopBoughtItem),
"ShopOutOfStock" => Ok(Self::ShopOutOfStock),
"ShopOutOfSpace" => Ok(Self::ShopOutOfSpace),
"ShopOutOfGold" => Ok(Self::ShopOutOfGold),
other => Err(Error::UnknownSseEventType(other.to_owned())),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Control(ControlEvent),
NewDay(Timestamped),
NationCreated(NationCreatedOrDeleted),
NationDeleted(NationCreatedOrDeleted),
NationRenamed(NationRenamed),
NationKingChanged(NationKingChanged),
NationMerged(NationMerged),
TownCreated(TownCreatedOrDeleted),
TownDeleted(TownCreatedOrDeleted),
TownRenamed(TownRenamed),
TownMayorChanged(TownMayorChanged),
TownMerged(TownMerged),
TownRuined(TownRuined),
TownReclaimed(TownReclaimed),
TownJoinedNation(TownNationChanged),
TownLeftNation(TownNationChanged),
ResidentJoinedTown(ResidentTownChanged),
ResidentLeftTown(ResidentTownChanged),
ShopSoldItem(ShopSoldItem),
ShopBoughtItem(ShopBoughtItem),
ShopOutOfStock(ShopUnavailable),
ShopOutOfSpace(ShopUnavailable),
ShopOutOfGold(Timestamped),
}
impl Event {
pub const fn kind(&self) -> Option<EventKind> {
match self {
Self::Control(_) => None,
Self::NewDay(_) => Some(EventKind::NewDay),
Self::NationCreated(_) => Some(EventKind::NationCreated),
Self::NationDeleted(_) => Some(EventKind::NationDeleted),
Self::NationRenamed(_) => Some(EventKind::NationRenamed),
Self::NationKingChanged(_) => Some(EventKind::NationKingChanged),
Self::NationMerged(_) => Some(EventKind::NationMerged),
Self::TownCreated(_) => Some(EventKind::TownCreated),
Self::TownDeleted(_) => Some(EventKind::TownDeleted),
Self::TownRenamed(_) => Some(EventKind::TownRenamed),
Self::TownMayorChanged(_) => Some(EventKind::TownMayorChanged),
Self::TownMerged(_) => Some(EventKind::TownMerged),
Self::TownRuined(_) => Some(EventKind::TownRuined),
Self::TownReclaimed(_) => Some(EventKind::TownReclaimed),
Self::TownJoinedNation(_) => Some(EventKind::TownJoinedNation),
Self::TownLeftNation(_) => Some(EventKind::TownLeftNation),
Self::ResidentJoinedTown(_) => Some(EventKind::ResidentJoinedTown),
Self::ResidentLeftTown(_) => Some(EventKind::ResidentLeftTown),
Self::ShopSoldItem(_) => Some(EventKind::ShopSoldItem),
Self::ShopBoughtItem(_) => Some(EventKind::ShopBoughtItem),
Self::ShopOutOfStock(_) => Some(EventKind::ShopOutOfStock),
Self::ShopOutOfSpace(_) => Some(EventKind::ShopOutOfSpace),
Self::ShopOutOfGold(_) => Some(EventKind::ShopOutOfGold),
}
}
}
pub(crate) fn parse_event(kind: EventKind, data: &str) -> Result<Event> {
Ok(match kind {
EventKind::NewDay => Event::NewDay(parse(data)?),
EventKind::NationCreated => Event::NationCreated(parse(data)?),
EventKind::NationDeleted => Event::NationDeleted(parse(data)?),
EventKind::NationRenamed => Event::NationRenamed(parse(data)?),
EventKind::NationKingChanged => Event::NationKingChanged(parse(data)?),
EventKind::NationMerged => Event::NationMerged(parse(data)?),
EventKind::TownCreated => Event::TownCreated(parse(data)?),
EventKind::TownDeleted => Event::TownDeleted(parse(data)?),
EventKind::TownRenamed => Event::TownRenamed(parse(data)?),
EventKind::TownMayorChanged => Event::TownMayorChanged(parse(data)?),
EventKind::TownMerged => Event::TownMerged(parse(data)?),
EventKind::TownRuined => Event::TownRuined(parse(data)?),
EventKind::TownReclaimed => Event::TownReclaimed(parse(data)?),
EventKind::TownJoinedNation => Event::TownJoinedNation(parse(data)?),
EventKind::TownLeftNation => Event::TownLeftNation(parse(data)?),
EventKind::ResidentJoinedTown => {
Event::ResidentJoinedTown(parse(data)?)
}
EventKind::ResidentLeftTown => Event::ResidentLeftTown(parse(data)?),
EventKind::ShopSoldItem => Event::ShopSoldItem(parse(data)?),
EventKind::ShopBoughtItem => Event::ShopBoughtItem(parse(data)?),
EventKind::ShopOutOfStock => Event::ShopOutOfStock(parse(data)?),
EventKind::ShopOutOfSpace => Event::ShopOutOfSpace(parse(data)?),
EventKind::ShopOutOfGold => Event::ShopOutOfGold(parse(data)?),
})
}
pub(crate) fn parse_sse_event(block: &str) -> Result<Option<Event>> {
let mut kind = None;
let mut data = String::new();
for line in block.lines() {
let line = line.strip_suffix('\r').unwrap_or(line);
if line.is_empty() || line.starts_with(':') {
continue;
}
let (field, value) = line.split_once(':').unwrap_or((line, ""));
let value = value.strip_prefix(' ').unwrap_or(value);
match field {
"event" => kind = Some(value.to_owned()),
"data" => {
if !data.is_empty() {
data.push('\n');
}
data.push_str(value);
}
_ => {}
}
}
if kind.is_none() && data.is_empty() {
return Ok(None);
}
let kind = kind.ok_or(Error::MissingSseEventType)?;
if data.is_empty() {
return Err(Error::MissingSseData);
}
match kind.as_str() {
"open" => parse_control_message(&data)
.map(ControlEvent::Open)
.map(Event::Control)
.map(Some),
"error" => parse_control_message(&data)
.map(ControlEvent::Error)
.map(Event::Control)
.map(Some),
"close" => parse_control_message(&data)
.map(ControlEvent::Close)
.map(Event::Control)
.map(Some),
"listening" => parse_listening(&data)
.map(ControlEvent::Listening)
.map(Event::Control)
.map(Some),
_ => parse_event(kind.parse()?, &data).map(Some),
}
}
fn parse<T: DeserializeOwned>(data: &str) -> Result<T> {
serde_json::from_str(data).map_err(Into::into)
}
fn parse_control_message(data: &str) -> Result<ControlMessage> {
parse(data)
}
fn parse_listening(data: &str) -> Result<Listening> {
let raw: RawListening = parse(data)?;
let valid = raw
.valid
.into_iter()
.map(|event| event.parse())
.collect::<Result<Vec<_>>>()?;
Ok(Listening {
valid,
invalid: raw.invalid,
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum ControlEvent {
Open(ControlMessage),
Listening(Listening),
Error(ControlMessage),
Close(ControlMessage),
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct ControlMessage {
pub message: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Listening {
pub valid: Vec<EventKind>,
pub invalid: Vec<String>,
}
#[derive(Deserialize)]
struct RawListening {
valid: Vec<String>,
invalid: Vec<String>,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Timestamped {
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NationCreatedOrDeleted {
pub nation: NamedUuid,
pub king: NamedUuid,
pub capital: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NationRenamed {
pub nation: NamedUuid,
pub old_name: String,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NationKingChanged {
pub nation: NamedUuid,
pub new_king: NamedUuid,
pub old_king: NamedUuid,
pub is_capital_change: bool,
pub new_capital: Option<NamedUuid>,
pub old_capital: Option<NamedUuid>,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NationMerged {
pub old_nation: NamedUuid,
pub remaining_nation: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownCreatedOrDeleted {
pub town: NamedUuid,
pub mayor: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownRenamed {
pub town: NamedUuid,
pub old_name: String,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownMayorChanged {
pub town: NamedUuid,
pub new_mayor: NamedUuid,
pub old_mayor: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownMerged {
pub old_town: NamedUuid,
pub remaining_town: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownRuined {
pub town: NamedUuid,
pub old_mayor: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownReclaimed {
pub town: NamedUuid,
pub new_mayor: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TownNationChanged {
pub nation: NamedUuid,
pub town: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResidentTownChanged {
pub town: NamedUuid,
pub resident: NamedUuid,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ShopSoldItem {
pub shop: EventShop,
pub buyer: String,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ShopBoughtItem {
pub shop: EventShop,
pub seller: String,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ShopUnavailable {
pub action: ShopUnavailableAction,
pub shop: EventShop,
#[cfg_attr(
feature = "chrono",
serde(with = "crate::model::timestamp::seconds")
)]
pub timestamp: Timestamp,
}
#[derive(Clone, Copy, Deserialize, Serialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ShopUnavailableAction {
OutOfStock,
OutOfSpace,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct EventShop {
pub item: String,
pub price: f64,
pub amount: u32,
#[serde(rename = "type")]
pub kind: ShopKind,
pub stock: u32,
#[serde(default, with = "optional_position_xyz_object")]
pub location: Option<PositionXYZ>,
}
mod optional_position_xyz_object {
use serde::{Deserialize, Serialize};
use super::{PositionXYZ, position_xyz_object};
pub fn serialize<S>(
position: &Option<PositionXYZ>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match position {
Some(position) => {
position_xyz_object::serialize(position, serializer)
}
None => Option::<PositionXYZ>::None.serialize(serializer),
}
}
pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<PositionXYZ>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<position_xyz_object::PositionXYZObject>::deserialize(
deserializer,
)
.map(|position| {
position.map(|position| PositionXYZ {
x: position.x,
y: position.y,
z: position.z,
})
})
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::endpoints::events::{
ControlEvent, Event, EventKind, ShopUnavailableAction, parse_event,
parse_sse_event,
};
#[cfg(feature = "chrono")]
fn timestamp_seconds(timestamp: crate::model::timestamp::Timestamp) -> i64 {
timestamp.timestamp()
}
#[cfg(not(feature = "chrono"))]
fn timestamp_seconds(timestamp: crate::model::timestamp::Timestamp) -> i64 {
timestamp
}
#[test]
fn parses_typed_town_event() {
let event = parse_event(
EventKind::TownRenamed,
&json!({
"town": {
"name": "New_Paris",
"uuid": "d6a45111-f95b-4ac2-ba52-07e8e449a622"
},
"oldName": "Paris",
"timestamp": 1774520132
})
.to_string(),
)
.unwrap();
let Event::TownRenamed(payload) = event else {
panic!("unexpected event variant");
};
assert_eq!(payload.old_name, "Paris");
assert_eq!(timestamp_seconds(payload.timestamp), 1_774_520_132);
}
#[test]
fn parses_shop_event_without_location() {
let event = parse_event(
EventKind::ShopOutOfStock,
&json!({
"action": "out_of_stock",
"shop": {
"item": "TOTEM_OF_UNDYING",
"price": 150.0,
"amount": 1,
"type": "selling",
"stock": 3
},
"timestamp": 1774520132
})
.to_string(),
)
.unwrap();
let Event::ShopOutOfStock(payload) = event else {
panic!("unexpected event variant");
};
assert_eq!(payload.action, ShopUnavailableAction::OutOfStock);
assert_eq!(payload.shop.location, None);
}
#[test]
fn parses_sse_frame_with_event_and_data() {
let event = parse_sse_event(
r#"event: NewDay
data: {"timestamp":1774520132}
"#,
)
.unwrap()
.unwrap();
assert_eq!(event.kind(), Some(EventKind::NewDay));
}
#[test]
fn parses_open_control_frame() {
let event = parse_sse_event(
r#"event: open
data: {"message":"Connected to the EarthMC API."}
"#,
)
.unwrap()
.unwrap();
let Event::Control(ControlEvent::Open(message)) = event else {
panic!("unexpected event variant");
};
assert_eq!(message.message, "Connected to the EarthMC API.");
}
#[test]
fn parses_listening_control_frame() {
let event = parse_sse_event(
r#"event: listening
data: {"valid":["NewDay","TownDeleted"],"invalid":["Nope"]}
"#,
)
.unwrap()
.unwrap();
let Event::Control(ControlEvent::Listening(listening)) = event else {
panic!("unexpected event variant");
};
assert_eq!(
listening.valid,
[EventKind::NewDay, EventKind::TownDeleted]
);
assert_eq!(listening.invalid, ["Nope"]);
}
#[test]
fn parses_error_and_close_control_frames() {
for (frame, expected) in [
(
r#"event: error
data: {"message":"Missing API key"}
"#,
"Missing API key",
),
(
r#"event: close
data: {"message":"EarthMC API shutting down."}
"#,
"EarthMC API shutting down.",
),
] {
let event = parse_sse_event(frame).unwrap().unwrap();
match event {
Event::Control(ControlEvent::Error(message))
| Event::Control(ControlEvent::Close(message)) => {
assert_eq!(message.message, expected);
}
_ => panic!("unexpected event variant"),
}
}
}
#[test]
fn all_event_kinds_round_trip_through_strings() {
assert_eq!(EventKind::all().len(), 22);
for kind in EventKind::all() {
assert_eq!(kind.as_str().parse::<EventKind>().unwrap(), *kind);
}
}
}