use serde::{Deserialize, Serialize};
pub const CONTRACT_VERSION: u32 = 1;
pub const ENV_VAGUS_BIN: &str = "VAGUS";
pub const ENV_VAULT: &str = "VAGUS_VAULT";
pub const ENV_DATA_DIR: &str = "VAGUS_DATA_DIR";
pub const ENV_CONFIG_DIR: &str = "VAGUS_CONFIG_DIR";
pub const ENV_VERSION: &str = "VAGUS_VERSION";
pub const ENV_PROTOCOL: &str = "VAGUS_PLUGIN_PROTOCOL";
pub const ENV_CONTRACT: &str = "VAGUS_PLUGIN_CONTRACT";
pub const PROTOCOL_NDJSON: &str = "ndjson";
pub const DESCRIBE_SUBCOMMAND: &str = "__describe";
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LogLevel {
Info,
Warn,
Error,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NoteAction {
Write,
Append,
Delete,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Event {
Log { level: LogLevel, msg: String },
Progress {
done: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
total: Option<u64>,
#[serde(default, skip_serializing_if = "String::is_empty")]
msg: String,
},
Note {
path: String,
#[serde(default = "default_note_action")]
action: NoteAction,
},
Result {
ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
summary: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "is_false")]
no_index: bool,
},
}
fn default_note_action() -> NoteAction {
NoteAction::Write
}
fn is_false(b: &bool) -> bool {
!*b
}
impl Event {
pub fn parse_line(line: &str) -> Option<Event> {
let line = line.trim();
if line.is_empty() {
return None;
}
serde_json::from_str::<Event>(line).ok()
}
pub fn to_line(&self) -> String {
serde_json::to_string(self).expect("Event serializes")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrips_each_variant() {
for ev in [
Event::Log {
level: LogLevel::Warn,
msg: "hi".into(),
},
Event::Progress {
done: 3,
total: Some(10),
msg: "fetching".into(),
},
Event::Note {
path: "30-Resources/slack/x.md".into(),
action: NoteAction::Append,
},
Event::Result {
ok: true,
summary: Some(serde_json::json!({"notes": 4})),
data: None,
no_index: false,
},
] {
let line = ev.to_line();
let back = Event::parse_line(&line).expect("parses");
assert_eq!(format!("{ev:?}"), format!("{back:?}"));
}
}
#[test]
fn progress_total_optional() {
let ev = Event::parse_line(r#"{"type":"progress","done":1}"#).unwrap();
match ev {
Event::Progress { done, total, msg } => {
assert_eq!(done, 1);
assert!(total.is_none());
assert!(msg.is_empty());
}
_ => panic!("wrong variant"),
}
}
#[test]
fn note_action_defaults_to_write() {
let ev = Event::parse_line(r#"{"type":"note","path":"a.md"}"#).unwrap();
assert!(matches!(
ev,
Event::Note {
action: NoteAction::Write,
..
}
));
}
#[test]
fn non_events_are_none() {
assert!(Event::parse_line("just some text").is_none());
assert!(Event::parse_line(r#"["a","b"]"#).is_none()); assert!(Event::parse_line(r#"{"type":"bogus"}"#).is_none());
assert!(Event::parse_line(" ").is_none());
}
}