Skip to content

Commit 652bb61

Browse files
committed
Add the ability to filter by key product areas or Drivers
1 parent 8eaefae commit 652bb61

File tree

7 files changed

+1107
-69
lines changed

7 files changed

+1107
-69
lines changed

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ audit-cli/
4646
│ ├── config/ # Configuration management
4747
│ │ ├── config.go # Config loading from file/env/args
4848
│ │ ├── config_test.go # Config tests
49-
│ │ └── url_mapping.go # URL-to-source-file mapping via Snooty Data API
49+
│ │ ├── url_mapping.go # URL-to-source-file mapping via Snooty Data API
50+
│ │ └── url_mapping_test.go # URL mapping tests
5051
│ ├── language/ # Programming language utilities
5152
│ │ ├── language.go # Language normalization, extensions, products
5253
│ │ └── language_test.go # Language tests

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,35 @@ rank,url
15651565
- `--format, -f <format>` - Output format: `text` (default), `json`, or `csv`
15661566
- `--output, -o <file>` - Output file path (default: stdout)
15671567
- `--details` - Show detailed per-product breakdown (for CSV output, includes per-product columns)
1568+
- `--filter <filter>` - Filter pages by product area (can be specified multiple times)
1569+
- `--list-drivers` - List all available driver filter options from the Snooty Data API
1570+
1571+
**Filtering:**
1572+
1573+
Use the `--filter` flag to focus on specific product areas. Multiple filters can be specified to include pages matching any filter.
1574+
1575+
Available filters:
1576+
- `search` - Pages with "atlas-search" or "search" in URL (excludes vector-search)
1577+
- `vector-search` - Pages with "vector-search" in URL
1578+
- `drivers` - All MongoDB driver documentation pages
1579+
- `driver:<name>` - Specific driver by project name (e.g., `driver:pymongo`, `driver:node`)
1580+
- `mongosh` - MongoDB Shell documentation pages
1581+
1582+
```bash
1583+
# Filter to only Atlas Search pages
1584+
./audit-cli report testable-code analytics.csv --filter search
1585+
1586+
# Filter to only PyMongo driver pages
1587+
./audit-cli report testable-code analytics.csv --filter driver:pymongo
1588+
1589+
# Filter to multiple areas (pages matching any filter are included)
1590+
./audit-cli report testable-code analytics.csv --filter drivers --filter mongosh
1591+
1592+
# List all available driver filter options
1593+
./audit-cli report testable-code --list-drivers
1594+
```
1595+
1596+
The `--list-drivers` flag queries the Snooty Data API to show all available driver project names that can be used with the `driver:<name>` filter. Results are cached for 24 hours.
15681597

15691598
**Testable Products:**
15701599

commands/report/testable-code/testable_code.go

Lines changed: 205 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ package testablecode
3838
import (
3939
"fmt"
4040
"os"
41+
"sort"
42+
"strings"
4143

4244
"github.com/grove-platform/audit-cli/internal/config"
4345
"github.com/spf13/cobra"
@@ -48,6 +50,8 @@ func NewTestableCodeCommand() *cobra.Command {
4850
var outputFormat string
4951
var showDetails bool
5052
var outputFile string
53+
var filters []string
54+
var listDrivers bool
5155

5256
cmd := &cobra.Command{
5357
Use: "testable-code <csv-file> [monorepo-path]",
@@ -74,12 +78,35 @@ Example CSV format:
7478
Testable products (have test infrastructure):
7579
- C#, Go, Java (Sync), Node.js, Python, MongoDB Shell
7680
81+
Filters (use --filter to focus on specific product areas):
82+
- search: Pages with "atlas-search" or "search" in URL (excludes vector-search)
83+
- vector-search: Pages with "vector-search" in URL
84+
- drivers: All MongoDB driver documentation pages
85+
- driver:<name>: Specific driver. Testable values include:
86+
csharp, golang, java, node, pymongo
87+
For the full list of options, use the --list-drivers flag.
88+
- mongosh: MongoDB Shell documentation pages
89+
90+
Multiple filters can be specified to include pages matching any filter.
91+
92+
Use --list-drivers to see available Driver filter options
93+
7794
Output formats:
7895
- text: Human-readable report with summary and detailed sections
7996
- json: Machine-readable JSON output
8097
- csv: Comma-separated values (summary by default, use --details for per-product breakdown)`,
81-
Args: cobra.RangeArgs(1, 2),
98+
Args: cobra.RangeArgs(0, 2),
8299
RunE: func(cmd *cobra.Command, args []string) error {
100+
// Handle --list-drivers flag
101+
if listDrivers {
102+
return runListDrivers()
103+
}
104+
105+
// Require CSV file if not listing drivers
106+
if len(args) < 1 {
107+
return fmt.Errorf("requires at least 1 arg(s), only received 0")
108+
}
109+
83110
csvPath := args[0]
84111

85112
// Get monorepo path
@@ -92,19 +119,76 @@ Output formats:
92119
return err
93120
}
94121

95-
return runTestableCode(csvPath, monorepoPath, outputFormat, showDetails, outputFile)
122+
return runTestableCode(csvPath, monorepoPath, outputFormat, showDetails, outputFile, filters)
96123
},
97124
}
98125

99126
cmd.Flags().StringVarP(&outputFormat, "format", "f", "text", "Output format: text, json, or csv")
100127
cmd.Flags().BoolVar(&showDetails, "details", false, "Show detailed per-product breakdown (for csv: one row per product per page)")
101128
cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file path (default: stdout)")
129+
cmd.Flags().StringSliceVar(&filters, "filter", nil, "Filter pages by product area (search, vector-search, drivers, driver:<name>, mongosh)")
130+
cmd.Flags().BoolVar(&listDrivers, "list-drivers", false, "List all drivers from the Snooty Data API")
102131

103132
return cmd
104133
}
105134

135+
// runListDrivers lists all drivers from the Snooty Data API.
136+
func runListDrivers() error {
137+
// Use the version that doesn't require a monorepo path
138+
urlMapping, err := config.GetURLMappingWithoutMonorepo()
139+
if err != nil {
140+
return fmt.Errorf("failed to get URL mapping: %w", err)
141+
}
142+
143+
driverSlugs := urlMapping.GetDriverSlugs()
144+
if len(driverSlugs) == 0 {
145+
fmt.Println("No drivers found in the Snooty Data API.")
146+
return nil
147+
}
148+
149+
// Build a list of driver info and sort by project name (the filter value)
150+
type driverInfo struct {
151+
projectName string
152+
slug string
153+
hasTestInfra bool
154+
}
155+
drivers := make([]driverInfo, 0, len(driverSlugs))
156+
for _, slug := range driverSlugs {
157+
projectName := urlMapping.URLSlugToProject[slug]
158+
drivers = append(drivers, driverInfo{
159+
projectName: projectName,
160+
slug: slug,
161+
hasTestInfra: TestableDrivers[projectName],
162+
})
163+
}
164+
// Sort alphabetically by project name
165+
sort.Slice(drivers, func(i, j int) bool {
166+
return drivers[i].projectName < drivers[j].projectName
167+
})
168+
169+
fmt.Println("Available driver filters:")
170+
fmt.Println("=========================")
171+
fmt.Println()
172+
fmt.Println("Use --filter driver:<name> with any of these values:")
173+
fmt.Println()
174+
for _, d := range drivers {
175+
testableMarker := ""
176+
if d.hasTestInfra {
177+
testableMarker = " (has test infrastructure)"
178+
}
179+
fmt.Printf(" --filter driver:%-20s (URL slug: %s)%s\n", d.projectName, d.slug, testableMarker)
180+
}
181+
fmt.Println()
182+
fmt.Println("Drivers with test infrastructure:")
183+
fmt.Printf(" %s\n", strings.Join(getTestableDriverNames(), ", "))
184+
fmt.Println()
185+
fmt.Println("Note: mongodb-shell is not a driver. Use --filter mongosh instead.")
186+
187+
return nil
188+
}
189+
106190
// runTestableCode is the main entry point for the testable-code command.
107-
func runTestableCode(csvPath, monorepoPath, outputFormat string, showDetails bool, outputFile string) error {
191+
func runTestableCode(csvPath, monorepoPath, outputFormat string, showDetails bool, outputFile string, filters []string) error {
108192
// Parse CSV file
109193
entries, err := ParseCSV(csvPath)
110194
if err != nil {
@@ -113,19 +197,34 @@ func runTestableCode(csvPath, monorepoPath, outputFormat string, showDetails boo
113197

114198
fmt.Fprintf(os.Stderr, "Parsed %d pages from CSV\n", len(entries))
115199

200+
// Get URL mapping early - needed for driver filters
201+
urlMapping, err := config.GetURLMapping(monorepoPath)
202+
if err != nil {
203+
return fmt.Errorf("failed to get URL mapping: %w", err)
204+
}
205+
206+
// Validate filters before applying
207+
if err := validateFilters(filters); err != nil {
208+
return err
209+
}
210+
211+
// Apply URL filters if specified
212+
if len(filters) > 0 {
213+
originalCount := len(entries)
214+
entries = filterEntries(entries, filters, urlMapping)
215+
fmt.Fprintf(os.Stderr, "Filtered to %d pages matching filter(s): %v\n", len(entries), filters)
216+
if len(entries) == 0 {
217+
fmt.Fprintf(os.Stderr, "Warning: No pages matched the specified filter(s). Original count: %d\n", originalCount)
218+
}
219+
}
220+
116221
// Load product mappings from rstspec.toml
117222
fmt.Fprintf(os.Stderr, "Loading product mappings from rstspec.toml...\n")
118223
mappings, err := LoadProductMappings()
119224
if err != nil {
120225
return fmt.Errorf("failed to load product mappings: %w", err)
121226
}
122227

123-
// Get URL mapping
124-
urlMapping, err := config.GetURLMapping(monorepoPath)
125-
if err != nil {
126-
return fmt.Errorf("failed to get URL mapping: %w", err)
127-
}
128-
129228
// Analyze each page
130229
var reports []PageReport
131230
for i, entry := range entries {
@@ -172,3 +271,100 @@ func runTestableCode(csvPath, monorepoPath, outputFormat string, showDetails boo
172271
}
173272
}
174273

274+
// filterEntries filters page entries based on the specified filters.
275+
// Returns entries that match any of the specified filters.
276+
func filterEntries(entries []PageEntry, filters []string, urlMapping *config.URLMapping) []PageEntry {
277+
var filtered []PageEntry
278+
for _, entry := range entries {
279+
if matchesAnyFilter(entry.URL, filters, urlMapping) {
280+
filtered = append(filtered, entry)
281+
}
282+
}
283+
return filtered
284+
}
285+
286+
// matchesAnyFilter checks if a URL matches any of the specified filters.
287+
func matchesAnyFilter(url string, filters []string, urlMapping *config.URLMapping) bool {
288+
for _, filter := range filters {
289+
if matchesFilter(url, filter, urlMapping) {
290+
return true
291+
}
292+
}
293+
return false
294+
}
295+
296+
// validateFilters validates that all specified filters are valid.
297+
// Returns an error if any filter is invalid.
298+
func validateFilters(filters []string) error {
299+
for _, filter := range filters {
300+
filterLower := strings.ToLower(filter)
301+
302+
// Check for driver:<name> pattern - any driver name is valid
303+
if strings.HasPrefix(filterLower, "driver:") {
304+
driverName := strings.TrimPrefix(filterLower, "driver:")
305+
// mongodb-shell should use mongosh filter since it's not a driver
306+
if driverName == "mongodb-shell" {
307+
return fmt.Errorf("invalid filter %q: mongodb-shell is not a driver, use --filter mongosh instead", filter)
308+
}
309+
// Any other driver name is valid - will just return no results if not found
310+
continue
311+
}
312+
313+
// Check known filters
314+
switch filterLower {
315+
case "search", "vector-search", "drivers", "mongosh":
316+
// Valid filters
317+
default:
318+
return fmt.Errorf("unknown filter %q.\nValid filters: search, vector-search, drivers, driver:<name>, mongosh\nUse --list-drivers to see available driver names", filter)
319+
}
320+
}
321+
return nil
322+
}
323+
324+
// getTestableDriverNames returns a sorted list of driver names with test infrastructure.
325+
func getTestableDriverNames() []string {
326+
var names []string
327+
for name := range TestableDrivers {
328+
names = append(names, name)
329+
}
330+
sort.Strings(names)
331+
return names
332+
}
333+
334+
// matchesFilter checks if a URL matches a specific filter.
335+
// Matching is case-insensitive.
336+
//
337+
// Supported filters:
338+
// - "search": matches URLs containing "atlas-search" or "search" but NOT "vector-search"
339+
// - "vector-search": matches URLs containing "vector-search"
340+
// - "drivers": matches all driver documentation URLs (excludes mongodb-shell)
341+
// - "driver:<name>": matches a specific driver by project name (e.g., driver:pymongo)
342+
// - "mongosh": matches MongoDB Shell documentation URLs
343+
func matchesFilter(url string, filter string, urlMapping *config.URLMapping) bool {
344+
urlLower := strings.ToLower(url)
345+
filterLower := strings.ToLower(filter)
346+
347+
// Check for driver:<name> pattern
348+
if strings.HasPrefix(filterLower, "driver:") {
349+
driverName := strings.TrimPrefix(filterLower, "driver:")
350+
return urlMapping.IsSpecificDriverURL(url, driverName)
351+
}
352+
353+
switch filterLower {
354+
case "search":
355+
// Match "atlas-search" or "search" but exclude "vector-search"
356+
if strings.Contains(urlLower, "vector-search") {
357+
return false
358+
}
359+
return strings.Contains(urlLower, "atlas-search") || strings.Contains(urlLower, "search")
360+
case "vector-search":
361+
return strings.Contains(urlLower, "vector-search")
362+
case "drivers":
363+
return urlMapping.IsDriverURL(url)
364+
case "mongosh":
365+
return urlMapping.IsMongoshURL(url)
366+
default:
367+
// This shouldn't happen if validateFilters was called first
368+
return false
369+
}
370+
}

0 commit comments

Comments
 (0)