From 524cb93ece47f6772d42e662f7189b46493eb5a2 Mon Sep 17 00:00:00 2001 From: Peter Bueschel Date: Fri, 14 Oct 2022 22:05:22 +0200 Subject: [PATCH] Add MultiImporter. Add GlobImporter. Add tests. --- example.jsonnet | 15 + glob.go | 367 ++++++++++++++++++ go.mod | 22 ++ go.sum | 56 +++ importer.go | 173 +++++++++ importer_test.go | 249 ++++++++++++ testdata/globDot/caller_dot_path.jsonnet | 10 + .../caller_dot_stem_double_star.jsonnet | 10 + .../caller_dot_stem_double_star_plus.jsonnet | 10 + testdata/globDot/host.libsonnet | 4 + testdata/globDot/subfolder/host.libsonnet | 1 + .../subfolder/subsubfolder/host.libsonnet | 1 + .../globPlus/caller_plus_double_star.jsonnet | 5 + ...caller_plus_double_star_continuous.jsonnet | 5 + .../globPlus/caller_plus_single_star.jsonnet | 5 + ...caller_plus_single_star_continuous.jsonnet | 5 + testdata/globPlus/diamondtest.jsonnet | 4 + testdata/globPlus/host.libsonnet | 4 + .../globPlus/subfolder/diamondtest.jsonnet | 4 + testdata/globPlus/subfolder/host.libsonnet | 1 + .../subsubfolder/diamondtest.jsonnet | 4 + .../subfolder/subsubfolder/host.libsonnet | 1 + testings.md | 177 +++++++++ 23 files changed, 1133 insertions(+) create mode 100644 example.jsonnet create mode 100644 glob.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 importer.go create mode 100644 importer_test.go create mode 100644 testdata/globDot/caller_dot_path.jsonnet create mode 100644 testdata/globDot/caller_dot_stem_double_star.jsonnet create mode 100644 testdata/globDot/caller_dot_stem_double_star_plus.jsonnet create mode 100644 testdata/globDot/host.libsonnet create mode 120000 testdata/globDot/subfolder/host.libsonnet create mode 120000 testdata/globDot/subfolder/subsubfolder/host.libsonnet create mode 100644 testdata/globPlus/caller_plus_double_star.jsonnet create mode 100644 testdata/globPlus/caller_plus_double_star_continuous.jsonnet create mode 100644 testdata/globPlus/caller_plus_single_star.jsonnet create mode 100644 testdata/globPlus/caller_plus_single_star_continuous.jsonnet create mode 100644 testdata/globPlus/diamondtest.jsonnet create mode 100644 testdata/globPlus/host.libsonnet create mode 100644 testdata/globPlus/subfolder/diamondtest.jsonnet create mode 120000 testdata/globPlus/subfolder/host.libsonnet create mode 100644 testdata/globPlus/subfolder/subsubfolder/diamondtest.jsonnet create mode 120000 testdata/globPlus/subfolder/subsubfolder/host.libsonnet create mode 100644 testings.md diff --git a/example.jsonnet b/example.jsonnet new file mode 100644 index 0000000..74aa498 --- /dev/null +++ b/example.jsonnet @@ -0,0 +1,15 @@ +local plus_continuous = import 'glob+!**/diamond*.jsonnet://testdata/globPlus/**/*.jsonnet'; +local plus = import 'glob+://testdata/globPlus/**/*.libsonnet'; + +local dot = import 'glob.stem+://testdata/globDot/**/*.libsonnet'; +// alias stem -> glob.stem +local dot_continuous = import 'stem://testdata/globDot/**/*.jsonnet'; + + +{ + dot: dot, + plus: plus, + + dot_continuous: dot_continuous, + plus_continuous: plus_continuous, +} diff --git a/glob.go b/glob.go new file mode 100644 index 0000000..38143c5 --- /dev/null +++ b/glob.go @@ -0,0 +1,367 @@ +package importer + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/google/go-jsonnet" + "github.com/lukasholzer/go-glob" + "go.uber.org/zap" +) + +var ( + excludeSeparator = "!" +) + +type ( + // globCacheKey is used for the globCache and helps also to identify + // "import cycles". + globCacheKey struct { + importedFrom string + importedPath string + } + + // GlobImporter can be used to allow import-paths with glob patterns inside. + // Continuous imports are also possible and allow glob pattern in resolved + // file/contents. + // Activate the glob-import via the following prefixa in front of the import + // path definition (see README file): + // - `glob.://`, where can be one of [path, file, dir, stem] + // - `glob.+://`, where can be one of [file, dir, stem] + // - `glob+://` + // + // For `glob.://` all resolved files will stored under its + // path, file(name), dir(name), stem (filename without extension). If multiple + // files would fit for the file, dirs or stem, only the last one will be used. + // Example: + // - Folders/files: + // - a.libsonnet + // - subfolder/a.libsonnet + // - Import path: + // - import 'glob.stem://**/*.libsonnet' + // - Result: + // { + // a: (import 'subfolder/a.libsonnet'); + // } + GlobImporter struct { + logger *zap.Logger + debug bool + separator string + // used in the CanHandle() and to store a possible alias. + prefixa map[string]string + aliases map[string]string + // lastFiles holds the last resolved files to enrich the import cycle + // error output and to set them as ignoreFiles in the go-glob options. + lastFiles []string + // when this cache get hit, a caller import uses the same import path + // inside the same filepath. Which means, there is an import cycle. + // A cycle ends up in a "max stack frames exceeded" and is tolerated. + // ( see also: https://github.com/google/go-jsonnet/issues/353 ) + cycleCache map[globCacheKey]struct{} + // excludePattern is used in the GlobImporter to ignore files matching + // the given pattern in '.gitIgnore' . + excludePattern string + } + + // orderedMap takes the glob.:// and glob.+:// results, + // unifies them & keeps the order. + orderedMap struct { + items map[string][]string + keys []string + } +) + +// newOrderedMap initialize a new orderedMap. +func newOrderedMap() *orderedMap { + return &orderedMap{ + items: make(map[string][]string), + keys: []string{}, + } +} + +// add is used to either really add a single value to a key or with `extend == true` +// extend the list of values with the new value for the given key. +func (o *orderedMap) add(key, value string, extend bool) { + item, exists := o.items[key] + + switch { + case !exists: + o.keys = append(o.keys, key) + o.items[key] = []string{value} + case extend: + o.items[key] = append(item, value) + case !extend: + o.items[key] = []string{value} + } +} + +// NewGlobImporter returns a GlobImporter with a no-op logger, an initialized +// cycleCache and the default prefixa. +func NewGlobImporter() *GlobImporter { + return &GlobImporter{ + separator: "://", + prefixa: map[string]string{ + "glob.path": "", + "glob.path+": "", + "glob-str.path": "", + "glob-str.path+": "", + "glob.file": "", + "glob.file+": "", + "glob-str.file": "", + "glob-str.file+": "", + "glob.dir": "", + "glob.dir+": "", + "glob-str.dir": "", + "glob-str.dir+": "", + "glob.stem": "", + "glob.stem+": "", + "glob-str.stem": "", + "glob-str.stem+": "", + "glob+": "", + "glob-str+": "", + }, + aliases: make(map[string]string), + logger: zap.New(nil), + cycleCache: make(map[globCacheKey]struct{}), + lastFiles: []string{}, + debug: false, + } +} + +func (g *GlobImporter) Exclude(pattern string) { + g.excludePattern = pattern +} + +// AddAliasPrefix binds a given alias to a given prefix. This prefix must exist +// and only one alias per prefix is possible. An alias must have the suffix +// "://". +func (g *GlobImporter) AddAliasPrefix(alias, prefix string) error { + if _, exists := g.prefixa[prefix]; !exists { + return fmt.Errorf("%w '%s'", ErrUnknownPrefix, prefix) + } + g.prefixa[prefix] = alias + g.aliases[alias] = prefix + + return nil +} + +// Logger can be used to set the zap.Logger for the GlobImporter. +func (g *GlobImporter) Logger(logger *zap.Logger) { + if logger != nil { + g.logger = logger + // used to enable also the go-glob debugging output + if ce := logger.Check(zap.DebugLevel, "debug logging enabled"); ce != nil { + g.debug = true + } + } +} + +// CanHandle implements the interface method of the Importer and returns true, +// if the path has on of the supported prefixa. Run .Prefixa() to get +// the supported prefixa. +func (g GlobImporter) CanHandle(path string) bool { + for k, v := range g.prefixa { + if strings.HasPrefix(path, k) || (strings.HasPrefix(path, v) && len(v) > 0) { + return true + } + } + + return false +} + +// Prefixa returns the list of supported prefixa for this importer. +func (g GlobImporter) Prefixa() []string { + return append(stringKeysFromMap(g.prefixa), stringValuesFromMap(g.prefixa)...) +} + +// Import implements the go-jsonnet iterface method and converts the resolved +// paths into readable paths for the original go-jsonnet FileImporter. +func (g *GlobImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error) { + logger := g.logger.Named("GlobImporter") + logger.Debug("Import()", + zap.String("importedFrom", importedFrom), + zap.String("importedPath", importedPath), + ) + + contents := jsonnet.MakeContents("") + + // Hack!!!: + // The resolved glob-imports are still found inside the same file (importedFrom) + // But the "foundAt" value is not allowed to be the same for multiple importer runs, + // causing different contents. + // Related: + // - https://github.com/google/go-jsonnet/issues/349 + // - https://github.com/google/go-jsonnet/issues/374 + // - https://github.com/google/go-jsonnet/issues/329 + // So I have to put for example a simple self-reference './' in front of the "importedFrom" path + // to fake the foundAt value. (tried multiple things, but even flushing the importerCache of + // the VM via running vm.Importer(...) again, couldn't solve this) + p := strings.Repeat("./", len(g.cycleCache)) + foundAt := p + "./" + importedFrom + cacheKey := globCacheKey{importedFrom, importedPath} + + if _, exists := g.cycleCache[cacheKey]; exists { + return contents, "", + fmt.Errorf( + "%w for import path '%s' in '%s'. Possible cycle in [%s]", + ErrImportCycle, + importedPath, importedFrom, strings.Join(g.lastFiles, " <-> "), + ) + } + // Add everything to the cache at the end + defer func() { + g.cycleCache[cacheKey] = struct{}{} + }() + prefix, pattern, err := g.parse(importedPath) + if err != nil { + return contents, foundAt, err + } + // this is the path of the import caller + cwd, _ := filepath.Split(importedFrom) + cwd = filepath.Clean(cwd) + + logger.Debug("parsed parameters from importedPath", + zap.String("prefix", prefix), + zap.String("pattern", pattern), + zap.String("cwd", cwd), + ) + + patterns := []string{pattern} + resolvedFiles, err := glob.Glob(&glob.Options{ + Patterns: patterns, + CWD: cwd, + Debug: g.debug, + IgnorePatterns: []string{g.excludePattern}, + IgnoreFiles: g.lastFiles, + AbsolutePaths: false, + }) + + if err != nil { + return contents, foundAt, fmt.Errorf("%w, used pattern: '%s'", err, pattern) + } + + logger.Debug("glob library returns", zap.Strings("files", resolvedFiles)) + + g.lastFiles = resolvedFiles + + files := allowedFiles(resolvedFiles, cwd, importedFrom) + joinedImports, err := g.handle(files, prefix) + + if err != nil { + return contents, foundAt, err + } + + contents = jsonnet.MakeContents(joinedImports) + + logger.Debug("returns", zap.String("contents", joinedImports), zap.String("foundAt", foundAt)) + + return contents, foundAt, nil +} + +func (g *GlobImporter) parse(importedPath string) (string, string, error) { + globPrefix, pattern, found := strings.Cut(importedPath, g.separator) + if !found { + return "", "", + fmt.Errorf("%w: missing separator '%s' in import path: %s", + ErrMalformedGlobPattern, g.separator, importedPath) + } + // handle excludePattern, if exists + prefix, excludePattern, _ := strings.Cut(globPrefix, excludeSeparator) + g.excludePattern = excludePattern + + return prefix, pattern, nil +} + +// allowedFiles removes ignoreFile from a given list of files and +// converts the rest via filepath.FromSlash(). +// Used to remove self reference of a file to avoid endless loops. +func allowedFiles(files []string, cwd, ignoreFile string) []string { + allowedFiles := []string{} + + for _, file := range files { + if filepath.Join(cwd, file) == ignoreFile { + continue + } + + importPath := filepath.FromSlash(file) + allowedFiles = append(allowedFiles, importPath) + } + + return allowedFiles +} + +// handle runs the logic behind the different glob prefixa and returns based on +// the prefix the import string. +func (g GlobImporter) handle(files []string, prefix string) (string, error) { + resolvedFiles := newOrderedMap() + importKind := "import" + + if strings.HasPrefix(prefix, "-str") { + prefix = strings.TrimPrefix(prefix, "-str") + importKind += "str" + } + // handle alias prefix + if p, exists := g.aliases[prefix]; exists { + prefix = p + } + + switch prefix { + case "glob+": + imports := make([]string, 0, len(files)) + + for _, f := range files { + i := fmt.Sprintf("(%s '%s')", importKind, f) + imports = append(imports, i) + } + + return strings.Join(imports, "+"), nil + case "glob.path", "glob.path+": + imports := make([]string, 0, len(files)) + + for _, f := range files { + imports = append(imports, fmt.Sprintf("'%s': (%s '%s'),", f, importKind, f)) + } + + return fmt.Sprintf("{\n%s\n}", strings.Join(imports, "\n")), nil + case "glob.stem", "glob.stem+": + for _, f := range files { + i := fmt.Sprintf("(%s '%s')", importKind, f) + _, filename := filepath.Split(f) + stem, _, _ := strings.Cut(filename, ".") + resolvedFiles.add(stem, i, strings.HasSuffix(prefix, "+")) + } + case "glob.file", "glob.file+": + for _, f := range files { + i := fmt.Sprintf("(%s '%s')", importKind, f) + _, filename := filepath.Split(f) + resolvedFiles.add(filename, i, strings.HasSuffix(prefix, "+")) + } + case "glob.dir", "glob.dir+": + for _, f := range files { + i := fmt.Sprintf("(%s '%s')", importKind, f) + dir, _ := filepath.Split(f) + resolvedFiles.add(dir, i, strings.HasSuffix(prefix, "+")) + } + default: + return "", fmt.Errorf("%w: %s", ErrUnknownPrefix, prefix) + } + + return createGlobDotImportsFrom(resolvedFiles), nil +} + +// createGlobDotImportsFrom transforms the orderedMap of resolvedFiles +// into the format `{ '': import '...' }`. +func createGlobDotImportsFrom(resolvedFiles *orderedMap) string { + var out strings.Builder + + out.WriteString("{\n") + + for _, k := range resolvedFiles.keys { + fmt.Fprintf(&out, "'%s': %s,\n", k, strings.Join(resolvedFiles.items[k], "+")) + } + + out.WriteString("\n}") + + return out.String() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..73795ae --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/peterbueschel/jsonnet-importer + +go 1.19 + +require ( + github.com/google/go-jsonnet v0.18.0 + github.com/lukasholzer/go-glob v0.0.0-20220307160753-0de2b7ff00d2 + github.com/stretchr/testify v1.8.0 + go.uber.org/zap v1.23.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e673ae7 --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= +github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lukasholzer/go-glob v0.0.0-20220307160753-0de2b7ff00d2 h1:hfdWBzGE/aa4FeVFebEsrzySfh3vm6+2rZIbil66FWI= +github.com/lukasholzer/go-glob v0.0.0-20220307160753-0de2b7ff00d2/go.mod h1:q8gWLeBs8h5yCgl+b6HXBP51/9qpDdfR+L0YqELnDsk= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/importer.go b/importer.go new file mode 100644 index 0000000..8177b82 --- /dev/null +++ b/importer.go @@ -0,0 +1,173 @@ +package importer + +import ( + "errors" + "fmt" + + "github.com/google/go-jsonnet" + "go.uber.org/zap" +) + +var ( + ErrNoImporter = errors.New("no importer") + ErrUnknownPrefix = errors.New("unknown prefix") + ErrMalformedAlias = errors.New("malformed alias") + ErrMalformedGlobPattern = errors.New("malformed glob pattern") + ErrImportCycle = errors.New("import cycle") +) + +type ( + + // Importer extends the jsonnet importer interface and adds a method to get + // the right importer for a given path. + Importer interface { + jsonnet.Importer + // CanHandle will be used to decide if an importer can handle the given + // import path. + CanHandle(path string) bool + // Logger can be used to set a zap.Logger for the importer. + // (see https://pkg.go.dev/go.uber.org/zap) + Logger(*zap.Logger) + // Prefixa returns the list of prefixa, which will trigger the specific + // importer. An empty list means no prefix used/needed. + Prefixa() []string + } + + // FallbackFileImporter is a wrapper for the original go-jsonnet FileImporter. + // The idea is to provide a chain for importers in the MultiImporter, with + // the FileImporter as fallback, if nothing else can handle the given + // import prefix (and of course also no prefix). + FallbackFileImporter struct { + *jsonnet.FileImporter + } + + // MultiImporter supports multiple importers and tries to find the right + // importer from a list of importers. + MultiImporter struct { + importers []Importer + logger *zap.Logger + } +) + +// NewFallbackFileImporter returns finally the original go-jsonnet FileImporter. +// As optional parameters extra library search paths (aka. jpath) can be provided too. +func NewFallbackFileImporter(jpaths ...string) *FallbackFileImporter { + return &FallbackFileImporter{FileImporter: &jsonnet.FileImporter{JPaths: jpaths}} +} + +// CanHandle method of the FallbackFileImporter returns always true. +func (f *FallbackFileImporter) CanHandle(_ string) bool { + return true +} + +// Logger implements the Logger interface method, but does not do anything as +// the FallbackFileImporter is just a wrapper for the go-jsonnet FileImporter. +func (f *FallbackFileImporter) Logger(_ *zap.Logger) {} + +// Prefixa for the FallbackFileImporter returns an empty list. +func (f *FallbackFileImporter) Prefixa() []string { + return []string{""} +} + +// NewMultiImporter returns an instance of a MultiImporter with default settings, +// like all custom importers + fallback importer. +func NewMultiImporter(importers ...Importer) *MultiImporter { + multiImporter := &MultiImporter{ + importers: importers, + logger: zap.New(nil), + } + if len(multiImporter.importers) == 0 { + multiImporter.importers = []Importer{ + NewGlobImporter(), + NewFallbackFileImporter(), + } + } + + return multiImporter +} + +// Logger method can be used to set a zap.Logger for all importers at once. +// (see https://pkg.go.dev/go.uber.org/zap) +func (m *MultiImporter) Logger(logger *zap.Logger) { + if logger != nil { + m.logger = logger + for _, i := range m.importers { + i.Logger(logger) + } + } +} + +// Import is used by go-jsonnet to run this importer. It implements the go-jsonnet +// Importer interface method. +func (m *MultiImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error) { + logger := m.logger.Named("MultiImporter") + logger.Debug( + "Import()", + zap.String("importedFrom", importedFrom), + zap.String("importedPath", importedPath), + ) + + importerTypes := make([]string, 0, len(m.importers)) + prefixa := make(map[string][]string, len(m.importers)) + + for _, importer := range m.importers { + t := fmt.Sprintf("%T", importer) + importerTypes = append(importerTypes, t) + prefixa[t] = importer.Prefixa() + } + + logger.Debug( + "trying to find the right importer for importedPath", + zap.Strings("importers", importerTypes), + zap.String("importedPath", importedPath), + ) + + for _, importer := range m.importers { + if importer.CanHandle(importedPath) { + logger.Info( + "found importer for importedPath", + zap.String("importer", fmt.Sprintf("%T", importer)), + zap.String("importedPath", importedPath), + ) + + contents, foundAt, err := importer.Import(importedFrom, importedPath) + if err != nil { + return jsonnet.MakeContents(""), + "", + fmt.Errorf("%w for importer: %T", err, importer) + } + + return contents, foundAt, nil + } + } + + logger.Error( + "found no importer for importedPath", + zap.String("importedPath", importedPath), + zap.Any("supported prefixa per loaded importer", prefixa), + ) + + return jsonnet.MakeContents(""), + "", + fmt.Errorf("%w can handle given path: '%s'", ErrNoImporter, importedPath) +} + +// stringKeysFromMap returns the keys from a map as slice +func stringKeysFromMap(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + return keys +} + +// stringValuesFromMap returns the values from a map as slice +func stringValuesFromMap(m map[string]string) []string { + values := make([]string, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + + return values +} diff --git a/importer_test.go b/importer_test.go new file mode 100644 index 0000000..41aff99 --- /dev/null +++ b/importer_test.go @@ -0,0 +1,249 @@ +package importer + +import ( + "os" + "testing" + + "github.com/google/go-jsonnet" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func TestMultiImporter_Behavior(t *testing.T) { + lvl := zap.NewAtomicLevel() + cfg := zap.NewDevelopmentEncoderConfig() + cfg.TimeKey = "" + + lvl.SetLevel(zap.DebugLevel) + + logger := zap.New(zapcore.NewCore( + zapcore.NewJSONEncoder(cfg), + zapcore.Lock(os.Stdout), + lvl, + )) + + if testing.Short() { + // disable logging output in tests + logger = zap.New(nil) + } + + tests := []struct { + name string + callerFile string + want string + wantErr bool + }{ + // -------------------------------------------------------- glob.:// + { + name: "glob_dot_path", + callerFile: "testdata/globDot/caller_dot_path.jsonnet", + want: `{ + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_path.jsonnet", + "testdata/globDot/host.libsonnet" + ], + "names": [ + "host.libsonnet" + ] +} +`, + }, + { + name: "glob_dot_stem_double_star", + callerFile: "testdata/globDot/caller_dot_stem_double_star.jsonnet", + want: `{ + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star.jsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] +} +`, + }, + { + name: "glob_dot_stem_double_star_plus", + callerFile: "testdata/globDot/caller_dot_stem_double_star_plus.jsonnet", + want: `{ + "checksum": 3, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star_plus.jsonnet", + "testdata/globDot/host.libsonnet", + "testdata/globDot/subfolder/host.libsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] +} +`, + }, + // ----------------------------------------------------------- glob+:// + { + name: "glob_plus_single_star", + callerFile: "testdata/globPlus/caller_plus_single_star.jsonnet", + want: `{ + "checksum": 1, + "imports": [ + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/host.libsonnet" + ] +} +`, + }, + { + name: "glob_plus_single_star_continuous", + callerFile: "testdata/globPlus/caller_plus_single_star_continuous.jsonnet", + want: `{ + "checksum": 1, + "imports": [ + "testdata/globPlus/caller_plus_single_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/host.libsonnet" + ] +} +`, + }, + { + name: "glob_plus_double_star", + callerFile: "testdata/globPlus/caller_plus_double_star.jsonnet", + want: `{ + "checksum": 3, + "imports": [ + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet" + ] +} +`, + }, + { + name: "glob_plus_double_star_continuous", + callerFile: "testdata/globPlus/caller_plus_double_star_continuous.jsonnet", + want: `{ + "checksum": 3, + "imports": [ + "testdata/globPlus/caller_plus_double_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet" + ] +} +`, + }, + { + name: "glob_plus_diamondtest", + callerFile: "testdata/globPlus/diamondtest.jsonnet", + wantErr: true, + want: ``, + }, + // ------------------------------------------------------------ complex + { + name: "complex test with multiple prefixa", + callerFile: "example.jsonnet", + want: excpectedComplexOutput, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewGlobImporter() + err := g.AddAliasPrefix("stem", "glob.stem") + if err != nil { + t.Errorf("AddAliasPrefix() failed: %v", err) + return + } + m := NewMultiImporter(g, NewFallbackFileImporter()) + g.Logger(logger) + + vm := jsonnet.MakeVM() + vm.Importer(m) + got, err := vm.EvaluateFile(tt.callerFile) + if (err != nil) != tt.wantErr { + t.Errorf("vm.EvaluateFile(%s) %v", tt.callerFile, err) + return + } + assert.Equal(t, tt.want, got) + }) + } +} + +var excpectedComplexOutput = `{ + "dot": { + "host": { + "checksum": 3, + "imports": [ + "testdata/globDot/host.libsonnet", + "testdata/globDot/subfolder/host.libsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ] + } + }, + "dot_continuous": { + "caller_dot_path": { + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_path.jsonnet", + "testdata/globDot/host.libsonnet" + ], + "names": [ + "host.libsonnet" + ] + }, + "caller_dot_stem_double_star": { + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star.jsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] + }, + "caller_dot_stem_double_star_plus": { + "checksum": 3, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star_plus.jsonnet", + "testdata/globDot/host.libsonnet", + "testdata/globDot/subfolder/host.libsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] + } + }, + "plus": { + "checksum": 3, + "imports": [ + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet" + ] + }, + "plus_continuous": { + "checksum": 8, + "imports": [ + "testdata/globPlus/caller_plus_single_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/caller_plus_double_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/host.libsonnet" + ] + } +} +` diff --git a/testdata/globDot/caller_dot_path.jsonnet b/testdata/globDot/caller_dot_path.jsonnet new file mode 100644 index 0000000..33323fc --- /dev/null +++ b/testdata/globDot/caller_dot_path.jsonnet @@ -0,0 +1,10 @@ +local files = import 'glob.path://*.libsonnet'; +{ + names: std.objectFields(files), + checksum+: std.foldl( + function(sum, f) sum + files[f].checksum, std.objectFields(files), 0 + ), + imports+: std.foldl( + function(list, f) list + files[f].imports, std.objectFields(files), [std.thisFile] + ), +} diff --git a/testdata/globDot/caller_dot_stem_double_star.jsonnet b/testdata/globDot/caller_dot_stem_double_star.jsonnet new file mode 100644 index 0000000..b7290d4 --- /dev/null +++ b/testdata/globDot/caller_dot_stem_double_star.jsonnet @@ -0,0 +1,10 @@ +local files = import 'glob.stem://**/*.libsonnet'; +{ + names: std.objectFields(files), + checksum+: std.foldl( + function(sum, f) sum + files[f].checksum, std.objectFields(files), 0 + ), + imports+: std.foldl( + function(list, f) list + files[f].imports, std.objectFields(files), [std.thisFile] + ), +} diff --git a/testdata/globDot/caller_dot_stem_double_star_plus.jsonnet b/testdata/globDot/caller_dot_stem_double_star_plus.jsonnet new file mode 100644 index 0000000..5276e68 --- /dev/null +++ b/testdata/globDot/caller_dot_stem_double_star_plus.jsonnet @@ -0,0 +1,10 @@ +local files = import 'glob.stem+://**/*.libsonnet'; +{ + names: std.objectFields(files), + checksum+: std.foldl( + function(sum, f) sum + files[f].checksum, std.objectFields(files), 0 + ), + imports+: std.foldl( + function(list, f) list + files[f].imports, std.objectFields(files), [std.thisFile] + ), +} diff --git a/testdata/globDot/host.libsonnet b/testdata/globDot/host.libsonnet new file mode 100644 index 0000000..e6a0fd7 --- /dev/null +++ b/testdata/globDot/host.libsonnet @@ -0,0 +1,4 @@ +{ + checksum+: 1, + imports+: [std.thisFile], +} diff --git a/testdata/globDot/subfolder/host.libsonnet b/testdata/globDot/subfolder/host.libsonnet new file mode 120000 index 0000000..204bfbd --- /dev/null +++ b/testdata/globDot/subfolder/host.libsonnet @@ -0,0 +1 @@ +../host.libsonnet \ No newline at end of file diff --git a/testdata/globDot/subfolder/subsubfolder/host.libsonnet b/testdata/globDot/subfolder/subsubfolder/host.libsonnet new file mode 120000 index 0000000..d1c030d --- /dev/null +++ b/testdata/globDot/subfolder/subsubfolder/host.libsonnet @@ -0,0 +1 @@ +../../host.libsonnet \ No newline at end of file diff --git a/testdata/globPlus/caller_plus_double_star.jsonnet b/testdata/globPlus/caller_plus_double_star.jsonnet new file mode 100644 index 0000000..59089e3 --- /dev/null +++ b/testdata/globPlus/caller_plus_double_star.jsonnet @@ -0,0 +1,5 @@ +(import 'glob+://**/*.libsonnet') + +{ + checksum: super.checksum, + imports: [std.thisFile] + super.imports, +} diff --git a/testdata/globPlus/caller_plus_double_star_continuous.jsonnet b/testdata/globPlus/caller_plus_double_star_continuous.jsonnet new file mode 100644 index 0000000..4da29c9 --- /dev/null +++ b/testdata/globPlus/caller_plus_double_star_continuous.jsonnet @@ -0,0 +1,5 @@ +(import 'glob+://**/*double*.jsonnet') + +{ + checksum: super.checksum, + imports: [std.thisFile] + super.imports, +} diff --git a/testdata/globPlus/caller_plus_single_star.jsonnet b/testdata/globPlus/caller_plus_single_star.jsonnet new file mode 100644 index 0000000..cd8fea4 --- /dev/null +++ b/testdata/globPlus/caller_plus_single_star.jsonnet @@ -0,0 +1,5 @@ +(import 'glob+://*.libsonnet') + +{ + checksum: super.checksum, + imports: [std.thisFile] + super.imports, +} diff --git a/testdata/globPlus/caller_plus_single_star_continuous.jsonnet b/testdata/globPlus/caller_plus_single_star_continuous.jsonnet new file mode 100644 index 0000000..476a1b8 --- /dev/null +++ b/testdata/globPlus/caller_plus_single_star_continuous.jsonnet @@ -0,0 +1,5 @@ +(import 'glob+://*single*.jsonnet') + +{ + checksum: super.checksum, + imports: [std.thisFile] + super.imports, +} diff --git a/testdata/globPlus/diamondtest.jsonnet b/testdata/globPlus/diamondtest.jsonnet new file mode 100644 index 0000000..72f8785 --- /dev/null +++ b/testdata/globPlus/diamondtest.jsonnet @@ -0,0 +1,4 @@ +local d = import 'glob+://**/diamondtest.jsonnet'; +{ + start: d, +} diff --git a/testdata/globPlus/host.libsonnet b/testdata/globPlus/host.libsonnet new file mode 100644 index 0000000..e6a0fd7 --- /dev/null +++ b/testdata/globPlus/host.libsonnet @@ -0,0 +1,4 @@ +{ + checksum+: 1, + imports+: [std.thisFile], +} diff --git a/testdata/globPlus/subfolder/diamondtest.jsonnet b/testdata/globPlus/subfolder/diamondtest.jsonnet new file mode 100644 index 0000000..d4b51b1 --- /dev/null +++ b/testdata/globPlus/subfolder/diamondtest.jsonnet @@ -0,0 +1,4 @@ +local d = import 'glob+://**/diamondtest.jsonnet'; +{ + sub: d, +} diff --git a/testdata/globPlus/subfolder/host.libsonnet b/testdata/globPlus/subfolder/host.libsonnet new file mode 120000 index 0000000..204bfbd --- /dev/null +++ b/testdata/globPlus/subfolder/host.libsonnet @@ -0,0 +1 @@ +../host.libsonnet \ No newline at end of file diff --git a/testdata/globPlus/subfolder/subsubfolder/diamondtest.jsonnet b/testdata/globPlus/subfolder/subsubfolder/diamondtest.jsonnet new file mode 100644 index 0000000..1c82d81 --- /dev/null +++ b/testdata/globPlus/subfolder/subsubfolder/diamondtest.jsonnet @@ -0,0 +1,4 @@ +local d = import '../../diamondtest.jsonnet'; +{ + subsub: d, +} diff --git a/testdata/globPlus/subfolder/subsubfolder/host.libsonnet b/testdata/globPlus/subfolder/subsubfolder/host.libsonnet new file mode 120000 index 0000000..d1c030d --- /dev/null +++ b/testdata/globPlus/subfolder/subsubfolder/host.libsonnet @@ -0,0 +1 @@ +../../host.libsonnet \ No newline at end of file diff --git a/testings.md b/testings.md new file mode 100644 index 0000000..a744e38 --- /dev/null +++ b/testings.md @@ -0,0 +1,177 @@ +# Behavior Tests + +Tests are also documentation, therefore the following sections provide an overview of the behavior tests per prefix. The files can be found under the `testdata` folder and the code to run the tests is inside the `importer_test.go` inside the `TestMultiImporter_Behavior()` function. + +## Prefix `glob.` And `glob.+` + +### Test Folder Structure + +```console + globDot + ├── caller_dot_path.jsonnet + ├── caller_dot_stem_double_star.jsonnet + ├── caller_dot_stem_double_star_plus.jsonnet + ├── host.libsonnet + └── subfolder + ├── host.libsonnet + └── subsubfolder + └── host.libsonnet +``` + +### Test File(s) Content + +- `` used in each `caller_dot_*.jsonnet` files: + ```jsonnet + local files = import ...; + { + checksum+: std.foldl(function(sum, f) sum + files[f].checksum, std.objectFields(files), 0), + imports+: std.foldl(function(list, f) list + files[f].imports, std.objectFields(files), [std.thisFile]), + names: std.objectFields(files), + } + ``` +- `caller_dot_path.jsonnet` content: `local files = import 'glob.path://*.libsonnet'; ` +- `caller_dot_stem.jsonnet` content: `local files = import 'glob.stem://**/*.libsonnet'; ` +- `caller_dot_stem_plus.jsonnet` content: `local files = import 'glob.stem+://**/*.jsonnet'; ` +- `caller_dot_dir_parent_plus.jsonnet` content: `local files = import 'glob.parent+://**/*.jsonnet'; ` +- `host.libsonnet` & `subfolder/subhost.libsonnet` & `subfolder/subsubfolder/subsubhost.libsonnet` content: `{ checksum+: 1, imports+: [std.thisFile] }` + +### Test Cases + +- `glob_dot_path`: + - `caller_dot_path.jsonnet` imports `host.libsonnet` under `'host.libsonnet': import 'host.libsonnet'` via `glob.path://*.libsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`; `"names"` are the variable names): + + ``` json + { + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_path.jsonnet", + "testdata/globDot/host.libsonnet" + ], + "names": [ + "host.libsonnet" + ] + } + + ``` +- `glob_dot_stem_double_star`: + - `caller_dot_stem.jsonnet` imports only the last resolved file `testdata/globDot/subfolder/subsubfolder/host.libsonnet` under + `host: import 'testdata/globDot/subfolder/subsubfolder/host.libsonnet'`via `glob.stem://**/*.libsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`; `"names"` are the variable names): + ```json + { + "checksum": 1, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star.jsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] + } + ``` +- `glob_dot_stem_double_star_plus`: + - `caller_dot_stem_plus.jsonnet` imports all resolved files and merges them under a single variable + `host: (import 'testdata/globDot/host.libsonnet') + (import 'testdata/globDot/subfolder/host.libsonnet') + (import 'testdata/globDot/subfolder/subsubfolder/host.libsonnet')` + via `glob.stem+://**/*.libsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`; `"names"` are the variable names): + + ``` json + { + "checksum": 3, + "imports": [ + "testdata/globDot/caller_dot_stem_double_star_plus.jsonnet", + "testdata/globDot/host.libsonnet", + "testdata/globDot/subfolder/host.libsonnet", + "testdata/globDot/subfolder/subsubfolder/host.libsonnet" + ], + "names": [ + "host" + ] + } + ``` + +## Prefix **`glob+`** + +### Test Folder Structure + +- ```console + globPlus + ├── caller_plus_double_star.jsonnet + ├── caller_plus_double_star_continuous.jsonnet + ├── caller_plus_single_star.jsonnet + ├── caller_plus_single_star_continuous.jsonnet + ├── host.libsonnet + └── subfolder + ├── host.libsonnet + └── subsubfolder + └── host.libsonnet + ``` + +### Test File(s) Content + +- `` used in each `caller_plus_*.jsonnet` files: `(import ...) + { checksum: super.checksum, imports: [std.thisFile] + super.imports }` +- `caller_plus_double_star.jsonnet` content: `(import 'glob+://**/*.libsonnet') + ` +- `caller_plus_double_star_continuous.jsonnet` content: `(import 'glob+://**/*double*.jsonnet') + ` +- `caller_plus_single_star.jsonnet` content: `(import 'glob+://*.libsonnet') + ` +- `caller_plus_single_star_continuous.jsonnet` content: `(import 'glob+://*single*.jsonnet') + ` +- TODO `caller_plus_single_star_relative.jsonnet` content: `(import 'glob+://../*.libsonnet') + ` +- `host.libsonnet` & `subfolder/subhost.libsonnet` & `subfolder/subsubfolder/subsubhost.libsonnet` content: `{ checksum+: 1, imports+: [std.thisFile] }` + +### Test Cases: + +- `glob_plus_single_star`: + - `caller_plus_single_star.jsonnet` imports `host.libsonnet` via `glob+://*.libsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`): + ```json + { + "checksum": 1, + "imports": [ + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/host.libsonnet" + ] + } + ``` +- `glob_plus_single_star_continuous`: + - `caller_plus_single_star_continuous.jsonnet` imports `caller_plus_single_star.jsonnet`, which in turn imports `host.libsonnet` via `glob+://*single*.libsonnet` and **not again** `caller_plus_single_star_continuous.jsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`): + ```json + { + "checksum": 1, + "imports": [ + "testdata/globPlus/caller_plus_single_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_single_star.jsonnet", + "testdata/globPlus/host.libsonnet" + ] + } + ``` +- `glob_plus_double_star`: + - `caller_plus_double_star.jsonnet` imports `host.libsonnet` & `subfolder/subhost.libsonnet` & `subfolder/subsubfolder/subsubhost.libsonnet` via `glob+://**/*.libsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`): + ```json + { + "checksum": 3, + "imports": [ + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet" + ] + } + ``` +- `glob_plus_double_star_continuous`: + - `caller_plus_double_star_continuous.jsonnet` imports `caller_plus_double_star.jsonnet`, which in turn imports `host.libsonnet` & `subfolder/subhost.libsonnet` & `subfolder/subsubfolder/subsubhost.libsonnet` via `glob+://**/*double*.libsonnet` and **not again** `caller_plus_double_star_continuous.jsonnet` + - expected json output (order in the `"imports"` field reflects the import chain; `"checksum"` comes from the `**/host.libsonnet`): + + ``` json + { + "checksum": 3, + "imports": [ + "testdata/globPlus/caller_plus_double_star_continuous.jsonnet", + "testdata/globPlus/caller_plus_double_star.jsonnet", + "testdata/globPlus/host.libsonnet", + "testdata/globPlus/subfolder/host.libsonnet", + "testdata/globPlus/subfolder/subsubfolder/host.libsonnet" + ] + } + ```