diff --git a/main.go b/main.go index ede2933..e404ca0 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { mode, _ := cmd.Flags().GetString("mode") verbose, _ := cmd.Flags().GetBool("verbose") appstore, _ := cmd.Flags().GetBool("appstore") + filter, _ := cmd.Flags().GetString("filter") log := logrus.New() if verbose { @@ -35,13 +36,14 @@ func main() { } mpr.SetLogger(log) - mpr.ExportModel(inputDirectory, outputDirectory, raw, mode, appstore) + mpr.ExportModel(inputDirectory, outputDirectory, raw, mode, appstore, filter) }, } cmdExportModel.Flags().StringP("input", "i", ".", "Path to directory or mpr file to export. If it's a directory, all mpr files will be exported") cmdExportModel.Flags().StringP("output", "o", "modelsource", "Path to directory to write the yaml files. If it doesn't exist, it will be created") cmdExportModel.Flags().StringP("mode", "m", "basic", "Export mode. Valid options: basic, advanced") + cmdExportModel.Flags().StringP("filter", "f", "", "Regex pattern to filter units by name. Only units with names matching the pattern will be exported") cmdExportModel.Flags().Bool("raw", false, "If set, the output yaml will include all attributes as they are in the model. Otherwise, only the relevant attributes are included. You should never need this. Only useful when you are developing new functionalities for this tool.") cmdExportModel.Flags().Bool("appstore", false, "If set, appstore modules will be included in the output") cmdExportModel.Flags().Bool("verbose", false, "Turn on for debug logs") diff --git a/mpr/microflow_test.go b/mpr/microflow_test.go index e7375dd..b910b56 100644 --- a/mpr/microflow_test.go +++ b/mpr/microflow_test.go @@ -13,7 +13,7 @@ import ( func TestMPRMicroflow(t *testing.T) { t.Run("microflow-simple", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced"); err != nil { + if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -42,7 +42,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-with-split", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced"); err != nil { + if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -71,7 +71,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-split-then-merge", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced"); err != nil { + if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } diff --git a/mpr/mpr.go b/mpr/mpr.go index 811e0df..6a36e21 100644 --- a/mpr/mpr.go +++ b/mpr/mpr.go @@ -8,6 +8,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" "gopkg.in/yaml.v3" @@ -30,7 +31,7 @@ const ( MaxComponentLength = 100 ) -func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode string, appstore bool) error { +func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode string, appstore bool, filter string) error { // create tmp directory in user tmp directory tmpDir := filepath.Join(os.TempDir(), "mxlint") @@ -63,8 +64,10 @@ func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode s return fmt.Errorf("error exporting metadata: %v", err) } - if err := exportUnits(inputDirectory, tmpDir, raw, mode); err != nil { - return fmt.Errorf("error exporting units: %v", err) + if filter != "^Metadata$" { + if err := exportUnits(inputDirectory, tmpDir, raw, mode, filter); err != nil { + return fmt.Errorf("error exporting units: %v", err) + } } // remove output directory if it exists @@ -452,7 +455,7 @@ func getMxDocuments(units []MxUnit, folders []MxFolder, mode string) ([]MxDocume return documents, nil } -func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode string) error { +func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode string, filter string) error { log.Debugf("Exporting units from %s to %s", inputDirectory, outputDirectory) units, err := getMxUnits(inputDirectory) @@ -469,7 +472,25 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s return fmt.Errorf("error getting documents: %v", err) } + // Compile the filter regex if provided + var filterRegex *regexp.Regexp + if filter != "" { + filterRegex, err = regexp.Compile(filter) + if err != nil { + return fmt.Errorf("invalid filter regex pattern: %v", err) + } + log.Infof("Applying filter: %s", filter) + } + + exportedCount := 0 for _, document := range documents { + // Apply filter if provided + if filterRegex != nil { + if !filterRegex.MatchString(document.Name) { + log.Debugf("Skipping document '%s' (does not match filter)", document.Name) + continue + } + } // write document // Sanitize the document path to handle invalid characters sanitizedPath := sanitizePath(document.Path) @@ -510,6 +531,11 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s log.Errorf("Error writing file: %v", err) return err } + exportedCount++ + } + + if filterRegex != nil { + log.Infof("Exported %d documents matching filter (out of %d total)", exportedCount, len(documents)) } return nil diff --git a/mpr/mpr_v1_test.go b/mpr/mpr_v1_test.go index 10d2727..adc8aba 100644 --- a/mpr/mpr_v1_test.go +++ b/mpr/mpr_v1_test.go @@ -42,7 +42,7 @@ func TestMPRMetadata(t *testing.T) { func TestMPRUnits(t *testing.T) { t.Run("single-mpr", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic"); err != nil { + if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) @@ -51,7 +51,7 @@ func TestMPRUnits(t *testing.T) { func TestIDAttributesExclusion(t *testing.T) { t.Run("verify-id-attributes-excluded", func(t *testing.T) { // Export units with ID attributes excluded - if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic"); err != nil { + if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file: %v", err) return } @@ -98,3 +98,86 @@ func TestIDAttributesExclusion(t *testing.T) { } }) } + +func TestFilterMetadataOnly(t *testing.T) { + t.Run("filter-metadata-exact-match", func(t *testing.T) { + // Clean up test directory + testDir := "./../tmp-filter-metadata" + os.RemoveAll(testDir) + defer os.RemoveAll(testDir) + + // Export with filter ^Metadata$ + // According to the code, when filter is "^Metadata$", only metadata is exported, no units + if err := ExportModel("./../resources/app-mpr-v1", testDir, false, "basic", false, "^Metadata$"); err != nil { + t.Errorf("Failed to export with Metadata filter: %v", err) + return + } + + // Check that Metadata.yaml exists + metadataPath := filepath.Join(testDir, "Metadata.yaml") + if _, err := os.Stat(metadataPath); os.IsNotExist(err) { + t.Errorf("Metadata.yaml was not created") + return + } + + // Check that no other files/directories were created (since filter is ^Metadata$ and units are skipped) + entries, err := os.ReadDir(testDir) + if err != nil { + t.Errorf("Failed to read test directory: %v", err) + return + } + + // Should only have Metadata.yaml + if len(entries) != 1 { + t.Errorf("Expected only Metadata.yaml, but found %d entries", len(entries)) + return + } + + if entries[0].Name() != "Metadata.yaml" { + t.Errorf("Expected Metadata.yaml, but found %s", entries[0].Name()) + } + }) +} + +func TestFilterConstantPattern(t *testing.T) { + t.Run("filter-constant-pattern", func(t *testing.T) { + // Clean up test directory + testDir := "./../tmp-filter-constant" + os.RemoveAll(testDir) + defer os.RemoveAll(testDir) + + // Export with filter ^Constant.* + // This pattern won't match any documents in the test data, so we should get only metadata + if err := ExportModel("./../resources/app-mpr-v1", testDir, false, "basic", false, "^Constant.*"); err != nil { + t.Errorf("Failed to export with Constant filter: %v", err) + return + } + + // Check that Metadata.yaml exists (always exported) + metadataPath := filepath.Join(testDir, "Metadata.yaml") + if _, err := os.Stat(metadataPath); os.IsNotExist(err) { + t.Errorf("Metadata.yaml was not created") + return + } + + // Check that no module directories were created (since no documents match the filter) + entries, err := os.ReadDir(testDir) + if err != nil { + t.Errorf("Failed to read test directory: %v", err) + return + } + + // Should only have Metadata.yaml since no documents match ^Constant.* + if len(entries) != 1 { + t.Errorf("Expected only Metadata.yaml when no documents match filter, but found %d entries", len(entries)) + for _, entry := range entries { + t.Logf("Found entry: %s", entry.Name()) + } + return + } + + if entries[0].Name() != "Metadata.yaml" { + t.Errorf("Expected Metadata.yaml, but found %s", entries[0].Name()) + } + }) +} diff --git a/mpr/mpr_v2_test.go b/mpr/mpr_v2_test.go index 67415b5..5ec4d07 100644 --- a/mpr/mpr_v2_test.go +++ b/mpr/mpr_v2_test.go @@ -39,7 +39,7 @@ func TestMPRV2Metadata(t *testing.T) { func TestMPRV2Units(t *testing.T) { t.Run("single-mpr", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v2", "./../tmp", false, "basic"); err != nil { + if err := exportUnits("./../resources/app-mpr-v2", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) diff --git a/serve/serve.go b/serve/serve.go index 681ffc4..7ace58b 100644 --- a/serve/serve.go +++ b/serve/serve.go @@ -189,7 +189,7 @@ func runServe(cmd *cobra.Command, args []string) { }() log.Infof("Running export-model and lint") - err := mpr.ExportModel(inputDirectory, outputDirectory, false, mode, false) + err := mpr.ExportModel(inputDirectory, outputDirectory, false, mode, false, "") if err != nil { log.Warningf("Export failed: %s", err) resultMutex.Lock()