feat(core): Add error types, config, and data model for items and events.

This commit is contained in:
2026-03-13 21:55:11 -04:00
parent de0431778f
commit 9ed8e898a5
7 changed files with 324 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
//! The core data unit. An [`Item`] is a single entry in the
//! menu, either a plain-text string or a JSON object with
//! structured fields.
use serde_json::Value;
/// A menu entry wrapping a JSON value. Plain text is stored
/// as `Value::String`, structured entries as
/// `Value::Object`. The label is extracted and cached at
/// construction time so display calls are just a pointer
/// dereference.
///
/// Serializes as the inner Value (transparent for output)
/// but construction always requires label extraction.
#[derive(Debug, Clone)]
pub struct Item {
pub value: Value,
label_cache: String,
}
impl Item {
/// Create an Item from a JSON value, extracting the display
/// label from the given key. String values use the string
/// itself. Object values look up the key.
pub fn new(value: Value, label_key: &str) -> Self {
let label_cache = extract_label(&value, label_key).to_string();
Self { value, label_cache }
}
/// Wrap a plain-text string as an Item. Stored internally
/// as `Value::String` with the label cached.
pub fn from_plain_text(line: &str) -> Self {
Self {
value: Value::String(line.to_string()),
label_cache: line.to_string(),
}
}
/// Get the display label for this item. Cached at
/// construction time, so this is just a borrow.
pub fn label(&self) -> &str {
&self.label_cache
}
}
/// Extract the display label from a JSON value.
fn extract_label<'a>(value: &'a Value, key: &str) -> &'a str {
match value {
Value::String(s) => s.as_str(),
Value::Object(map) => map.get(key).and_then(|v| v.as_str()).unwrap_or(""),
_ => "",
}
}
impl serde::Serialize for Item {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for Item {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = Value::deserialize(deserializer)?;
// Default to "label" key when deserializing. Callers that need
// a different key should construct via Item::new() instead.
Ok(Item::new(value, "label"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn label_from_string() {
let item = Item::new(json!("hello"), "label");
assert_eq!(item.label(), "hello");
}
#[test]
fn label_from_object_with_key() {
let item = Item::new(json!({"label": "foo", "value": 42}), "label");
assert_eq!(item.label(), "foo");
}
#[test]
fn label_from_object_missing_key() {
let item = Item::new(json!({"name": "bar"}), "label");
assert_eq!(item.label(), "");
}
#[test]
fn label_from_object_custom_key() {
let item = Item::new(json!({"name": "bar", "label": "ignored"}), "name");
assert_eq!(item.label(), "bar");
}
#[test]
fn label_from_number() {
let item = Item::new(json!(42), "label");
assert_eq!(item.label(), "");
}
#[test]
fn label_from_null() {
let item = Item::new(json!(null), "label");
assert_eq!(item.label(), "");
}
#[test]
fn from_plain_text() {
let item = Item::from_plain_text("hello world");
assert_eq!(item.label(), "hello world");
assert!(item.value.is_string());
}
#[test]
fn serialize_string_item() {
let item = Item::from_plain_text("test");
let json = serde_json::to_string(&item).unwrap_or_default();
assert_eq!(json, "\"test\"");
}
#[test]
fn serialize_object_item() {
let item = Item::new(json!({"label": "foo"}), "label");
let json = serde_json::to_string(&item).unwrap_or_default();
assert_eq!(json, r#"{"label":"foo"}"#);
}
}