Skip to content

Commit 870057e

Browse files
authored
Add comprehensive documentation to PackageExtractor pattern (#3838)
1 parent 59bea31 commit 870057e

1 file changed

Lines changed: 165 additions & 15 deletions

File tree

pkg/workflow/package_extraction.go

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@
22
//
33
// # Package Extraction Framework
44
//
5-
// This file provides a generic framework for extracting package names from command strings.
6-
// The PackageExtractor type can be configured to handle different package managers
7-
// (npm, pip, uv, go, etc.) with minimal code duplication.
5+
// This file provides a reusable, configurable framework for extracting package names
6+
// from command strings. The PackageExtractor type eliminates code duplication by
7+
// providing a single abstraction that handles different package managers (npm, pip,
8+
// uv, go, etc.) with minimal configuration.
89
//
9-
// # Usage Example
10+
// # Purpose and Benefits
11+
//
12+
// The PackageExtractor pattern exists to:
13+
// - Prevent code duplication across package manager extraction functions
14+
// - Provide a consistent interface for command parsing
15+
// - Centralize package name extraction logic
16+
// - Reduce maintenance burden by having a single implementation
17+
//
18+
// # Usage Pattern
19+
//
20+
// Instead of implementing custom extraction logic, configure a PackageExtractor
21+
// with the appropriate settings for your package manager:
1022
//
1123
// extractor := PackageExtractor{
1224
// CommandNames: []string{"pip", "pip3"},
@@ -16,8 +28,70 @@
1628
// packages := extractor.ExtractPackages("pip install requests")
1729
// // Returns: []string{"requests"}
1830
//
19-
// For package-specific extraction, see npm.go, pip.go, and dependabot.go.
20-
// For validation, see validation.go.
31+
// # Package Manager Examples
32+
//
33+
// NPM (npx):
34+
//
35+
// extractor := PackageExtractor{
36+
// CommandNames: []string{"npx"},
37+
// RequiredSubcommand: "", // No subcommand needed
38+
// TrimSuffixes: "&|;",
39+
// }
40+
// packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
41+
// // Returns: []string{"@playwright/mcp@latest"}
42+
//
43+
// Python (pip):
44+
//
45+
// extractor := PackageExtractor{
46+
// CommandNames: []string{"pip", "pip3"},
47+
// RequiredSubcommand: "install", // Must have "install" subcommand
48+
// TrimSuffixes: "&|;",
49+
// }
50+
// packages := extractor.ExtractPackages("pip install requests==2.28.0")
51+
// // Returns: []string{"requests==2.28.0"}
52+
//
53+
// Go:
54+
//
55+
// installExtractor := PackageExtractor{
56+
// CommandNames: []string{"go"},
57+
// RequiredSubcommand: "install",
58+
// TrimSuffixes: "&|;",
59+
// }
60+
// packages := installExtractor.ExtractPackages("go install github.com/user/tool@v1.0.0")
61+
// // Returns: []string{"github.com/user/tool@v1.0.0"}
62+
//
63+
// Python (uv):
64+
//
65+
// extractor := PackageExtractor{
66+
// CommandNames: []string{"uvx"},
67+
// RequiredSubcommand: "",
68+
// TrimSuffixes: "&|;",
69+
// }
70+
// packages := extractor.ExtractPackages("uvx black")
71+
// // Returns: []string{"black"}
72+
//
73+
// # Best Practices
74+
//
75+
// - ALWAYS use PackageExtractor instead of reimplementing extraction logic
76+
// - Configure CommandNames for all variations of the command (e.g., ["pip", "pip3"])
77+
// - Set RequiredSubcommand only when the package manager requires it (e.g., "install" for pip)
78+
// - Include common shell operators in TrimSuffixes (typically "&|;")
79+
// - For special cases, use the exported FindPackageName method to reuse logic
80+
//
81+
// # Configuration Details
82+
//
83+
// - CommandNames: List of command names to match (case-sensitive)
84+
// - RequiredSubcommand: Subcommand that must appear before the package name
85+
// (empty string if package comes directly after command)
86+
// - TrimSuffixes: Characters to remove from the end of package names
87+
// (useful for shell operators like "&", "|", ";")
88+
//
89+
// For package-specific extraction implementations, see:
90+
// - npm.go (npx packages)
91+
// - pip.go (pip and uv packages)
92+
// - dependabot.go (go packages)
93+
//
94+
// For package validation, see validation.go.
2195
package workflow
2296

2397
import (
@@ -27,42 +101,118 @@ import (
27101
// PackageExtractor provides a configurable framework for extracting package names
28102
// from command-line strings. It can be configured to handle different package
29103
// managers (npm, pip, uv, go) by setting the appropriate command names and options.
104+
//
105+
// This type is the core of the package extraction pattern. Use it instead of
106+
// writing custom parsing logic to avoid code duplication.
107+
//
108+
// Configuration:
109+
// - Set CommandNames to all variants of the command (e.g., ["pip", "pip3"])
110+
// - Set RequiredSubcommand if the package manager requires a subcommand
111+
// (e.g., "install" for pip, "get" for go)
112+
// - Set TrimSuffixes to remove shell operators from package names
113+
// (typically "&|;")
114+
//
115+
// Examples:
116+
//
117+
// // For npx (no subcommand):
118+
// extractor := PackageExtractor{
119+
// CommandNames: []string{"npx"},
120+
// RequiredSubcommand: "",
121+
// TrimSuffixes: "&|;",
122+
// }
123+
//
124+
// // For pip (with "install" subcommand):
125+
// extractor := PackageExtractor{
126+
// CommandNames: []string{"pip", "pip3"},
127+
// RequiredSubcommand: "install",
128+
// TrimSuffixes: "&|;",
129+
// }
30130
type PackageExtractor struct {
31-
// CommandNames is the list of command names to look for (e.g., ["pip", "pip3"])
131+
// CommandNames is the list of command names to look for.
132+
// Include all variations of the command (e.g., ["pip", "pip3"]).
133+
// Matching is case-sensitive and exact.
134+
//
135+
// Examples:
136+
// - ["npx"] for npm packages
137+
// - ["pip", "pip3"] for Python packages
138+
// - ["go"] for Go packages
139+
// - ["uvx"] for uv tool packages
32140
CommandNames []string
33141

34142
// RequiredSubcommand is the subcommand that must follow the command name
35-
// (e.g., "install" for pip). If empty, the package name is expected immediately
36-
// after the command name (e.g., "npx <package>").
143+
// before the package name appears. Set to empty string if the package name
144+
// comes directly after the command.
145+
//
146+
// Examples:
147+
// - "install" for pip (pip install <package>)
148+
// - "get" for go (go get <package>)
149+
// - "" for npx (npx <package>)
37150
RequiredSubcommand string
38151

39-
// TrimSuffixes is a string of characters to trim from the end of package names
40-
// (e.g., "&|;" for shell operators)
152+
// TrimSuffixes is a string of characters to trim from the end of package names.
153+
// This is useful for removing shell operators that may appear after package names
154+
// in command strings.
155+
//
156+
// Recommended value: "&|;" (covers common shell operators)
157+
//
158+
// Examples:
159+
// - "pip install requests;" → extracts "requests" (trims ";")
160+
// - "npx playwright&" → extracts "playwright" (trims "&")
41161
TrimSuffixes string
42162
}
43163

44164
// ExtractPackages extracts package names from command strings using the configured
45165
// extraction rules. It processes multi-line command strings and returns all found
46166
// package names.
47167
//
168+
// This is the main entry point for package extraction. Call this method with your
169+
// command string(s) after configuring the PackageExtractor.
170+
//
48171
// The extraction process:
49172
// 1. Split commands by newlines
50173
// 2. Split each line into words
51-
// 3. Find command name matches
174+
// 3. Find command name matches (from CommandNames)
52175
// 4. If RequiredSubcommand is set, look for that subcommand
53176
// 5. Skip flags (words starting with -)
54177
// 6. Extract package name and trim configured suffixes
55178
// 7. Return first package found per command invocation
56179
//
57-
// Example usage:
180+
// Multi-line commands are supported:
181+
//
182+
// commands := `pip install requests
183+
// pip install numpy`
184+
// packages := extractor.ExtractPackages(commands)
185+
// // Returns: []string{"requests", "numpy"}
186+
//
187+
// Flags are automatically skipped:
188+
//
189+
// packages := extractor.ExtractPackages("pip install --upgrade requests")
190+
// // Returns: []string{"requests"}
191+
//
192+
// Shell operators are automatically trimmed:
193+
//
194+
// packages := extractor.ExtractPackages("npx playwright;")
195+
// // Returns: []string{"playwright"}
196+
//
197+
// Example usage with pip:
58198
//
59199
// extractor := PackageExtractor{
60-
// CommandNames: []string{"pip", "pip3"},
200+
// CommandNames: []string{"pip", "pip3"},
61201
// RequiredSubcommand: "install",
62-
// TrimSuffixes: "&|;",
202+
// TrimSuffixes: "&|;",
63203
// }
64204
// packages := extractor.ExtractPackages("pip install requests==2.28.0")
65205
// // Returns: []string{"requests==2.28.0"}
206+
//
207+
// Example usage with npx:
208+
//
209+
// extractor := PackageExtractor{
210+
// CommandNames: []string{"npx"},
211+
// RequiredSubcommand: "",
212+
// TrimSuffixes: "&|;",
213+
// }
214+
// packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
215+
// // Returns: []string{"@playwright/mcp@latest"}
66216
func (pe *PackageExtractor) ExtractPackages(commands string) []string {
67217
var packages []string
68218
lines := strings.Split(commands, "\n")

0 commit comments

Comments
 (0)