cuenv_workspaces/core/traits.rs
1//! Core traits for workspace discovery, lockfile parsing, and dependency resolution.
2
3use crate::core::types::{DependencyRef, LockfileEntry, Workspace, WorkspaceMember};
4use crate::error::Result;
5use std::path::Path;
6
7/// Type alias for dependency graphs using petgraph.
8///
9/// The graph nodes are [`DependencyRef`]s representing packages, and edges
10/// represent dependency relationships (no edge data needed).
11pub type DependencyGraph = petgraph::Graph<DependencyRef, ()>;
12
13/// Discovers workspace configuration from a root directory.
14///
15/// Implementations of this trait handle the package manager-specific logic
16/// for finding workspace members, validating their structure, and building
17/// a complete workspace representation.
18///
19/// # Example
20///
21/// ```rust,ignore
22/// use cuenv_workspaces::{WorkspaceDiscovery, Workspace};
23/// use std::path::Path;
24///
25/// struct CargoDiscovery;
26///
27/// impl WorkspaceDiscovery for CargoDiscovery {
28/// fn discover(&self, root: &Path) -> Result<Workspace> {
29/// // Find Cargo.toml, parse workspace members, etc.
30/// todo!()
31/// }
32///
33/// fn find_members(&self, root: &Path) -> Result<Vec<WorkspaceMember>> {
34/// // Use glob patterns from Cargo.toml to find member crates
35/// todo!()
36/// }
37///
38/// fn validate_member(&self, member_path: &Path) -> Result<bool> {
39/// // Check for Cargo.toml in member directory
40/// todo!()
41/// }
42/// }
43/// ```
44pub trait WorkspaceDiscovery {
45 /// Discovers the complete workspace configuration from a root directory.
46 ///
47 /// This method should:
48 /// 1. Locate the workspace configuration file (e.g., `package.json`, `Cargo.toml`)
49 /// 2. Parse workspace member patterns
50 /// 3. Find all workspace members
51 /// 4. Validate each member
52 /// 5. Build and return a complete [`Workspace`]
53 ///
54 /// # Errors
55 ///
56 /// Returns an error if:
57 /// - The workspace configuration file is not found
58 /// - The configuration is invalid
59 /// - Members cannot be discovered or validated
60 fn discover(&self, root: &Path) -> Result<Workspace>;
61
62 /// Finds all workspace members using glob patterns or other discovery mechanisms.
63 ///
64 /// This method should scan the workspace root for members based on the
65 /// package manager's conventions (e.g., `packages/*` for npm workspaces,
66 /// `members = [...]` for Cargo).
67 ///
68 /// # Errors
69 ///
70 /// Returns an error if member discovery fails (e.g., invalid glob patterns,
71 /// I/O errors).
72 fn find_members(&self, root: &Path) -> Result<Vec<WorkspaceMember>>;
73
74 /// Validates that a potential workspace member has the required manifest files.
75 ///
76 /// For example:
77 /// - npm/Bun/pnpm/Yarn: Check for `package.json`
78 /// - Cargo: Check for `Cargo.toml`
79 ///
80 /// # Errors
81 ///
82 /// Returns an error if validation cannot be performed (e.g., I/O errors).
83 /// Returns `Ok(false)` if the member is invalid, `Ok(true)` if valid.
84 fn validate_member(&self, member_path: &Path) -> Result<bool>;
85}
86
87/// Parses package manager-specific lockfiles into structured entries.
88///
89/// Each package manager has its own lockfile format:
90/// - npm: `package-lock.json`
91/// - Bun: `bun.lock` (JSONC format)
92/// - pnpm: `pnpm-lock.yaml`
93/// - Yarn Classic: `yarn.lock`
94/// - Yarn Modern: `yarn.lock` (different format)
95/// - Cargo: `Cargo.lock`
96///
97/// Implementations handle the parsing logic for these formats.
98///
99/// # Example
100///
101/// ```rust,ignore
102/// use cuenv_workspaces::{LockfileParser, LockfileEntry};
103/// use std::path::Path;
104///
105/// struct NpmLockfileParser;
106///
107/// impl LockfileParser for NpmLockfileParser {
108/// fn parse(&self, lockfile_path: &Path) -> Result<Vec<LockfileEntry>> {
109/// // Parse package-lock.json and convert to LockfileEntry structs
110/// todo!()
111/// }
112///
113/// fn supports_lockfile(&self, path: &Path) -> bool {
114/// path.file_name()
115/// .and_then(|n| n.to_str())
116/// .map(|n| n == "package-lock.json")
117/// .unwrap_or(false)
118/// }
119///
120/// fn lockfile_name(&self) -> &str {
121/// "package-lock.json"
122/// }
123/// }
124/// ```
125pub trait LockfileParser {
126 /// Parses a lockfile into structured entries.
127 ///
128 /// Each entry represents a resolved dependency with its version, source,
129 /// checksum, and direct dependencies.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if:
134 /// - The lockfile cannot be read
135 /// - The lockfile format is invalid or corrupted
136 /// - Required fields are missing
137 fn parse(&self, lockfile_path: &Path) -> Result<Vec<LockfileEntry>>;
138
139 /// Checks if this parser supports the given lockfile path.
140 ///
141 /// This is typically a simple filename check, but may involve inspecting
142 /// file contents for formats that can't be distinguished by name alone.
143 fn supports_lockfile(&self, path: &Path) -> bool;
144
145 /// Returns the expected lockfile name for this parser.
146 ///
147 /// For example: `"package-lock.json"`, `"Cargo.lock"`, `"pnpm-lock.yaml"`.
148 fn lockfile_name(&self) -> &str;
149}
150
151/// Builds dependency graphs from workspace and lockfile data.
152///
153/// This trait combines workspace-internal dependencies (workspace protocol references)
154/// with external dependencies from lockfiles to build a complete dependency graph.
155///
156/// # Example
157///
158/// ```rust,ignore
159/// use cuenv_workspaces::{DependencyResolver, Workspace, LockfileEntry};
160///
161/// struct NpmDependencyResolver;
162///
163/// impl DependencyResolver for NpmDependencyResolver {
164/// fn resolve_dependencies(
165/// &self,
166/// workspace: &Workspace,
167/// lockfile: &[LockfileEntry],
168/// ) -> Result<DependencyGraph> {
169/// // Build graph with both workspace and external dependencies
170/// todo!()
171/// }
172///
173/// fn resolve_workspace_deps(&self, workspace: &Workspace) -> Result<Vec<DependencyRef>> {
174/// // Find dependencies using "workspace:*" protocol
175/// todo!()
176/// }
177///
178/// fn resolve_external_deps(&self, lockfile: &[LockfileEntry]) -> Result<Vec<DependencyRef>> {
179/// // Extract all external dependencies from lockfile
180/// todo!()
181/// }
182///
183/// fn detect_workspace_protocol(&self, spec: &str) -> bool {
184/// spec.starts_with("workspace:")
185/// }
186/// }
187/// ```
188pub trait DependencyResolver {
189 /// Builds a complete dependency graph from workspace and lockfile data.
190 ///
191 /// The graph should include:
192 /// - Workspace members as nodes
193 /// - Workspace-internal dependencies (workspace protocol)
194 /// - External dependencies from the lockfile
195 /// - Edges representing dependency relationships
196 ///
197 /// # Errors
198 ///
199 /// Returns an error if:
200 /// - Dependencies cannot be resolved
201 /// - Circular dependencies are detected (if validation is enabled)
202 /// - Required dependencies are missing from the lockfile
203 fn resolve_dependencies(
204 &self,
205 workspace: &Workspace,
206 lockfile: &[LockfileEntry],
207 ) -> Result<DependencyGraph>;
208
209 /// Resolves workspace-internal dependencies.
210 ///
211 /// These are dependencies that reference other workspace members using
212 /// package manager-specific protocols:
213 /// - npm/pnpm/Yarn: `"workspace:*"`, `"workspace:^"`, etc.
214 /// - Cargo: `{ workspace = true }`
215 ///
216 /// # Errors
217 ///
218 /// Returns an error if workspace dependencies cannot be resolved.
219 fn resolve_workspace_deps(&self, workspace: &Workspace) -> Result<Vec<DependencyRef>>;
220
221 /// Resolves external dependencies from lockfile entries.
222 ///
223 /// Extracts all non-workspace dependencies from the lockfile.
224 ///
225 /// # Errors
226 ///
227 /// Returns an error if dependencies cannot be extracted or validated.
228 fn resolve_external_deps(&self, lockfile: &[LockfileEntry]) -> Result<Vec<DependencyRef>>;
229
230 /// Detects if a dependency specification uses the workspace protocol.
231 ///
232 /// # Examples
233 ///
234 /// - npm/pnpm/Yarn: `"workspace:*"` → `true`
235 /// - Cargo: Requires parsing TOML to check `workspace = true`
236 /// - Regular semver: `"^1.0.0"` → `false`
237 fn detect_workspace_protocol(&self, spec: &str) -> bool;
238}