use ryo_pattern::{GeneratorLoader, GeneratorTemplate};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GeneratorScope {
Global,
Project,
}
impl std::fmt::Display for GeneratorScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GeneratorScope::Global => write!(f, "global"),
GeneratorScope::Project => write!(f, "project"),
}
}
}
#[derive(Debug, Error)]
pub enum GeneratorStoreError {
#[error("Failed to load generator: {0}")]
Load(#[from] ryo_pattern::GeneratorLoadError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Home directory not found")]
NoHomeDir,
}
#[derive(Debug, Clone)]
pub struct GeneratorEntry {
pub template: GeneratorTemplate,
pub scope: GeneratorScope,
}
#[derive(Debug, Default)]
pub struct GeneratorStore {
global: Vec<GeneratorEntry>,
project: Vec<GeneratorEntry>,
}
impl GeneratorStore {
pub const GLOBAL_GENERATORS_DIR: &'static str = "generators";
pub const PROJECT_GENERATORS_DIR: &'static str = ".ryo/generators";
pub fn new() -> Self {
Self::default()
}
pub fn load(project_path: &Path) -> Result<Self, GeneratorStoreError> {
let global = Self::load_global()?;
let project = Self::load_project(project_path)?;
Ok(Self { global, project })
}
pub fn global_only() -> Result<Self, GeneratorStoreError> {
Ok(Self {
global: Self::load_global()?,
project: Vec::new(),
})
}
fn load_global() -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
let home = dirs::home_dir().ok_or(GeneratorStoreError::NoHomeDir)?;
let global_dir = home.join(".ryo").join(Self::GLOBAL_GENERATORS_DIR);
if !global_dir.exists() {
return Ok(Vec::new());
}
Self::load_from_dir(&global_dir, GeneratorScope::Global)
}
fn load_project(project_path: &Path) -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
let project_dir = project_path.join(Self::PROJECT_GENERATORS_DIR);
if !project_dir.exists() {
return Ok(Vec::new());
}
Self::load_from_dir(&project_dir, GeneratorScope::Project)
}
fn load_from_dir(
dir: &Path,
scope: GeneratorScope,
) -> Result<Vec<GeneratorEntry>, GeneratorStoreError> {
let templates = GeneratorLoader::load_dir(dir)?;
Ok(templates
.into_iter()
.map(|template| GeneratorEntry { template, scope })
.collect())
}
pub fn find_by_id(&self, id: &str) -> Option<&GeneratorEntry> {
if let Some(entry) = self.project.iter().find(|e| e.template.id() == id) {
return Some(entry);
}
self.global.iter().find(|e| e.template.id() == id)
}
pub fn find_by_name(&self, name: &str) -> Option<&GeneratorEntry> {
if let Some(entry) = self.project.iter().find(|e| e.template.name() == name) {
return Some(entry);
}
self.global.iter().find(|e| e.template.name() == name)
}
pub fn all_generators(&self) -> impl Iterator<Item = &GeneratorEntry> {
let project_ids: std::collections::HashSet<_> =
self.project.iter().map(|e| e.template.id()).collect();
self.project.iter().chain(
self.global
.iter()
.filter(move |e| !project_ids.contains(e.template.id())),
)
}
pub fn len(&self) -> usize {
let project_ids: std::collections::HashSet<_> =
self.project.iter().map(|e| e.template.id()).collect();
let global_unique = self
.global
.iter()
.filter(|e| !project_ids.contains(e.template.id()))
.count();
self.project.len() + global_unique
}
pub fn is_empty(&self) -> bool {
self.global.is_empty() && self.project.is_empty()
}
pub fn list_names(&self) -> Vec<(&str, GeneratorScope)> {
self.all_generators()
.map(|e| (e.template.name(), e.scope))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
fn create_test_generator(dir: &Path, name: &str) {
let content = format!(
r#"
generator:
id: "{name}"
name: "{name}"
description: Test generator
params:
- name: value
description: Test param
required: true
template:
code: "// Generated: {{{{value}}}}"
"#
);
let path = dir.join(format!("{}.yaml", name));
let mut file = std::fs::File::create(path).unwrap();
file.write_all(content.as_bytes()).unwrap();
}
#[test]
fn test_load_from_dir() {
let temp = TempDir::new().unwrap();
create_test_generator(temp.path(), "test_gen");
let entries = GeneratorStore::load_from_dir(temp.path(), GeneratorScope::Project).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].template.name(), "test_gen");
}
#[test]
fn test_find_by_name() {
let temp = TempDir::new().unwrap();
let gen_dir = temp.path().join(".ryo/generators");
std::fs::create_dir_all(&gen_dir).unwrap();
create_test_generator(&gen_dir, "my_generator");
let store = GeneratorStore::load(temp.path()).unwrap();
let entry = store.find_by_name("my_generator");
assert!(entry.is_some());
assert_eq!(entry.unwrap().scope, GeneratorScope::Project);
}
#[test]
fn test_empty_store() {
let store = GeneratorStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
}