146 lines
4.3 KiB
Rust
146 lines
4.3 KiB
Rust
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::BTreeMap;
|
|
use std::fs::{self, File};
|
|
use std::io::Write;
|
|
#[cfg(test)]
|
|
use std::io::{BufRead, BufReader};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use crate::registry::ActionDefinition;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ActionDictionaryRecord {
|
|
pub action_id: String,
|
|
pub aliases: Vec<String>,
|
|
pub description: String,
|
|
pub intent_examples: Vec<String>,
|
|
pub payload_schema: serde_json::Value,
|
|
pub executable: crate::registry::ExecutableDefinition,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ActionIndex {
|
|
pub schema_version: String,
|
|
pub actions: BTreeMap<String, ActionDictionaryRecord>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DictionarySummary {
|
|
pub action_count: usize,
|
|
pub jsonl_path: PathBuf,
|
|
pub index_path: PathBuf,
|
|
}
|
|
|
|
pub fn default_dictionary_dir() -> PathBuf {
|
|
PathBuf::from("dictionary")
|
|
}
|
|
|
|
pub fn generate_dictionary_files(
|
|
actions: &[ActionDefinition],
|
|
out_dir: &Path,
|
|
) -> Result<DictionarySummary> {
|
|
fs::create_dir_all(out_dir)
|
|
.with_context(|| format!("creating dictionary directory {}", out_dir.display()))?;
|
|
|
|
let jsonl_path = out_dir.join("actions.jsonl");
|
|
let index_path = out_dir.join("actions.index.json");
|
|
let records = actions
|
|
.iter()
|
|
.cloned()
|
|
.map(ActionDictionaryRecord::from)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut jsonl =
|
|
File::create(&jsonl_path).with_context(|| format!("creating {}", jsonl_path.display()))?;
|
|
for record in &records {
|
|
serde_json::to_writer(&mut jsonl, record)?;
|
|
jsonl.write_all(b"\n")?;
|
|
}
|
|
|
|
let index = ActionIndex {
|
|
schema_version: "1.0".to_string(),
|
|
actions: records
|
|
.into_iter()
|
|
.map(|record| (record.action_id.clone(), record))
|
|
.collect(),
|
|
};
|
|
|
|
let pretty = serde_json::to_string_pretty(&index)?;
|
|
fs::write(&index_path, format!("{pretty}\n"))
|
|
.with_context(|| format!("writing {}", index_path.display()))?;
|
|
|
|
Ok(DictionarySummary {
|
|
action_count: index.actions.len(),
|
|
jsonl_path,
|
|
index_path,
|
|
})
|
|
}
|
|
|
|
pub fn load_action_index(dictionary_dir: &Path) -> Result<ActionIndex> {
|
|
let path = dictionary_dir.join("actions.index.json");
|
|
let raw = fs::read_to_string(&path).with_context(|| {
|
|
format!(
|
|
"reading {}; run `cargo run -- dictionary generate` first",
|
|
path.display()
|
|
)
|
|
})?;
|
|
let index = serde_json::from_str::<ActionIndex>(&raw)
|
|
.with_context(|| format!("parsing {}", path.display()))?;
|
|
Ok(index)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn read_jsonl_records(path: &Path) -> Result<Vec<ActionDictionaryRecord>> {
|
|
let file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
|
|
let reader = BufReader::new(file);
|
|
let mut records = Vec::new();
|
|
|
|
for line in reader.lines() {
|
|
let line = line?;
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
records.push(serde_json::from_str::<ActionDictionaryRecord>(&line)?);
|
|
}
|
|
|
|
Ok(records)
|
|
}
|
|
|
|
impl From<ActionDefinition> for ActionDictionaryRecord {
|
|
fn from(action: ActionDefinition) -> Self {
|
|
Self {
|
|
action_id: action.action_id,
|
|
aliases: action.aliases,
|
|
description: action.description,
|
|
intent_examples: action.intent_examples,
|
|
payload_schema: action.payload_schema,
|
|
executable: action.executable,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::registry::registered_actions;
|
|
|
|
#[test]
|
|
fn generates_jsonl_and_index() {
|
|
let temp = tempfile::tempdir().expect("temp dir");
|
|
let summary = generate_dictionary_files(®istered_actions(), temp.path()).unwrap();
|
|
|
|
assert_eq!(summary.action_count, 1);
|
|
assert!(summary.jsonl_path.exists());
|
|
assert!(summary.index_path.exists());
|
|
|
|
let records = read_jsonl_records(&summary.jsonl_path).unwrap();
|
|
assert_eq!(records.len(), 1);
|
|
assert_eq!(records[0].action_id, "logger");
|
|
|
|
let index = load_action_index(temp.path()).unwrap();
|
|
assert!(index.actions.contains_key("logger"));
|
|
assert!(!index.actions.contains_key("log"));
|
|
}
|
|
}
|