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(), ".claude/agents");
43//! assert_eq!(snippet_type.default_directory(), ".claude/agpm/snippets");
44//! assert_eq!(script_type.default_directory(), ".claude/agpm/scripts");
45//! assert_eq!(hook_type.default_directory(), ".claude/agpm/hooks");
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/agpm/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/agpm/scripts`
93/// - **Common Use Cases**: Validation scripts, automation tools, hook executables
94///
95/// ## Hook
96/// - **Purpose**: Event-based automation configurations for Claude Code
97/// - **Default Directory**: `.claude/agpm/hooks`
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(), ".claude/agents");
137///
138/// let snippet = ResourceType::Snippet;
139/// assert_eq!(snippet.default_directory(), ".claude/agpm/snippets");
140///
141/// let script = ResourceType::Script;
142/// assert_eq!(script.default_directory(), ".claude/agpm/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/agpm/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`] → `".claude/agents"`
261 /// - [`Snippet`] → `".claude/agpm/snippets"`
262 /// - [`Command`] → `.claude/commands`
263 /// - [`McpServer`] → `.claude/agpm/mcp-servers`
264 /// - [`Script`] → `.claude/agpm/scripts`
265 /// - [`Hook`] → `.claude/agpm/hooks`
266 ///
267 /// # Examples
268 ///
269 /// ```rust,no_run
270 /// use agpm_cli::core::ResourceType;
271 ///
272 /// assert_eq!(ResourceType::Agent.default_directory(), ".claude/agents");
273 /// assert_eq!(ResourceType::Snippet.default_directory(), ".claude/agpm/snippets");
274 /// assert_eq!(ResourceType::Command.default_directory(), ".claude/commands");
275 /// assert_eq!(ResourceType::McpServer.default_directory(), ".claude/agpm/mcp-servers");
276 /// assert_eq!(ResourceType::Script.default_directory(), ".claude/agpm/scripts");
277 /// assert_eq!(ResourceType::Hook.default_directory(), ".claude/agpm/hooks");
278 /// ```
279 ///
280 /// # Note
281 ///
282 /// This is just the default convention. Users can install resources to any
283 /// directory by specifying custom paths in their manifest files.
284 ///
285 /// [`Agent`]: ResourceType::Agent
286 /// [`Snippet`]: ResourceType::Snippet
287 /// [`Command`]: ResourceType::Command
288 /// [`McpServer`]: ResourceType::McpServer
289 /// [`Script`]: ResourceType::Script
290 /// [`Hook`]: ResourceType::Hook
291 #[must_use]
292 pub const fn default_directory(&self) -> &str {
293 match self {
294 Self::Agent => ".claude/agents",
295 Self::Snippet => ".claude/agpm/snippets",
296 Self::Command => ".claude/commands",
297 Self::McpServer => ".claude/agpm/mcp-servers",
298 Self::Script => ".claude/agpm/scripts",
299 Self::Hook => ".claude/agpm/hooks",
300 }
301 }
302}
303
304impl std::fmt::Display for ResourceType {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 Self::Agent => write!(f, "agent"),
308 Self::Snippet => write!(f, "snippet"),
309 Self::Command => write!(f, "command"),
310 Self::McpServer => write!(f, "mcp-server"),
311 Self::Script => write!(f, "script"),
312 Self::Hook => write!(f, "hook"),
313 }
314 }
315}
316
317impl std::str::FromStr for ResourceType {
318 type Err = crate::core::AgpmError;
319
320 fn from_str(s: &str) -> Result<Self, Self::Err> {
321 match s.to_lowercase().as_str() {
322 "agent" | "agents" => Ok(Self::Agent),
323 "snippet" | "snippets" => Ok(Self::Snippet),
324 "command" | "commands" => Ok(Self::Command),
325 "mcp-server" | "mcp-servers" | "mcpserver" | "mcp" => Ok(Self::McpServer),
326 "script" | "scripts" => Ok(Self::Script),
327 "hook" | "hooks" => Ok(Self::Hook),
328 _ => Err(crate::core::AgpmError::InvalidResourceType {
329 resource_type: s.to_string(),
330 }),
331 }
332 }
333}
334
335/// Base trait defining the interface for all AGPM resources
336///
337/// This trait provides a common interface for different types of resources (agents, snippets)
338/// managed by AGPM. It abstracts the core operations that can be performed on any resource,
339/// including validation, installation, and metadata access.
340///
341/// # Design Principles
342///
343/// - **Type Safety**: Each resource has a specific [`ResourceType`]
344/// - **Validation**: Resources can validate their own structure and dependencies
345/// - **Installation**: Resources know how to install themselves to target locations
346/// - **Metadata**: Resources provide structured metadata for tooling and display
347/// - **Flexibility**: Resources can be profiled or configured during installation
348///
349/// # Implementation Requirements
350///
351/// Implementors of this trait should:
352/// - Provide meaningful error messages in validation failures
353/// - Support atomic installation operations (no partial installs on failure)
354/// - Generate deterministic installation paths
355/// - Include rich metadata for resource discovery and management
356///
357/// # Examples
358///
359/// ## Basic Resource Usage Pattern
360///
361/// ```rust,no_run
362/// use agpm_cli::core::{Resource, ResourceType};
363/// use anyhow::Result;
364/// use std::path::Path;
365///
366/// fn process_resource(resource: &dyn Resource) -> Result<()> {
367/// // Get basic information
368/// println!("Processing resource: {}", resource.name());
369/// println!("Type: {}", resource.resource_type());
370///
371/// if let Some(description) = resource.description() {
372/// println!("Description: {}", description);
373/// }
374///
375/// // Validate the resource
376/// resource.validate()?;
377///
378/// // Install to default location
379/// let target = Path::new("./resources");
380/// let install_path = resource.install_path(target);
381/// resource.install(&install_path, None)?;
382///
383/// Ok(())
384/// }
385/// ```
386///
387/// ## Metadata Extraction
388///
389/// ```rust,no_run
390/// use agpm_cli::core::Resource;
391/// use anyhow::Result;
392///
393/// fn extract_metadata(resource: &dyn Resource) -> Result<()> {
394/// let metadata = resource.metadata()?;
395///
396/// // Metadata is JSON Value for flexibility
397/// if let Some(version) = metadata.get("version") {
398/// println!("Resource version: {}", version);
399/// }
400///
401/// if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
402/// println!("Tags: {:?}", tags);
403/// }
404///
405/// Ok(())
406/// }
407/// ```
408///
409/// # Trait Object Usage
410///
411/// The trait is object-safe and can be used as a trait object:
412///
413/// ```rust,no_run
414/// use agpm_cli::core::Resource;
415/// use std::any::Any;
416///
417/// fn handle_resource(resource: Box<dyn Resource>) {
418/// println!("Handling resource: {}", resource.name());
419///
420/// // Can be downcasted to concrete types if needed
421/// let any = resource.as_any();
422/// // ... downcasting logic
423/// }
424/// ```
425pub trait Resource {
426 /// Get the unique name identifier for this resource
427 ///
428 /// The name is used to identify the resource in manifests, lockfiles,
429 /// and CLI operations. It should be unique within a project's namespace.
430 ///
431 /// # Returns
432 ///
433 /// A string slice containing the resource name
434 ///
435 /// # Examples
436 ///
437 /// ```rust,no_run
438 /// use agpm_cli::core::Resource;
439 ///
440 /// fn print_resource_info(resource: &dyn Resource) {
441 /// println!("Resource name: {}", resource.name());
442 /// }
443 /// ```
444 fn name(&self) -> &str;
445
446 /// Get the resource type classification
447 ///
448 /// Returns the [`ResourceType`] enum value that identifies what kind of
449 /// resource this is (Agent, Snippet, etc.).
450 ///
451 /// # Returns
452 ///
453 /// The [`ResourceType`] for this resource
454 ///
455 /// # Examples
456 ///
457 /// ```rust,no_run
458 /// use agpm_cli::core::{Resource, ResourceType};
459 ///
460 /// fn categorize_resource(resource: &dyn Resource) {
461 /// match resource.resource_type() {
462 /// ResourceType::Agent => println!("This is an AI agent"),
463 /// ResourceType::Snippet => println!("This is a code snippet"),
464 /// ResourceType::Command => println!("This is a Claude Code command"),
465 /// ResourceType::McpServer => println!("This is an MCP server"),
466 /// ResourceType::Script => println!("This is an executable script"),
467 /// ResourceType::Hook => println!("This is a hook configuration"),
468 /// }
469 /// }
470 /// ```
471 fn resource_type(&self) -> ResourceType;
472
473 /// Get the human-readable description of this resource
474 ///
475 /// Returns an optional description that explains what the resource does
476 /// or how it should be used. This is typically displayed in resource
477 /// listings and help text.
478 ///
479 /// # Returns
480 ///
481 /// - `Some(description)` if the resource has a description
482 /// - `None` if no description is available
483 ///
484 /// # Examples
485 ///
486 /// ```rust,no_run
487 /// use agpm_cli::core::Resource;
488 ///
489 /// fn show_resource_details(resource: &dyn Resource) {
490 /// println!("Name: {}", resource.name());
491 /// if let Some(desc) = resource.description() {
492 /// println!("Description: {}", desc);
493 /// } else {
494 /// println!("No description available");
495 /// }
496 /// }
497 /// ```
498 fn description(&self) -> Option<&str>;
499
500 /// Get the list of dependencies required by this resource
501 ///
502 /// Returns dependencies that must be installed before this resource
503 /// can function properly. Dependencies are resolved automatically
504 /// during installation.
505 ///
506 /// # Returns
507 ///
508 /// - `Some(dependencies)` if the resource has dependencies
509 /// - `None` if the resource has no dependencies
510 ///
511 /// # Examples
512 ///
513 /// ```rust,no_run
514 /// use agpm_cli::core::Resource;
515 ///
516 /// fn check_dependencies(resource: &dyn Resource) {
517 /// if let Some(deps) = resource.dependencies() {
518 /// println!("{} has {} dependencies", resource.name(), deps.len());
519 /// for dep in deps {
520 /// // Process dependency...
521 /// }
522 /// } else {
523 /// println!("{} has no dependencies", resource.name());
524 /// }
525 /// }
526 /// ```
527 fn dependencies(&self) -> Option<&[crate::config::Dependency]>;
528
529 /// Validate the resource structure and content
530 ///
531 /// Performs comprehensive validation of the resource including:
532 /// - File structure integrity
533 /// - Content format validation
534 /// - Dependency constraint checking
535 /// - Metadata consistency
536 ///
537 /// # Returns
538 ///
539 /// - `Ok(())` if the resource is valid
540 /// - `Err(error)` with detailed validation failure information
541 ///
542 /// # Examples
543 ///
544 /// ```rust,no_run
545 /// use agpm_cli::core::Resource;
546 /// use anyhow::Result;
547 ///
548 /// fn validate_before_install(resource: &dyn Resource) -> Result<()> {
549 /// resource.validate()
550 /// .map_err(|e| anyhow::anyhow!("Resource validation failed: {}", e))?;
551 ///
552 /// println!("Resource {} is valid", resource.name());
553 /// Ok(())
554 /// }
555 /// ```
556 fn validate(&self) -> Result<()>;
557
558 /// Install the resource to the specified target path
559 ///
560 /// Performs the actual installation of the resource files to the target
561 /// location. This operation should be atomic - either it succeeds completely
562 /// or fails without making any changes.
563 ///
564 /// # Arguments
565 ///
566 /// * `target` - The directory path where the resource should be installed
567 /// * `profile` - Optional profile name for customized installation (may be unused)
568 ///
569 /// # Returns
570 ///
571 /// - `Ok(())` if installation succeeds
572 /// - `Err(error)` if installation fails with detailed error information
573 ///
574 /// # Examples
575 ///
576 /// ```rust,no_run
577 /// use agpm_cli::core::Resource;
578 /// use std::path::Path;
579 /// use anyhow::Result;
580 ///
581 /// fn install_resource(resource: &dyn Resource) -> Result<()> {
582 /// let target = Path::new("./installed-resources");
583 ///
584 /// // Validate first
585 /// resource.validate()?;
586 ///
587 /// // Install without profile
588 /// resource.install(target, None)?;
589 ///
590 /// println!("Successfully installed {}", resource.name());
591 /// Ok(())
592 /// }
593 /// ```
594 fn install(&self, target: &Path, profile: Option<&str>) -> Result<()>;
595
596 /// Calculate the installation path for this resource
597 ///
598 /// Determines where this resource would be installed relative to a base
599 /// directory. This is used for path planning and conflict detection.
600 ///
601 /// # Arguments
602 ///
603 /// * `base` - The base directory for installation
604 ///
605 /// # Returns
606 ///
607 /// The full path where this resource would be installed
608 ///
609 /// # Examples
610 ///
611 /// ```rust,no_run
612 /// use agpm_cli::core::Resource;
613 /// use std::path::Path;
614 ///
615 /// fn check_install_location(resource: &dyn Resource) {
616 /// let base = Path::new("/project/resources");
617 /// let install_path = resource.install_path(base);
618 ///
619 /// println!("{} would be installed to: {}",
620 /// resource.name(),
621 /// install_path.display()
622 /// );
623 /// }
624 /// ```
625 fn install_path(&self, base: &Path) -> std::path::PathBuf;
626
627 /// Get structured metadata for this resource as JSON
628 ///
629 /// Returns resource metadata in a flexible JSON format that can include
630 /// version information, tags, author details, and other custom fields.
631 /// This metadata is used for resource discovery, filtering, and display.
632 ///
633 /// # Returns
634 ///
635 /// - `Ok(json_value)` containing the metadata
636 /// - `Err(error)` if metadata cannot be generated or parsed
637 ///
638 /// # Metadata Structure
639 ///
640 /// While flexible, metadata typically includes:
641 /// - `name`: Resource name
642 /// - `type`: Resource type
643 /// - `version`: Version information
644 /// - `description`: Human-readable description
645 /// - `tags`: Array of classification tags
646 /// - `dependencies`: Dependency information
647 ///
648 /// # Examples
649 ///
650 /// ```rust,no_run
651 /// use agpm_cli::core::Resource;
652 /// use anyhow::Result;
653 ///
654 /// fn show_resource_metadata(resource: &dyn Resource) -> Result<()> {
655 /// let metadata = resource.metadata()?;
656 ///
657 /// if let Some(version) = metadata.get("version") {
658 /// println!("Version: {}", version);
659 /// }
660 ///
661 /// if let Some(tags) = metadata.get("tags").and_then(|t| t.as_array()) {
662 /// print!("Tags: ");
663 /// for tag in tags {
664 /// print!("{} ", tag.as_str().unwrap_or("?"));
665 /// }
666 /// println!();
667 /// }
668 ///
669 /// Ok(())
670 /// }
671 /// ```
672 fn metadata(&self) -> Result<serde_json::Value>;
673
674 /// Get a reference to this resource as [`std::any::Any`] for downcasting
675 ///
676 /// This method enables downcasting from the [`Resource`] trait object to
677 /// concrete resource implementations when needed for type-specific operations.
678 ///
679 /// # Returns
680 ///
681 /// A reference to this resource as [`std::any::Any`]
682 ///
683 /// # Examples
684 ///
685 /// ```rust,no_run
686 /// use agpm_cli::core::Resource;
687 /// use std::any::Any;
688 ///
689 /// // Hypothetical concrete resource type
690 /// struct MyAgent {
691 /// name: String,
692 /// // ... other fields
693 /// }
694 ///
695 /// fn try_downcast_to_agent(resource: &dyn Resource) {
696 /// let any = resource.as_any();
697 ///
698 /// if let Some(agent) = any.downcast_ref::<MyAgent>() {
699 /// println!("Successfully downcasted to MyAgent: {}", agent.name);
700 /// } else {
701 /// println!("Resource is not a MyAgent type");
702 /// }
703 /// }
704 /// ```
705 fn as_any(&self) -> &dyn std::any::Any;
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn test_resource_type_default_directory() {
714 assert_eq!(ResourceType::Agent.default_directory(), ".claude/agents");
715 assert_eq!(ResourceType::Snippet.default_directory(), ".claude/agpm/snippets");
716 assert_eq!(ResourceType::Command.default_directory(), ".claude/commands");
717 assert_eq!(ResourceType::McpServer.default_directory(), ".claude/agpm/mcp-servers");
718 assert_eq!(ResourceType::Script.default_directory(), ".claude/agpm/scripts");
719 assert_eq!(ResourceType::Hook.default_directory(), ".claude/agpm/hooks");
720 }
721
722 #[test]
723 fn test_resource_type_display() {
724 assert_eq!(ResourceType::Agent.to_string(), "agent");
725 assert_eq!(ResourceType::Snippet.to_string(), "snippet");
726 assert_eq!(ResourceType::Command.to_string(), "command");
727 assert_eq!(ResourceType::McpServer.to_string(), "mcp-server");
728 assert_eq!(ResourceType::Script.to_string(), "script");
729 assert_eq!(ResourceType::Hook.to_string(), "hook");
730 }
731
732 #[test]
733 fn test_resource_type_from_str() {
734 use std::str::FromStr;
735
736 assert_eq!(ResourceType::from_str("agent").unwrap(), ResourceType::Agent);
737 assert_eq!(ResourceType::from_str("snippet").unwrap(), ResourceType::Snippet);
738 assert_eq!(ResourceType::from_str("AGENT").unwrap(), ResourceType::Agent);
739 assert_eq!(ResourceType::from_str("Snippet").unwrap(), ResourceType::Snippet);
740 assert_eq!(ResourceType::from_str("command").unwrap(), ResourceType::Command);
741 assert_eq!(ResourceType::from_str("COMMAND").unwrap(), ResourceType::Command);
742 assert_eq!(ResourceType::from_str("mcp-server").unwrap(), ResourceType::McpServer);
743 assert_eq!(ResourceType::from_str("MCP").unwrap(), ResourceType::McpServer);
744 assert_eq!(ResourceType::from_str("script").unwrap(), ResourceType::Script);
745 assert_eq!(ResourceType::from_str("SCRIPT").unwrap(), ResourceType::Script);
746 assert_eq!(ResourceType::from_str("hook").unwrap(), ResourceType::Hook);
747 assert_eq!(ResourceType::from_str("HOOK").unwrap(), ResourceType::Hook);
748
749 assert!(ResourceType::from_str("invalid").is_err());
750 }
751
752 #[test]
753 fn test_resource_type_serialization() {
754 let agent = ResourceType::Agent;
755 let json = serde_json::to_string(&agent).unwrap();
756 assert_eq!(json, "\"agent\"");
757
758 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
759 assert_eq!(deserialized, ResourceType::Agent);
760
761 // Test command serialization
762 let command = ResourceType::Command;
763 let json = serde_json::to_string(&command).unwrap();
764 assert_eq!(json, "\"command\"");
765
766 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
767 assert_eq!(deserialized, ResourceType::Command);
768
769 // Test mcp-server serialization
770 let mcp_server = ResourceType::McpServer;
771 let json = serde_json::to_string(&mcp_server).unwrap();
772 assert_eq!(json, "\"mcp-server\"");
773
774 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
775 assert_eq!(deserialized, ResourceType::McpServer);
776
777 // Test script serialization
778 let script = ResourceType::Script;
779 let json = serde_json::to_string(&script).unwrap();
780 assert_eq!(json, "\"script\"");
781
782 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
783 assert_eq!(deserialized, ResourceType::Script);
784
785 // Test hook serialization
786 let hook = ResourceType::Hook;
787 let json = serde_json::to_string(&hook).unwrap();
788 assert_eq!(json, "\"hook\"");
789
790 let deserialized: ResourceType = serde_json::from_str(&json).unwrap();
791 assert_eq!(deserialized, ResourceType::Hook);
792 }
793
794 #[test]
795 fn test_resource_type_equality() {
796 assert_eq!(ResourceType::Command, ResourceType::Command);
797 assert_ne!(ResourceType::Command, ResourceType::Agent);
798 assert_ne!(ResourceType::Command, ResourceType::Snippet);
799 assert_eq!(ResourceType::McpServer, ResourceType::McpServer);
800 assert_ne!(ResourceType::McpServer, ResourceType::Agent);
801 assert_eq!(ResourceType::Script, ResourceType::Script);
802 assert_ne!(ResourceType::Script, ResourceType::Hook);
803 assert_eq!(ResourceType::Hook, ResourceType::Hook);
804 assert_ne!(ResourceType::Hook, ResourceType::Agent);
805 }
806
807 #[test]
808 fn test_resource_type_copy() {
809 let command = ResourceType::Command;
810 let copied = command; // ResourceType implements Copy, so this creates a copy
811 assert_eq!(command, copied);
812 }
813
814 #[test]
815 fn test_resource_type_all() {
816 let all_types = ResourceType::all();
817 assert_eq!(all_types.len(), 6);
818 assert_eq!(all_types[0], ResourceType::Agent);
819 assert_eq!(all_types[1], ResourceType::Snippet);
820 assert_eq!(all_types[2], ResourceType::Command);
821 assert_eq!(all_types[3], ResourceType::McpServer);
822 assert_eq!(all_types[4], ResourceType::Script);
823 assert_eq!(all_types[5], ResourceType::Hook);
824
825 // Test that we can iterate
826 let mut count = 0;
827 for _ in ResourceType::all() {
828 count += 1;
829 }
830 assert_eq!(count, 6);
831 }
832}