| use std::sync::Arc; |
|
|
| use anyhow::{Context, anyhow}; |
| use forge_app::SkillFetchService; |
| use forge_domain::Skill; |
| use tokio::sync::OnceCell; |
|
|
| |
| |
| |
| |
| |
| pub struct ForgeSkillFetch<R> { |
| repository: Arc<R>, |
| cache: OnceCell<Vec<Skill>>, |
| } |
|
|
| impl<R> ForgeSkillFetch<R> { |
| |
| pub fn new(repository: Arc<R>) -> Self { |
| Self { repository, cache: OnceCell::new() } |
| } |
| } |
|
|
| #[async_trait::async_trait] |
| impl<R: forge_domain::SkillRepository> SkillFetchService for ForgeSkillFetch<R> { |
| async fn fetch_skill(&self, skill_name: String) -> anyhow::Result<Skill> { |
| |
| let skills = self.get_or_load_skills().await?; |
|
|
| |
| skills |
| .iter() |
| .find(|skill| skill.name == skill_name) |
| .cloned() |
| .ok_or_else(|| { |
| anyhow!("Skill '{skill_name}' not found. Please check the available skills list.") |
| }) |
| } |
|
|
| async fn list_skills(&self) -> anyhow::Result<Vec<Skill>> { |
| self.get_or_load_skills().await.cloned() |
| } |
| } |
|
|
| impl<R: forge_domain::SkillRepository> ForgeSkillFetch<R> { |
| |
| async fn get_or_load_skills(&self) -> anyhow::Result<&Vec<Skill>> { |
| self.cache |
| .get_or_try_init(|| async { |
| self.repository |
| .load_skills() |
| .await |
| .context("Failed to load skills") |
| }) |
| .await |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use forge_domain::Skill; |
| use pretty_assertions::assert_eq; |
|
|
| use super::*; |
|
|
| struct MockSkillRepository { |
| skills: Vec<Skill>, |
| } |
|
|
| #[async_trait::async_trait] |
| impl forge_domain::SkillRepository for MockSkillRepository { |
| async fn load_skills(&self) -> anyhow::Result<Vec<Skill>> { |
| Ok(self.skills.clone()) |
| } |
| } |
|
|
| #[tokio::test] |
| async fn test_fetch_skill_found() { |
| |
| let skills = vec![ |
| Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"), |
| Skill::new("xlsx", "Handle Excel files", "Excel handling skill") |
| .path("/skills/xlsx.md"), |
| ]; |
| let repo = MockSkillRepository { skills: skills.clone() }; |
| let fetch_service = ForgeSkillFetch::new(Arc::new(repo)); |
|
|
| |
| let actual = fetch_service.fetch_skill("pdf".to_string()).await; |
|
|
| |
| assert!(actual.is_ok()); |
| let expected = |
| Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"); |
| assert_eq!(actual.unwrap(), expected); |
| } |
|
|
| #[tokio::test] |
| async fn test_fetch_skill_not_found() { |
| |
| let skills = vec![ |
| Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"), |
| ]; |
| let repo = MockSkillRepository { skills }; |
| let fetch_service = ForgeSkillFetch::new(Arc::new(repo)); |
|
|
| |
| let actual = fetch_service.fetch_skill("unknown".to_string()).await; |
|
|
| |
| assert!(actual.is_err()); |
| let error = actual.unwrap_err().to_string(); |
| assert!(error.contains("Skill 'unknown' not found")); |
| } |
|
|
| #[tokio::test] |
| async fn test_list_skills() { |
| |
| let expected = vec![ |
| Skill::new("pdf", "Handle PDF files", "PDF handling skill").path("/skills/pdf.md"), |
| Skill::new("xlsx", "Handle Excel files", "Excel handling skill") |
| .path("/skills/xlsx.md"), |
| ]; |
| let repo = MockSkillRepository { skills: expected.clone() }; |
| let fetch_service = ForgeSkillFetch::new(Arc::new(repo)); |
|
|
| |
| let actual = fetch_service.list_skills().await.unwrap(); |
|
|
| |
| assert_eq!(actual, expected); |
| } |
| } |
|
|