Skip to content

Commit f4428e6

Browse files
sbarzowskisparkprime
authored andcommitted
Better Importer interface
As discussed in #190
1 parent fde815f commit f4428e6

File tree

6 files changed

+144
-77
lines changed

6 files changed

+144
-77
lines changed

imports.go

Lines changed: 119 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -23,67 +23,87 @@ import (
2323
"path"
2424
)
2525

26-
// ImportedData represents imported data and where it came from.
27-
type ImportedData struct {
28-
FoundHere string
29-
Content string
30-
}
31-
3226
// An Importer imports data from a path.
27+
// TODO(sbarzowski) caching of errors (may require breaking changes)
3328
type Importer interface {
34-
Import(codeDir string, importedPath string) (*ImportedData, error)
29+
// Import fetches data from a given path. It may be relative
30+
// to the file where we do the import. What "relative path"
31+
// means depends on the importer.
32+
//
33+
// It is required that:
34+
// a) for given (importedFrom, importedPath) the same
35+
// (contents, foundAt) are returned on subsequent calls.
36+
// b) for given foundAt, the contents are always the same
37+
//
38+
// It is recommended that if there are multiple locations that
39+
// need to be probed (e.g. relative + multiple library paths)
40+
// then all results of all attempts will be cached separately,
41+
// both nonexistence and contents of existing ones.
42+
// FileImporter may serve as an example.
43+
Import(importedFrom, importedPath string) (contents Contents, foundAt string, err error)
3544
}
3645

37-
// ImportCacheValue represents a value in an imported-data cache.
38-
type ImportCacheValue struct {
39-
// nil if we got an error
40-
data *ImportedData
41-
42-
// nil if we got an error or have only imported it via importstr
43-
asCode potentialValue
44-
45-
// Errors can occur during import, we have to cache these too.
46-
err error
46+
// Contents is a representation of imported data. It is a simple
47+
// string wrapper, which makes it easier to enforce the caching policy.
48+
type Contents struct {
49+
data *string
4750
}
4851

49-
type importCacheKey struct {
50-
dir string
51-
importedPath string
52+
func (c Contents) String() string {
53+
return *c.data
5254
}
5355

54-
type importCacheMap map[importCacheKey]*ImportCacheValue
56+
// MakeContents creates Contents from a string.
57+
func MakeContents(s string) Contents {
58+
return Contents{
59+
data: &s,
60+
}
61+
}
5562

5663
// ImportCache represents a cache of imported data.
64+
//
65+
// While the user-defined Importer implementations
66+
// are required to cache file content, this cache
67+
// is an additional layer of optimization that caches values
68+
// (i.e. the result of executing the file content).
69+
// It also verifies that the content pointer is the same for two foundAt values.
5770
type ImportCache struct {
58-
cache importCacheMap
59-
importer Importer
71+
foundAtVerification map[string]Contents
72+
codeCache map[string]potentialValue
73+
importer Importer
6074
}
6175

62-
// MakeImportCache creates and ImportCache using an importer.
76+
// MakeImportCache creates an ImportCache using an Importer.
6377
func MakeImportCache(importer Importer) *ImportCache {
64-
return &ImportCache{importer: importer, cache: make(importCacheMap)}
78+
return &ImportCache{
79+
importer: importer,
80+
foundAtVerification: make(map[string]Contents),
81+
codeCache: make(map[string]potentialValue),
82+
}
6583
}
6684

67-
func (cache *ImportCache) importData(key importCacheKey) *ImportCacheValue {
68-
if cached, ok := cache.cache[key]; ok {
69-
return cached
85+
func (cache *ImportCache) importData(importedFrom, importedPath string) (contents Contents, foundAt string, err error) {
86+
contents, foundAt, err = cache.importer.Import(importedFrom, importedPath)
87+
if err != nil {
88+
return Contents{}, "", err
7089
}
71-
data, err := cache.importer.Import(key.dir, key.importedPath)
72-
cached := &ImportCacheValue{
73-
data: data,
74-
err: err,
90+
if cached, importedBefore := cache.foundAtVerification[foundAt]; importedBefore {
91+
if cached != contents {
92+
panic(fmt.Sprintf("importer problem: a different instance of Contents returned when importing %#v again", foundAt))
93+
}
94+
} else {
95+
cache.foundAtVerification[foundAt] = contents
7596
}
76-
cache.cache[key] = cached
77-
return cached
97+
return
7898
}
7999

80100
// ImportString imports a string, caches it and then returns it.
81-
func (cache *ImportCache) ImportString(codeDir, importedPath string, e *evaluator) (*valueString, error) {
82-
cached := cache.importData(importCacheKey{codeDir, importedPath})
83-
if cached.err != nil {
84-
return nil, e.Error(cached.err.Error())
101+
func (cache *ImportCache) ImportString(importedFrom, importedPath string, e *evaluator) (*valueString, error) {
102+
data, _, err := cache.importData(importedFrom, importedPath)
103+
if err != nil {
104+
return nil, e.Error(err.Error())
85105
}
86-
return makeValueString(cached.data.Content), nil
106+
return makeValueString(data.String()), nil
87107
}
88108

89109
func codeToPV(e *evaluator, filename string, code string) potentialValue {
@@ -100,69 +120,101 @@ func codeToPV(e *evaluator, filename string, code string) potentialValue {
100120
}
101121

102122
// ImportCode imports code from a path.
103-
func (cache *ImportCache) ImportCode(codeDir, importedPath string, e *evaluator) (value, error) {
104-
cached := cache.importData(importCacheKey{codeDir, importedPath})
105-
if cached.err != nil {
106-
return nil, e.Error(cached.err.Error())
123+
func (cache *ImportCache) ImportCode(importedFrom, importedPath string, e *evaluator) (value, error) {
124+
contents, foundAt, err := cache.importData(importedFrom, importedPath)
125+
if err != nil {
126+
return nil, e.Error(err.Error())
107127
}
108-
if cached.asCode == nil {
109-
// File hasn't been parsed before, update the cache record.
110-
cached.asCode = codeToPV(e, cached.data.FoundHere, cached.data.Content)
128+
var pv potentialValue
129+
if cachedPV, isCached := cache.codeCache[foundAt]; !isCached {
130+
// File hasn't been parsed and analyzed before, update the cache record.
131+
pv = codeToPV(e, foundAt, contents.String())
132+
cache.codeCache[foundAt] = pv
133+
} else {
134+
pv = cachedPV
111135
}
112-
return e.evaluate(cached.asCode)
136+
return e.evaluate(pv)
113137
}
114138

115139
// Concrete importers
116140
// -------------------------------------
117141

118-
// FileImporter imports data from files.
142+
// FileImporter imports data from the filesystem.
119143
type FileImporter struct {
120-
JPaths []string
144+
JPaths []string
145+
fsCache map[string]*fsCacheEntry
146+
}
147+
148+
type fsCacheEntry struct {
149+
exists bool
150+
contents Contents
121151
}
122152

123-
func tryPath(dir, importedPath string) (found bool, content []byte, foundHere string, err error) {
153+
func (importer *FileImporter) tryPath(dir, importedPath string) (found bool, contents Contents, foundHere string, err error) {
154+
if importer.fsCache == nil {
155+
importer.fsCache = make(map[string]*fsCacheEntry)
156+
}
124157
var absPath string
125158
if path.IsAbs(importedPath) {
126159
absPath = importedPath
127160
} else {
128161
absPath = path.Join(dir, importedPath)
129162
}
130-
content, err = ioutil.ReadFile(absPath)
131-
if os.IsNotExist(err) {
132-
return false, nil, "", nil
163+
var entry *fsCacheEntry
164+
if cacheEntry, isCached := importer.fsCache[absPath]; isCached {
165+
entry = cacheEntry
166+
} else {
167+
contentBytes, err := ioutil.ReadFile(absPath)
168+
if err != nil {
169+
if os.IsNotExist(err) {
170+
entry = &fsCacheEntry{
171+
exists: false,
172+
}
173+
} else {
174+
return false, Contents{}, "", err
175+
}
176+
} else {
177+
entry = &fsCacheEntry{
178+
exists: true,
179+
contents: MakeContents(string(contentBytes)),
180+
}
181+
}
182+
importer.fsCache[absPath] = entry
133183
}
134-
return true, content, absPath, err
184+
return entry.exists, entry.contents, absPath, nil
135185
}
136186

137-
// Import imports a file.
138-
func (importer *FileImporter) Import(dir, importedPath string) (*ImportedData, error) {
139-
found, content, foundHere, err := tryPath(dir, importedPath)
187+
// Import imports file from the filesystem.
188+
func (importer *FileImporter) Import(importedFrom, importedPath string) (contents Contents, foundAt string, err error) {
189+
dir, _ := path.Split(importedFrom)
190+
found, content, foundHere, err := importer.tryPath(dir, importedPath)
140191
if err != nil {
141-
return nil, err
192+
return Contents{}, "", err
142193
}
143194

144195
for i := len(importer.JPaths) - 1; !found && i >= 0; i-- {
145-
found, content, foundHere, err = tryPath(importer.JPaths[i], importedPath)
196+
found, content, foundHere, err = importer.tryPath(importer.JPaths[i], importedPath)
146197
if err != nil {
147-
return nil, err
198+
return Contents{}, "", err
148199
}
149200
}
150201

151202
if !found {
152-
return nil, fmt.Errorf("couldn't open import %#v: no match locally or in the Jsonnet library paths", importedPath)
203+
return Contents{}, "", fmt.Errorf("couldn't open import %#v: no match locally or in the Jsonnet library paths", importedPath)
153204
}
154-
return &ImportedData{Content: string(content), FoundHere: foundHere}, nil
205+
return content, foundHere, nil
155206
}
156207

157208
// MemoryImporter "imports" data from an in-memory map.
158209
type MemoryImporter struct {
159-
Data map[string]string
210+
Data map[string]Contents
160211
}
161212

162-
// Import imports a map entry.
163-
func (importer *MemoryImporter) Import(dir, importedPath string) (*ImportedData, error) {
213+
// Import fetches data from a map entry.
214+
// All paths are treated as absolute keys.
215+
func (importer *MemoryImporter) Import(importedFrom, importedPath string) (contents Contents, foundAt string, err error) {
164216
if content, ok := importer.Data[importedPath]; ok {
165-
return &ImportedData{Content: content, FoundHere: importedPath}, nil
217+
return content, importedPath, nil
166218
}
167-
return nil, fmt.Errorf("import not available %v", importedPath)
219+
return Contents{}, "", fmt.Errorf("import not available %v", importedPath)
168220
}

interpreter.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"bytes"
2121
"fmt"
2222
"math"
23-
"path"
2423
"reflect"
2524
"sort"
2625

@@ -412,12 +411,12 @@ func (i *interpreter) evaluate(a ast.Node, tc tailCallStatus) (value, error) {
412411
return nil, e.Error(fmt.Sprintf("Value non indexable: %v", reflect.TypeOf(targetValue)))
413412

414413
case *ast.Import:
415-
codeDir, _ := path.Split(node.Loc().FileName)
416-
return i.importCache.ImportCode(codeDir, node.File.Value, e)
414+
codePath := node.Loc().FileName
415+
return i.importCache.ImportCode(codePath, node.File.Value, e)
417416

418417
case *ast.ImportStr:
419-
codeDir, _ := path.Split(node.Loc().FileName)
420-
return i.importCache.ImportString(codeDir, node.File.Value, e)
418+
codePath := node.Loc().FileName
419+
return i.importCache.ImportString(codePath, node.File.Value, e)
421420

422421
case *ast.LiteralBoolean:
423422
return makeValueBoolean(node.Value), nil

jsonnet_test.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ func removeExcessiveWhitespace(s string) string {
100100
func TestCustomImporter(t *testing.T) {
101101
vm := MakeVM()
102102
vm.Importer(&MemoryImporter{
103-
map[string]string{
104-
"a.jsonnet": "2 + 2",
105-
"b.jsonnet": "3 + 3",
103+
map[string]Contents{
104+
"a.jsonnet": MakeContents("2 + 2"),
105+
"b.jsonnet": MakeContents("3 + 3"),
106106
},
107107
})
108108
input := `[import "a.jsonnet", importstr "b.jsonnet"]`
@@ -116,3 +116,17 @@ func TestCustomImporter(t *testing.T) {
116116
t.Errorf("Expected %q, but got %q", expected, actual)
117117
}
118118
}
119+
120+
func TestContents(t *testing.T) {
121+
a := "aaa"
122+
c1 := MakeContents(a)
123+
a = "bbb"
124+
if c1.String() != "aaa" {
125+
t.Errorf("Contents should be immutable")
126+
}
127+
c2 := MakeContents(a)
128+
c3 := MakeContents(a)
129+
if c2 == c3 {
130+
t.Errorf("Contents should distinguish between different instances even if they have the same data inside")
131+
}
132+
}

parser/parser_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ limitations under the License.
1616
package parser
1717

1818
import (
19-
"fmt"
2019
"testing"
2120
)
2221

@@ -126,7 +125,6 @@ var tests = []string{
126125
func TestParser(t *testing.T) {
127126
for _, s := range tests {
128127
t.Run(s, func(t *testing.T) {
129-
fmt.Println(s)
130128
tokens, err := Lex("test", s)
131129
if err != nil {
132130
t.Errorf("Unexpected lex error\n input: %v\n error: %v", s, err)

testdata/import_twice.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"import \"true.jsonnet\"\nimport \"true.jsonnet\"\n"

testdata/import_twice.jsonnet

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
local x = importstr "import.jsonnet";
2+
local y = importstr "import.jsonnet";
3+
x + y

0 commit comments

Comments
 (0)