From e812ea6b83fdee9483cfb69055cdbcabe2c4e6cd Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Sat, 13 Jan 2024 17:09:13 +0100 Subject: [PATCH] feat: add snippet validator, fixes #101 --- extension/snippet_validator.go | 225 ++++++++++++++++++++++++++++ extension/snippet_validator_test.go | 79 ++++++++++ extension/validator.go | 2 + go.mod | 5 + go.sum | 11 ++ 5 files changed, 322 insertions(+) create mode 100644 extension/snippet_validator.go create mode 100644 extension/snippet_validator_test.go diff --git a/extension/snippet_validator.go b/extension/snippet_validator.go new file mode 100644 index 00000000..564128ea --- /dev/null +++ b/extension/snippet_validator.go @@ -0,0 +1,225 @@ +package extension + +import ( + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "strings" + + "github.com/wI2L/jsondiff" +) + +func validateStorefrontSnippets(context *ValidationContext) { + rootDir := context.Extension.GetRootDir() + if err := validateStorefrontSnippetsByPath(rootDir, context); err != nil { + return + } + + for _, extraBundle := range context.Extension.GetExtensionConfig().Build.ExtraBundles { + bundlePath := rootDir + + if extraBundle.Path != "" { + bundlePath = path.Join(bundlePath, extraBundle.Path) + } else { + bundlePath = path.Join(bundlePath, extraBundle.Name) + } + + if err := validateStorefrontSnippetsByPath(bundlePath, context); err != nil { + return + } + } +} + +func validateStorefrontSnippetsByPath(extensionRoot string, context *ValidationContext) error { + snippetFolder := path.Join(extensionRoot, "Resources", "snippet") + + if _, err := os.Stat(snippetFolder); err != nil { + return nil //nolint:nilerr + } + + dirEntries, err := os.ReadDir(snippetFolder) + if err != nil { + return err + } + + files := make([]string, 0) + + for _, dirEntry := range dirEntries { + if dirEntry.IsDir() { + continue + } + + if !strings.HasSuffix(dirEntry.Name(), ".json") { + continue + } + + files = append(files, path.Join(snippetFolder, dirEntry.Name())) + } + + if len(files) == 1 { + // We have no other file to compare against + return nil + } + + var mainFile string + + for _, file := range files { + if strings.HasSuffix(filepath.Base(file), "en-GB.json") { + mainFile = file + } + } + + if len(mainFile) == 0 { + context.AddWarning(fmt.Sprintf("No en-GB.json file found in %s, using %s", snippetFolder, files[0])) + mainFile = files[0] + } + + mainFileContent, err := os.ReadFile(mainFile) + if err != nil { + return err + } + + for _, file := range files { + // makes no sense to compare to ourself + if file == mainFile { + continue + } + + if err := compareSnippets(mainFileContent, file, context, extensionRoot); err != nil { + return err + } + } + + return nil +} + +func validateAdministrationSnippets(context *ValidationContext) { + rootDir := context.Extension.GetRootDir() + if err := validateAdministrationByPath(rootDir, context); err != nil { + return + } + + for _, extraBundle := range context.Extension.GetExtensionConfig().Build.ExtraBundles { + bundlePath := rootDir + + if extraBundle.Path != "" { + bundlePath = path.Join(bundlePath, extraBundle.Path) + } else { + bundlePath = path.Join(bundlePath, extraBundle.Name) + } + + if err := validateAdministrationByPath(bundlePath, context); err != nil { + return + } + } +} + +func validateAdministrationByPath(extensionRoot string, context *ValidationContext) error { + adminFolder := path.Join(extensionRoot, "Resources", "app", "administration") + + if _, err := os.Stat(adminFolder); err != nil { + return nil //nolint:nilerr + } + + snippetFiles := make(map[string][]string) + + err := filepath.WalkDir(adminFolder, func(path string, d os.DirEntry, err error) error { + if d.IsDir() { + return nil + } + + if filepath.Ext(path) != ".json" { + return nil + } + + containingFolder := filepath.Dir(path) + + if filepath.Base(containingFolder) != "snippet" { + return nil + } + + if _, ok := snippetFiles[containingFolder]; !ok { + snippetFiles[containingFolder] = []string{} + } + + snippetFiles[containingFolder] = append(snippetFiles[containingFolder], path) + + return nil + }) + if err != nil { + return err + } + + for folder, files := range snippetFiles { + if len(files) == 1 { + // We have no other file to compare against + continue + } + + var mainFile string + + for _, file := range files { + if strings.HasSuffix(filepath.Base(file), "en-GB.json") { + mainFile = file + } + } + + if len(mainFile) == 0 { + context.AddWarning(fmt.Sprintf("No en-GB.json file found in %s, using %s", folder, files[0])) + mainFile = files[0] + } + + mainFileContent, err := os.ReadFile(mainFile) + if err != nil { + return err + } + + for _, file := range files { + // makes no sense to compare to ourself + if file == mainFile { + continue + } + + if err := compareSnippets(mainFileContent, file, context, extensionRoot); err != nil { + return err + } + } + } + + return nil +} + +func compareSnippets(mainFile []byte, file string, context *ValidationContext, extensionRoot string) error { + checkFile, err := os.ReadFile(file) + if err != nil { + return err + } + + compare, err := jsondiff.CompareJSON(mainFile, checkFile) + if err != nil { + return err + } + + for _, diff := range compare { + normalizedPath := strings.ReplaceAll(file, extensionRoot+"/", "") + + if diff.Type == jsondiff.OperationReplace && reflect.TypeOf(diff.OldValue) != reflect.TypeOf(diff.Value) { + context.AddError(fmt.Sprintf("Snippet file: %s, key: %s, has the type %s, but in the main language it is %s", normalizedPath, diff.Path, reflect.TypeOf(diff.OldValue), reflect.TypeOf(diff.Value))) + continue + } + + if diff.Type == jsondiff.OperationAdd { + context.AddError(fmt.Sprintf("Snippet file: %s, missing key \"%s\" in this snippet file, but defined in the main language", normalizedPath, diff.Path)) + continue + } + + if diff.Type == jsondiff.OperationRemove { + context.AddError(fmt.Sprintf("Snippet file: %s, key: %s, is not defined in the main language", normalizedPath, diff.Path)) + continue + } + } + + return nil +} diff --git a/extension/snippet_validator_test.go b/extension/snippet_validator_test.go new file mode 100644 index 00000000..e73173f7 --- /dev/null +++ b/extension/snippet_validator_test.go @@ -0,0 +1,79 @@ +package extension + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSnippetValidateNoExistingFolderAdmin(t *testing.T) { + context := newValidationContext(PlatformPlugin{ + path: "test", + config: &Config{}, + }) + + validateAdministrationSnippets(context) +} + +func TestSnippetValidateNoExistingFolderStorefront(t *testing.T) { + context := newValidationContext(PlatformPlugin{ + path: "test", + config: &Config{}, + }) + + validateAdministrationSnippets(context) +} + +func TestSnippetValidateStorefrontByPathOneFileIsIgnored(t *testing.T) { + tmpDir := t.TempDir() + + context := newValidationContext(PlatformPlugin{ + path: tmpDir, + config: &Config{}, + }) + + _ = os.MkdirAll(path.Join(tmpDir, "Resources", "snippet"), os.ModePerm) + _ = os.WriteFile(path.Join(tmpDir, "Resources", "snippet", "storefront.en-GB.json"), []byte(`{}`), os.ModePerm) + + assert.NoError(t, validateStorefrontSnippetsByPath(tmpDir, context)) + assert.Len(t, context.errors, 0) + assert.Len(t, context.warnings, 0) +} + +func TestSnippetValidateStorefrontByPathSameFile(t *testing.T) { + tmpDir := t.TempDir() + + context := newValidationContext(PlatformPlugin{ + path: tmpDir, + config: &Config{}, + }) + + _ = os.MkdirAll(path.Join(tmpDir, "Resources", "snippet"), os.ModePerm) + _ = os.WriteFile(path.Join(tmpDir, "Resources", "snippet", "storefront.en-GB.json"), []byte(`{"test": "1"}`), os.ModePerm) + _ = os.WriteFile(path.Join(tmpDir, "Resources", "snippet", "storefront.de-DE.json"), []byte(`{"test": "2"}`), os.ModePerm) + + assert.NoError(t, validateStorefrontSnippetsByPath(tmpDir, context)) + assert.Len(t, context.errors, 0) + assert.Len(t, context.warnings, 0) +} + +func TestSnippetValidateStorefrontByPathTestDifferent(t *testing.T) { + tmpDir := t.TempDir() + + context := newValidationContext(PlatformPlugin{ + path: tmpDir, + config: &Config{}, + }) + + _ = os.MkdirAll(path.Join(tmpDir, "Resources", "snippet"), os.ModePerm) + _ = os.WriteFile(path.Join(tmpDir, "Resources", "snippet", "storefront.en-GB.json"), []byte(`{"a": "1"}`), os.ModePerm) + _ = os.WriteFile(path.Join(tmpDir, "Resources", "snippet", "storefront.de-DE.json"), []byte(`{"b": "2"}`), os.ModePerm) + + assert.NoError(t, validateStorefrontSnippetsByPath(tmpDir, context)) + assert.Len(t, context.errors, 2) + assert.Len(t, context.warnings, 0) + assert.Equal(t, "Snippet file: Resources/snippet/storefront.de-DE.json, key: /a, is not defined in the main language", context.errors[0]) + assert.Equal(t, "Snippet file: Resources/snippet/storefront.de-DE.json, missing key \"/b\" in this snippet file, but defined in the main language", context.errors[1]) +} diff --git a/extension/validator.go b/extension/validator.go index d998a820..949e71a6 100644 --- a/extension/validator.go +++ b/extension/validator.go @@ -48,6 +48,8 @@ func RunValidation(ctx context.Context, ext Extension) *ValidationContext { runDefaultValidate(context) ext.Validate(ctx, context) + validateAdministrationSnippets(context) + validateStorefrontSnippets(context) return context } diff --git a/go.mod b/go.mod index 19cd2e6d..14184f51 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,11 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/wI2L/jsondiff v0.5.0 // indirect golang.org/x/sync v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 5871cfb7..d3d14f6e 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,19 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/vulcand/oxy/v2 v2.0.0 h1:V+scHhd2xBjO8ojBRgxCM+OdZxRA/YTs8M70w5tdNy8= github.com/vulcand/oxy/v2 v2.0.0/go.mod h1:uIAz3sYafO7i+V3SC8oDlMn/lt1i9aWcyXuXqVswKzE= +github.com/wI2L/jsondiff v0.5.0 h1:RRMTi/mH+R2aXcPe1VYyvGINJqQfC3R+KSEakuU1Ikw= +github.com/wI2L/jsondiff v0.5.0/go.mod h1:qqG6hnK0Lsrz2BpIVCxWiK9ItsBCpIZQiv0izJjOZ9s= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=