agpm_cli/core/resource.rs
1//! Resource abstractions for AGPM
2//!
3//! This module defines the core resource types and management traits that form the foundation
4//! of AGPM's resource system. Resources are the fundamental units that AGPM manages, installs,
5//! and tracks across different source repositories.
6//!
7//! # Resource Model
8//!
9//! AGPM supports different types of resources, each with specific characteristics:
10//! - **Agents**: AI assistant configurations and prompts
11//! - **Snippets**: Reusable code templates and examples
12//!
13//! Resources are distributed as markdown files (.md) that may contain frontmatter metadata
14//! for configuration and dependency information.
15//!
16//! # Core Types
17//!
18//! - [`ResourceType`] - Enumeration of supported resource types
19//! - [`Resource`] - Trait defining the interface for all resource types
20//!
21//! # Resource Management
22//!
23//! Resources are defined in the project's `agpm.toml` file and installed to specific
24//! directories based on their type. Scripts and hooks have special handling for
25//! Claude Code integration.
26//!
27//! # Examples
28//!
29//! ## Working with Resource Types
30//!
31//! ```rust,no_run
32//! use agpm_cli::core::ResourceType;
33//! use std::path::Path;
34//!
35//! // Convert strings to resource types
36//! let agent_type: ResourceType = "agent".parse().unwrap();
37//! let snippet_type: ResourceType = "snippet".parse().unwrap();
38//! let script_type: ResourceType = "script".parse().unwrap();
39//! let hook_type: ResourceType = "hook".parse().unwrap();
40//!
41//! // Get default directory names
42//! assert_eq!(agent_type.default_directory(), Some(".claude/agents"));
43//! assert_eq!(snippet_type.default_directory(), Some(".claude/snippets"));
44//! assert_eq!(script_type.default_directory(), Some(".claude/scripts"));
45//! assert_eq!(hook_type.default_directory(), None); // Hooks merged into config, not staged
46//! ```
47//!
48//! ## Serialization Support
49//!
50//! ```rust,no_run
51//! use agpm_cli::core::ResourceType;
52//!
53//! // ResourceType implements Serialize/Deserialize
54//! let agent = ResourceType::Agent;
55//! let json = serde_json::to_string(&agent).unwrap();
56//! assert_eq!(json, "\"agent\"");
57//!
58//! let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
59//! assert_eq!(deserialized, ResourceType::Agent);
60//! ```
61
62use anyhow::Result;
63use serde::{Deserialize, Serialize};
64use std::path::Path;
65
66/// Enumeration of supported resource types in AGPM
67///
68/// This enum defines the different categories of resources that AGPM can manage.
69/// Each resource type has specific characteristics, installation paths, and
70/// manifest file requirements.
71///
72/// # Serialization
73///
74/// `ResourceType` implements [`serde::Serialize`] and [`serde::Deserialize`]
75/// using lowercase string representations ("agent", "snippet") for JSON/TOML
76/// compatibility.
77///
78/// # Resource Type Characteristics
79///
80/// ## Agent
81/// - **Purpose**: AI assistant configurations, prompts, and behavioral definitions
82/// - **Default Directory**: `.claude/agents`
83/// - **Common Use Cases**: Claude Code agents, custom AI assistants, specialized prompts
84///
85/// ## Snippet
86/// - **Purpose**: Reusable code templates, examples, and documentation fragments
87/// - **Default Directory**: `.claude/snippets`
88/// - **Common Use Cases**: Code templates, configuration examples, documentation
89///
90/// ## Script
91/// - **Purpose**: Executable files that can be run by hooks or independently
92/// - **Default Directory**: `.claude/scripts`
93/// - **Common Use Cases**: Validation scripts, automation tools, hook executables
94///
95/// ## Hook
96/// - **Purpose**: Event-based automation configurations for Claude Code
97/// - **Staging**: Merged into `.claude/settings.local.json`, not staged to disk
98/// - **Common Use Cases**: Pre/Post tool use validation, custom event handlers
99///
100/// # Examples
101///
102/// ## Basic Usage
103///
104/// ```rust,no_run
105/// use agpm_cli::core::ResourceType;
106///
107/// let agent = ResourceType::Agent;
108/// let snippet = ResourceType::Snippet;
109///
110/// assert_eq!(agent.to_string(), "agent");
111/// assert_eq!(snippet.to_string(), "snippet");
112/// ```
113///
114/// ## String Parsing
115///
116/// ```rust,no_run
117/// use agpm_cli::core::ResourceType;
118/// use std::str::FromStr;
119///
120/// let agent: ResourceType = "agent".parse().unwrap();
121/// let snippet: ResourceType = "SNIPPET".parse().unwrap(); // Case insensitive
122///
123/// assert_eq!(agent, ResourceType::Agent);
124/// assert_eq!(snippet, ResourceType::Snippet);
125///
126/// // Invalid resource type
127/// assert!("invalid".parse::<ResourceType>().is_err());
128/// ```
129///
130/// ## Directory Names
131///
132/// ```rust,no_run
133/// use agpm_cli::core::ResourceType;
134///
135/// let agent = ResourceType::Agent;
136/// assert_eq!(agent.default_directory(), Some(".claude/agents"));
137///
138/// let snippet = ResourceType::Snippet;
139/// assert_eq!(snippet.default_directory(), Some(".claude/snippets"));
140///
141/// let script = ResourceType::Script;
142/// assert_eq!(script.default_directory(), Some(".claude/scripts"));
143/// ```
144///
145/// ## JSON Serialization
146///
147/// ```rust,no_run
148/// use agpm_cli::core::ResourceType;
149///
150/// let agent = ResourceType::Agent;
151/// let json = serde_json::to_string(&agent).unwrap();
152/// assert_eq!(json, "\"agent\"");
153///
154/// let parsed: ResourceType = serde_json::from_str("\"snippet\"").unwrap();
155/// assert_eq!(parsed, ResourceType::Snippet);
156/// ```
157#[derive(
158 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default,
159)]
160#[serde(rename_all = "lowercase")]
161pub enum ResourceType {
162 /// AI assistant configurations and prompts
163 ///
164 /// Agents define AI assistant behavior, including system prompts, specialized
165 /// capabilities, and configuration parameters. They are typically stored as
166 /// markdown files with frontmatter containing metadata.
167 Agent,
168
169 /// Reusable code templates and examples
170 ///
171 /// Snippets contain reusable code fragments, configuration examples, or
172 /// documentation templates that can be shared across projects.
173 #[default]
174 Snippet,
175
176 /// Claude Code commands
177 ///
178 /// Commands define custom slash commands that can be used within Claude Code
179 /// to perform specific actions or automate workflows.
180 Command,
181
182 /// MCP (Model Context Protocol) servers
183 ///
184 /// MCP servers provide integrations with external systems and services,
185 /// allowing Claude Code to access databases, APIs, and other tools.
186 #[serde(rename = "mcp-server")]
187 McpServer,
188
189 /// Executable script files
190 ///
191 /// Scripts are executable files (.sh, .js, .py, etc.) that can be referenced
192 /// by hooks or run independently. They are installed to .claude/scripts/
193 Script,
194
195 /// Hook configuration files
196 ///
197 /// Hooks define event-based automation in Claude Code. They are JSON files
198 /// that configure scripts to run at specific events (`PreToolUse`, `PostToolUse`, etc.)
199 /// and are merged into settings.local.json
200 Hook,
201 // Future resource types can be added here
202}
203
204impl ResourceType {
205 /// Get all resource types in a consistent order
206 ///
207 /// Returns an array containing all `ResourceType` variants. This is useful
208 /// for iterating over all resource types when processing manifests, lockfiles,
209 /// or performing batch operations.
210 ///
211 /// The order is guaranteed to be stable: Agent, Snippet, Command, `McpServer`, Script, Hook
212 ///
213 /// # Examples
214 ///
215 /// ```rust,no_run
216 /// use agpm_cli::core::ResourceType;
217 ///
218 /// // Iterate over all resource types
219 /// for resource_type in ResourceType::all() {
220 /// println!("Processing {}", resource_type);
221 /// }
222 ///
223 /// // Count total resource types
224 /// assert_eq!(ResourceType::all().len(), 6);
225 /// ```
226 pub const fn all() -> &'static [Self] {
227 &[Self::Agent, Self::Snippet, Self::Command, Self::McpServer, Self::Script, Self::Hook]
228 }
229
230 /// Get the plural form of the resource type.
231 ///
232 /// Returns the plural form used in lockfile dependency references and TOML sections.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use agpm_cli::core::ResourceType;
238 ///
239 /// assert_eq!(ResourceType::Agent.to_plural(), "agents");
240 /// assert_eq!(ResourceType::McpServer.to_plural(), "mcp-servers");
241 /// ```
242 pub const fn to_plural(&self) -> &'static str {
243 match self {
244 Self::Agent => "agents",
245 Self::Snippet => "snippets",
246 Self::Command => "commands",
247 Self::Script => "scripts",
248 Self::Hook => "hooks",
249 Self::McpServer => "mcp-servers",
250 }
251 }
252
253 /// Get the default installation directory name for this resource type
254 ///
255 /// Returns the conventional directory name where resources of this type
256 /// are typically installed in AGPM projects.
257 ///
258 /// # Returns
259 ///
260 /// - [`Agent`] → `Some(".claude/agents")`
261 /// - [`Snippet`] → `Some(".claude/snippets")`
262 /// - [`Command`] → `Some(".claude/commands")`
263 /// - [`McpServer`] → `None` (merged into `.mcp.json`, not staged to disk)
264 /// - [`Script`] → `Some(".claude/scripts")`
265 /// - [`Hook`] → `None` (merged into `.claude/settings.local.json`, not staged to disk)
266 ///
267 /// # Examples
268 ///
269 /// ```rust,no_run
270 /// use agpm_cli::core::ResourceType;
271 ///
272 /// assert_eq!(ResourceType::Agent.default_directory(), Some(".claude/agents"));
273 /// assert_eq!(ResourceType::Snippet.default_directory(), Some(".claude/snippets"));
274 /// assert_eq!(ResourceType::Command.default_directory(), Some(".claude/commands"));
275 /// assert_eq!(ResourceType::Script.default_directory(), Some(".claude/scripts"));
276 /// // Hook and McpServer return None as they don't stage to disk
277 /// assert_eq!(ResourceType::Hook.default_directory(), None);
278 /// assert_eq!(ResourceType::McpServer.default_directory(), None);
279 /// ```
280 ///
281 /// # Note
282 ///
283 /// This is just the default convention. Users can install resources to any
284 /// directory by specifying custom paths in their manifest files.
285 ///
286 /// [`Agent`]: ResourceType::Agent
287 /// [`Snippet`]: ResourceType::Snippet
288 /// [`Command`]: ResourceType::Command
289 /// [`McpServer`]: ResourceType::McpServer
290 /// [`Script`]: ResourceType::Script
291 /// [`Hook`]: ResourceType::Hook
292 #[must_use]
293 pub const fn default_directory(&self) -> Option<&str> {
294 match self {
295 Self::Agent => Some(".claude/agents"),
296 Self::Snippet => Some(".claude/snippets"),
297 Self::Command => Some(".claude/commands"),
298 Self::McpServer => None, // Merged into .mcp.json, not staged to disk
299 Self::Script => Some(".claude/scripts"),
300 Self::Hook => None, // Merged into .claude/settings.local.json, not staged to disk
301 }
302 }
303
304 /// Get the default tool for this resource type.
305 ///
306 /// Returns the default tool that should be used when no explicit tool is specified
307 /// in the dependency configuration. Different resource types have different defaults:
308 ///
309 /// - Most resources default to "claude-code"
310 /// - Snippets default to "agpm" (shared infrastructure across tools)
311 ///
312 /// # Returns
313 ///
314 /// - [`ResourceType::Agent`] → `"claude-code"`
315 /// - [`ResourceType::Snippet`] → `"agpm"` (shared infrastructure)
316 /// - [`ResourceType::Command`] → `"claude-code"`
317 /// - [`ResourceType::McpServer`] → `"claude-code"`
318 /// - [`ResourceType::Script`] → `"claude-code"`
319 /// - [`ResourceType::Hook`] → `"claude-code"`
320 ///
321 /// # Examples
322 ///
323 /// ```rust,no_run
324 /// use agpm_cli::core::ResourceType;
325 ///
326 /// assert_eq!(ResourceType::Agent.default_tool(), "claude-code");
327 /// assert_eq!(ResourceType::Snippet.default_tool(), "agpm");
328 /// assert_eq!(ResourceType::Command.default_tool(), "claude-code");
329 /// ```
330 ///
331 /// # Rationale
332 ///
333 /// Snippets are designed to be shared content across multiple tools (Claude Code,
334 /// OpenCode, etc.). The `.agpm/snippets/` location provides shared infrastructure
335 /// that can be referenced by resources from different tools. Therefore, snippets
336 /// default to the "agpm" tool type.
337 ///
338 /// Users can still explicitly set `tool = "claude-code"` for a snippet if they want
339 /// it installed to `.claude/snippets/` instead.
340 #[must_use]
341 pub const fn default_tool(&self) -> &'static str {
342 match self {
343 Self::Snippet => "agpm", // Snippets use shared infrastructure
344 _ => "claude-code", // All other resources default to claude-code
345 }
346 }
347}
348
349impl std::fmt::Display for ResourceType {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 match self {
352 Self::Agent => write!(f, "agent"),
353 Self::Snippet => write!(f, "snippet"),
354 Self::Command => write!(f, "command"),
355 Self::McpServer => write!(f, "mcp-server"),
356 Self::Script => write!(f, "script"),
357 Self::Hook => write!(f, "hook"),
358 }
359 }
360}
361
362impl std::str::FromStr for ResourceType {
363 type Err = crate::core::AgpmError;
364
365 fn from_str(s: &str) -> Result<Self, Self::Err> {
366 match s.to_lowercase().as_str() {
367 "agent" | "agents" => Ok(Self::Agent),
368 "snippet" | "snippets" => Ok(Self::Snippet),
369 "command" | "commands" => Ok(Self::Command),
370 "mcp-server" | "mcp-servers" | "mcpserver" | "mcp" => Ok(Self::McpServer),
371 "script" | "scripts" => Ok(Self::Script),
372 "hook" | "hooks" => Ok(Self::Hook),
373 _ => Err(crate::core::AgpmError::InvalidResourceType {
374 resource_type: s.to_string(),
375 }),
376 }
377 }
378}
379
380/// Base trait defining the interface for all AGPM resources
381///
382/// This trait provides a common interface for different types of resources (agents, snippets)
383/// managed by AGPM. It abstracts the core operations that can be performed on any resource,
384/// including validation, installation, and metadata access.
385///
386/// # Design Principles
387///
388/// - **Type Safety**: Each resource has a specific [`ResourceType`]
389/// - **Validation**: Resources can validate their own structure and dependencies
390/// - **Installation**: Resources know how to install themselves to target locations
391/// - **Metadata**: Resources provide structured metadata for tooling and display
392/// - **Flexibility**: Resources can be profiled or configured during installation
393///
394/// # Implementation Requirements
395///
396/// Implementors of this trait should:
397/// - Provide meaningful error messages in validation failures
398/// - Support atomic installation operations (no partial installs on failure)
399/// - Generate deterministic installation paths
400/// - Include rich metadata for resource discovery and management
401///
402/// # Examples
403///
404/// ## Basic Resource Usage Pattern
405///
406/// ```rust,no_run
407/// use agpm_cli::core::{Resource, ResourceType};
408/// use anyhow::Result;
409/// use std::path::Path;
410///
411/// fn process_resource(resource: &dyn Resource) -> Result<()> {
412/// // Get basic information
413/// println!("Processing resource: {}", resource.name());
414/// println!("Type: {}", resource.resource_type());
415///
416/// if let Some(description) = resource.description() {
417/// println!("Description: {}", description);
418/// }
419///
420/// // Validate the resource
421/// resource.validate()?;
422///
423/// // Install to default location
424/// let target = Path::new("./resources");
425/// let install_path = resource.install_path(target);
426/// resource.install(&install_path, None)?;
427///
428/// Ok(())
429/// }
430/// ```
431///
432/// ## Metadata Extraction
433///
434/// ```rust,no_run
435/// use agpm_cli::core::Resource;
436/// use anyhow::Result;
437///
438/// fn extract_metadata(resource: &dyn Resource) -> Result<()> {
439/// let metadata = resource.metadata()?;
440///
441/// // Metadata is JSON Value for flexibility
442/// if let Some(version) = metadata.get("version") {
443/// println!("Resource version: {}", version);
444/// }
445///
446/// if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
447/// println!("Tags: {:?}", tags);
448/// }
449///
450/// Ok(())
451/// }
452/// ```
453///
454/// # Trait Object Usage
455///
456/// The trait is object-safe and can be used as a trait object:
457///
458/// ```rust,no_run
459/// use agpm_cli::core::Resource;
460/// use std::any::Any;
461///
462/// fn handle_resource(resource: Box<dyn Resource>) {
463/// println!("Handling resource: {}", resource.name());
464///
465/// // Can be downcasted to concrete types if needed
466/// let any = resource.as_any();
467/// // ... downcasting logic
468/// }
469/// ```
470pub trait Resource {
471 /// Get the unique name identifier for this resource
472 ///
473 /// The name is used to identify the resource in manifests, lockfiles,
474 /// and CLI operations. It should be unique within a project's namespace.
475 ///
476 /// # Returns
477 ///
478 /// A string slice containing the resource name
479 ///
480 /// # Examples
481 ///
482 /// ```rust,no_run
483 /// use agpm_cli::core::Resource;
484 ///
485 /// fn print_resource_info(resource: &dyn Resource) {
486 /// println!("Resource name: {}", resource.name());
487 /// }
488 /// ```
489 fn name(&self) -> &str;
490
491 /// Get the resource type classification
492 ///
493 /// Returns the [`ResourceType`] enum value that identifies what kind of
494 /// resource this is (Agent, Snippet, etc.).
495 ///
496 /// # Returns
497 ///
498 /// The [`ResourceType`] for this resource
499 ///
500 /// # Examples
501 ///
502 /// ```rust,no_run
503 /// use agpm_cli::core::{Resource, ResourceType};
504 ///
505 /// fn categorize_resource(resource: &dyn Resource) {
506 /// match resource.resource_type() {
507 /// ResourceType::Agent => println!("This is an AI agent"),
508 /// ResourceType::Snippet => println!("This is a code snippet"),
509 /// ResourceType::Command => println!("This is a Claude Code command"),
510 /// ResourceType::McpServer => println!("This is an MCP server"),
511 /// ResourceType::Script => println!("This is an executable script"),
512 /// ResourceType::Hook => println!("This is a hook configuration"),
513 /// }
514 /// }
515 /// ```
516 fn resource_type(&self) -> ResourceType;
517
518 /// Get the human-readable description of this resource
519 ///
520 /// Returns an optional description that explains what the resource does
521 /// or how it should be used. This is typically displayed in resource
522 /// listings and help text.
523 ///
524 /// # Returns
525 ///
526 /// - `Some(description)` if the resource has a description
527 /// - `None` if no description is available
528 ///
529 /// # Examples
530 ///
531 /// ```rust,no_run
532 /// use agpm_cli::core::Resource;
533 ///
534 /// fn show_resource_details(resource: &dyn Resource) {
535 /// println!("Name: {}", resource.name());
536 /// if let Some(desc) = resource.description() {
537 /// println!("Description: {}", desc);
538 /// } else {
539 /// println!("No description available");
540 /// }
541 /// }
542 /// ```
543 fn description(&self) -> Option<&str>;
544
545 /// Validate the resource structure and content
546 ///
547 /// Performs comprehensive validation of the resource including:
548 /// - File structure integrity
549 /// - Content format validation
550 /// - Dependency constraint checking
551 /// - Metadata consistency
552 ///
553 /// # Returns
554 ///
555 /// - `Ok(())` if the resource is valid
556 /// - `Err(error)` with detailed validation failure information
557 ///
558 /// # Examples
559 ///
560 /// ```rust,no_run
561 /// use agpm_cli::core::Resource;
562 /// use anyhow::Result;
563 ///
564 /// fn validate_before_install(resource: &dyn Resource) -> Result<()> {
565 /// resource.validate()
566 /// .map_err(|e| anyhow::anyhow!("Resource validation failed: {}", e))?;
567 ///
568 /// println!("Resource {} is valid", resource.name());
569 /// Ok(())
570 /// }
571 /// ```
572 fn validate(&self) -> Result<()>;
573
574 /// Install the resource to the specified target path
575 ///
576 /// Performs the actual installation of the resource files to the target
577 /// location. This operation should be atomic - either it succeeds completely
578 /// or fails without making any changes.
579 ///
580 /// # Arguments
581 ///
582 /// * `target` - The directory path where the resource should be installed
583 /// * `profile` - Optional profile name for customized installation (may be unused)
584 ///
585 /// # Returns
586 ///
587 /// - `Ok(())` if installation succeeds
588 /// - `Err(error)` if installation fails with detailed error information
589 ///
590 /// # Examples
591 ///
592 /// ```rust,no_run
593 /// use agpm_cli::core::Resource;
594 /// use std::path::Path;
595 /// use anyhow::Result;
596 ///
597 /// fn install_resource(resource: &dyn Resource) -> Result<()> {
598 /// let target = Path::new("./installed-resources");
599 ///
600 /// // Validate first
601 /// resource.validate()?;
602 ///
603 /// // Install without profile
604 /// resource.install(target, None)?;
605 ///
606 /// println!("Successfully installed {}", resource.name());
607 /// Ok(())
608 /// }
609 /// ```
610 fn install(&self, target: &Path, profile: Option<&str>) -> Result<()>;
611
612 /// Calculate the installation path for this resource
613 ///
614 /// Determines where this resource would be installed relative to a base
615 /// directory. This is used for path planning and conflict detection.
616 ///
617 /// # Arguments
618 ///
619 /// * `base` - The base directory for installation
620 ///
621 /// # Returns
622 ///
623 /// The full path where this resource would be installed
624 ///
625 /// # Examples
626 ///
627 /// ```rust,no_run
628 /// use agpm_cli::core::Resource;
629 /// use std::path::Path;
630 ///
631 /// fn check_install_location(resource: &dyn Resource) {
632 /// let base = Path::new("/project/resources");
633 /// let install_path = resource.install_path(base);
634 ///
635 /// println!("{} would be installed to: {}",
636 /// resource.name(),
637 /// install_path.display()
638 /// );
639 /// }
640 /// ```
641 fn install_path(&self, base: &Path) -> std::path::PathBuf;
642
643 /// Get structured metadata for this resource as JSON
644 ///
645 /// Returns resource metadata in a flexible JSON format that can include
646 /// version information, tags, author details, and other custom fields.
647 /// This metadata is used for resource discovery, filtering, and display.
648 ///
649 /// # Returns
650 ///
651 /// - `Ok(json_value)` containing the metadata
652 /// - `Err(error)` if metadata cannot be generated or parsed
653 ///
654 /// # Metadata Structure
655 ///
656 /// While flexible, metadata typically includes:
657 /// - `name`: Resource name
658 /// - `type`: Resource type
659 /// - `version`: Version information
660 /// - `description`: Human-readable description
661 /// - `tags`: Array of classification tags
662 /// - `dependencies`: Dependency information
663 ///
664 /// # Examples
665 ///
666 /// ```rust,no_run
667 /// use agpm_cli::core::Resource;
668 /// use anyhow::Result;
669 ///
670 /// fn show_resource_metadata(resource: &dyn Resource) -> Result<()> {
671 /// let metadata = resource.metadata()?;
672 ///
673 /// if let Some(version) = metadata.get("version") {
674 /// println!("Version: {}", version);
675 /// }
676 ///
677 /// if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
678 /// print!("Tags: ");
679 /// for tag in tags {
680 /// print!("{} ", tag.as_str().unwrap_or("?"));
681 /// }
682 /// println!();
683 /// }
684 ///
685 /// Ok(())
686 /// }
687 /// ```
688 fn metadata(&self) -> Result<serde_json::Value>;
689
690 /// Get a reference to this resource as [`std::any::Any`] for downcasting
691 ///
692 /// This method enables downcasting from the [`Resource`] trait object to
693 /// concrete resource implementations when needed for type-specific operations.
694 ///
695 /// # Returns
696 ///
697 /// A reference to this resource as [`std::any::Any`]
698 ///
699 /// # Examples
700 ///
701 /// ```rust,no_run
702 /// use agpm_cli::core::Resource;
703 /// use std::any::Any;
704 ///
705 /// // Hypothetical concrete resource type
706 /// struct MyAgent {
707 /// name: String,
708 /// // ... other fields
709 /// }
710 ///
711 /// fn try_downcast_to_agent(resource: &dyn Resource) {
712 /// let any = resource.as_any();
713 ///
714 /// if let Some(agent) = any.downcast_ref::<MyAgent>() {
715 /// println!("Successfully downcasted to MyAgent: {}", agent.name);
716 /// } else {
717 /// println!("Resource is not a MyAgent type");
718 /// }
719 /// }
720 /// ```
721 fn as_any(&self) -> &dyn std::any::Any;
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
729 fn test_resource_type_default_directory() {
730 assert_eq!(ResourceType::Agent.default_directory(), Some(".claude/agents"));
731 assert_eq!(ResourceType::Snippet.default_directory(), Some(".claude/snippets"));
732 assert_eq!(ResourceType::Command.default_directory(), Some(".claude/commands"));
733 assert_eq!(ResourceType::McpServer.default_directory(), None);
734 assert_eq!(ResourceType::Script.default_directory(), Some(".claude/scripts"));
735 assert_eq!(ResourceType::Hook.default_directory(), None);
736 }
737
738 #[test]
739 fn test_resource_type_display() {
740 assert_eq!(ResourceType::Agent.to_string(), "agent");
741 assert_eq!(ResourceType::Snippet.to_string(), "snippet");
742 assert_eq!(ResourceType::Command.to_string(), "command");
743 assert_eq!(ResourceType::McpServer.to_string(), "mcp-server");
744 assert_eq!(ResourceType::Script.to_string(), "script");
745 assert_eq!(ResourceType::Hook.to_string(), "hook");
746 }
747
748 #[test]
749 fn test_resource_type_from_str() {
750 use std::str::FromStr;
751
752 assert_eq!(ResourceType::from_str("agent").unwrap(), ResourceType::Agent);
753 assert_eq!(ResourceType::from_str("snippet").unwrap(), ResourceType::Snippet);
754 assert_eq!(ResourceType::from_str("AGENT").unwrap(), ResourceType::Agent);
755 assert_eq!(ResourceType::from_str("Snippet").unwrap(), ResourceType::Snippet);
756 assert_eq!(ResourceType::from_str("command").unwrap(), ResourceType::Command);
757 assert_eq!(ResourceType::from_str("COMMAND").unwrap(), ResourceType::Command);
758 assert_eq!(ResourceType::from_str("mcp-server").unwrap(), ResourceType::McpServer);
759 assert_eq!(ResourceType::from_str("MCP").unwrap(), ResourceType::McpServer);
760 assert_eq!(ResourceType::from_str("script").unwrap(), ResourceType::Script);
761 assert_eq!(ResourceType::from_str("SCRIPT").unwrap(), ResourceType::Script);
762 assert_eq!(ResourceType::from_str("hook").unwrap(), ResourceType::Hook);
763 assert_eq!(ResourceType::from_str("HOOK").unwrap(), ResourceType::Hook);
764
765 assert!(ResourceType::from_str("invalid").is_err());
766 }
767
768 #[test]
769 fn test_resource_type_serialization() {
770 let agent = ResourceType::Agent;
771 let json = serde_json::to_string(&agent).unwrap();
772 assert_eq!(json, "\"agent\"");
773
774 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
775 assert_eq!(deserialized, ResourceType::Agent);
776
777 // Test command serialization
778 let command = ResourceType::Command;
779 let json = serde_json::to_string(&command).unwrap();
780 assert_eq!(json, "\"command\"");
781
782 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
783 assert_eq!(deserialized, ResourceType::Command);
784
785 // Test mcp-server serialization
786 let mcp_server = ResourceType::McpServer;
787 let json = serde_json::to_string(&mcp_server).unwrap();
788 assert_eq!(json, "\"mcp-server\"");
789
790 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
791 assert_eq!(deserialized, ResourceType::McpServer);
792
793 // Test script serialization
794 let script = ResourceType::Script;
795 let json = serde_json::to_string(&script).unwrap();
796 assert_eq!(json, "\"script\"");
797
798 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
799 assert_eq!(deserialized, ResourceType::Script);
800
801 // Test hook serialization
802 let hook = ResourceType::Hook;
803 let json = serde_json::to_string(&hook).unwrap();
804 assert_eq!(json, "\"hook\"");
805
806 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
807 assert_eq!(deserialized, ResourceType::Hook);
808 }
809
810 #[test]
811 fn test_resource_type_equality() {
812 assert_eq!(ResourceType::Command, ResourceType::Command);
813 assert_ne!(ResourceType::Command, ResourceType::Agent);
814 assert_ne!(ResourceType::Command, ResourceType::Snippet);
815 assert_eq!(ResourceType::McpServer, ResourceType::McpServer);
816 assert_ne!(ResourceType::McpServer, ResourceType::Agent);
817 assert_eq!(ResourceType::Script, ResourceType::Script);
818 assert_ne!(ResourceType::Script, ResourceType::Hook);
819 assert_eq!(ResourceType::Hook, ResourceType::Hook);
820 assert_ne!(ResourceType::Hook, ResourceType::Agent);
821 }
822
823 #[test]
824 fn test_resource_type_copy() {
825 let command = ResourceType::Command;
826 let copied = command; // ResourceType implements Copy, so this creates a copy
827 assert_eq!(command, copied);
828 }
829
830 #[test]
831 fn test_resource_type_all() {
832 let all_types = ResourceType::all();
833 assert_eq!(all_types.len(), 6);
834 assert_eq!(all_types[0], ResourceType::Agent);
835 assert_eq!(all_types[1], ResourceType::Snippet);
836 assert_eq!(all_types[2], ResourceType::Command);
837 assert_eq!(all_types[3], ResourceType::McpServer);
838 assert_eq!(all_types[4], ResourceType::Script);
839 assert_eq!(all_types[5], ResourceType::Hook);
840
841 // Test that we can iterate
842 let mut count = 0;
843 for _ in ResourceType::all() {
844 count += 1;
845 }
846 assert_eq!(count, 6);
847 }
848}