@@ -38,6 +38,8 @@ package testablecode
3838import (
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:
7478Testable 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+
7794Output 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.\n Valid filters: search, vector-search, drivers, driver:<name>, mongosh\n Use --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