use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::Rich;
pub const RICH_KEY: &str = "com.ianm.mtrix";
pub const RICH_KEY_NEW: &str = "com.talaagent.mxblocks";
pub const RICH_KEYS: &[&str] = &[RICH_KEY_NEW, RICH_KEY];
pub const USED_KEYS: &[&str] = &["com.talaagent.mxblocks.used", "com.ianm.mtrix.used"];
pub const ALBUM_KEYS: &[&str] = &["com.talaagent.mxblocks.album", "com.ianm.mtrix.album"];
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WriteKey {
#[default]
Legacy,
New,
#[serde(untagged)]
Custom(String),
}
impl WriteKey {
pub fn rich_key(&self) -> &str {
match self {
WriteKey::Legacy => RICH_KEY,
WriteKey::New => RICH_KEY_NEW,
WriteKey::Custom(key) => key,
}
}
pub fn used_key(&self) -> String {
format!("{}.used", self.rich_key())
}
pub fn album_key(&self) -> String {
format!("{}.album", self.rich_key())
}
pub fn write_rich(&self, content: &mut Map<String, Value>, rich: &Rich) {
strip_rich(content);
content.insert(
self.rich_key().to_owned(),
serde_json::to_value(rich).unwrap_or(Value::Null),
);
}
pub fn write_used(&self, content: &mut Map<String, Value>, used: Value) {
for k in USED_KEYS {
content.remove(*k);
}
content.insert(self.used_key().to_owned(), used);
}
pub fn write_album(&self, content: &mut Map<String, Value>, album: Value) {
for k in ALBUM_KEYS {
content.remove(*k);
}
content.insert(self.album_key().to_owned(), album);
}
}
fn first_of<'a>(
content: &'a Map<String, Value>,
keys: &[&'static str],
) -> Option<(&'static str, &'a Value)> {
keys.iter().find_map(|k| content.get(*k).map(|v| (*k, v)))
}
pub fn rich_value(content: &Map<String, Value>) -> Option<(&'static str, &Value)> {
first_of(content, RICH_KEYS)
}
pub fn read_rich(content: &Map<String, Value>) -> Option<Rich> {
RICH_KEYS
.iter()
.filter_map(|k| content.get(*k))
.find_map(|v| serde_json::from_value(v.clone()).ok())
}
pub fn read_used(content: &Map<String, Value>) -> Option<&Value> {
first_of(content, USED_KEYS).map(|(_, v)| v)
}
pub fn read_album(content: &Map<String, Value>) -> Option<&Value> {
first_of(content, ALBUM_KEYS).map(|(_, v)| v)
}
pub fn strip_rich(content: &mut Map<String, Value>) -> bool {
let mut any = false;
for k in RICH_KEYS {
any |= content.remove(*k).is_some();
}
any
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentKeys {
write: WriteKey,
read: Vec<String>,
}
impl Default for ContentKeys {
fn default() -> Self {
Self {
write: WriteKey::default(),
read: RICH_KEYS.iter().map(|key| (*key).to_owned()).collect(),
}
}
}
impl ContentKeys {
pub fn new(key: impl Into<String>) -> Self {
let key = key.into();
Self {
write: WriteKey::Custom(key.clone()),
read: vec![key],
}
}
pub fn with_read_aliases<I, S>(mut self, aliases: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for alias in aliases {
let alias = alias.into();
if !self.read.contains(&alias) {
self.read.push(alias);
}
}
self
}
pub fn writer(&self) -> &WriteKey {
&self.write
}
pub fn read_keys(&self) -> &[String] {
&self.read
}
pub fn rich_value<'k, 'c>(
&'k self,
content: &'c Map<String, Value>,
) -> Option<(&'k str, &'c Value)> {
self.read
.iter()
.find_map(|key| content.get(key).map(|v| (key.as_str(), v)))
}
pub fn read_rich(&self, content: &Map<String, Value>) -> Option<Rich> {
self.read
.iter()
.filter_map(|key| content.get(key))
.find_map(|value| serde_json::from_value(value.clone()).ok())
}
pub fn read_used<'c>(&self, content: &'c Map<String, Value>) -> Option<&'c Value> {
self.read
.iter()
.find_map(|key| content.get(&format!("{key}.used")))
}
pub fn read_album<'c>(&self, content: &'c Map<String, Value>) -> Option<&'c Value> {
self.read
.iter()
.find_map(|key| content.get(&format!("{key}.album")))
}
pub fn strip_rich(&self, content: &mut Map<String, Value>) -> bool {
let mut removed = false;
for key in &self.read {
removed |= content.remove(key).is_some();
}
removed
}
pub fn write_rich(&self, content: &mut Map<String, Value>, rich: &Rich) {
self.strip_rich(content);
content.insert(
self.write.rich_key().to_owned(),
serde_json::to_value(rich).unwrap_or(Value::Null),
);
}
pub fn write_used(&self, content: &mut Map<String, Value>, used: Value) {
for key in &self.read {
content.remove(&format!("{key}.used"));
}
content.insert(self.write.used_key(), used);
}
pub fn write_album(&self, content: &mut Map<String, Value>, album: Value) {
for key in &self.read {
content.remove(&format!("{key}.album"));
}
content.insert(self.write.album_key(), album);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Button, Style};
use serde_json::json;
fn rich() -> Rich {
Rich::buttons(vec![Button::react("OK", Style::Default, "👍")])
}
#[test]
fn custom_namespaces_read_aliases_and_remove_stale_values_on_edit() {
let keys = ContentKeys::new("org.example.blocks")
.with_read_aliases(["org.example.old", "org.example.old"]);
let value = rich();
let mut content = Map::new();
content.insert(
"org.example.old".into(),
serde_json::to_value(&value).unwrap(),
);
content.insert("org.example.unrelated".into(), json!(true));
assert_eq!(keys.read_rich(&content), Some(value.clone()));
assert_eq!(read_rich(&content), None);
keys.write_rich(&mut content, &value);
assert!(!content.contains_key("org.example.old"));
assert_eq!(content["org.example.unrelated"], json!(true));
assert_eq!(keys.rich_value(&content).unwrap().0, "org.example.blocks");
assert_eq!(keys.read_keys().len(), 2);
}
#[test]
fn custom_reply_and_album_keys_are_consistent() {
let keys = ContentKeys::new("org.example.blocks").with_read_aliases(["org.example.old"]);
let mut content = Map::new();
content.insert("org.example.old.used".into(), json!({"id": "old"}));
content.insert("org.example.old.album".into(), json!({"index": 2}));
assert_eq!(keys.read_used(&content), Some(&json!({"id": "old"})));
assert_eq!(keys.read_album(&content), Some(&json!({"index": 2})));
keys.write_used(&mut content, json!({"id": "new"}));
keys.write_album(&mut content, json!({"index": 3}));
assert!(!content.contains_key("org.example.old.used"));
assert!(!content.contains_key("org.example.old.album"));
assert_eq!(keys.read_used(&content), Some(&json!({"id": "new"})));
assert_eq!(keys.read_album(&content), Some(&json!({"index": 3})));
assert_eq!(read_used(&content), None);
assert_eq!(read_album(&content), None);
}
#[test]
fn custom_writer_round_trips_in_configuration() {
let key = WriteKey::Custom("org.example.blocks".into());
let encoded = serde_json::to_value(&key).unwrap();
assert_eq!(encoded, json!("org.example.blocks"));
assert_eq!(serde_json::from_value::<WriteKey>(encoded).unwrap(), key);
}
}