From 39fe254f0aea0373cb811f751a7efc84b3ea44b4 Mon Sep 17 00:00:00 2001 From: Brandon Bennett Date: Mon, 25 Nov 2024 11:19:47 -0700 Subject: [PATCH] add context support for Loader --- compiler.go | 32 ++++++++++++++++++------- draft_test.go | 5 ++-- example_http_test.go | 15 ++++++++---- invalid_schemas_test.go | 7 +++--- loader.go | 52 ++++++++++++++++++++++++++++++++++------- objcompiler.go | 25 ++++++++++---------- roots.go | 37 +++++++++++++++-------------- suite_test.go | 7 +++--- 8 files changed, 120 insertions(+), 60 deletions(-) diff --git a/compiler.go b/compiler.go index 4da7361..633e68d 100644 --- a/compiler.go +++ b/compiler.go @@ -1,6 +1,7 @@ package jsonschema import ( + "context" "fmt" "regexp" "slices" @@ -134,7 +135,13 @@ func (c *Compiler) AddResource(url string, doc any) error { // UseLoader overrides the default [URLLoader] used // to load schema resources. +// +// Deprecated: use URLLoaderContext func (c *Compiler) UseLoader(loader URLLoader) { + c.roots.loader.loader = contextAdapterLoader{loader} +} + +func (c *Compiler) UseLoaderContext(loader URLLoaderContext) { c.roots.loader.loader = loader } @@ -175,26 +182,33 @@ func (c *Compiler) MustCompile(loc string) *Schema { } // Compile compiles json-schema at given loc. +// +// Deprecated: use [CompileContext] func (c *Compiler) Compile(loc string) (*Schema, error) { + return c.CompileContext(context.Background(), loc) +} + +// CompileContext compiles json-schema at given loc. +func (c *Compiler) CompileContext(ctx context.Context, loc string) (*Schema, error) { uf, err := absolute(loc) if err != nil { return nil, err } - up, err := c.roots.resolveFragment(*uf) + up, err := c.roots.resolveFragment(ctx, *uf) if err != nil { return nil, err } - return c.doCompile(up) + return c.doCompile(ctx, up) } -func (c *Compiler) doCompile(up urlPtr) (*Schema, error) { +func (c *Compiler) doCompile(ctx context.Context, up urlPtr) (*Schema, error) { q := &queue{} compiled := 0 c.enqueue(q, up) for q.len() > compiled { sch := q.at(compiled) - if err := c.roots.ensureSubschema(sch.up); err != nil { + if err := c.roots.ensureSubschema(ctx, sch.up); err != nil { return nil, err } r := c.roots.roots[sch.up.url] @@ -202,7 +216,7 @@ func (c *Compiler) doCompile(up urlPtr) (*Schema, error) { if err != nil { return nil, err } - if err := c.compileValue(v, sch, r, q); err != nil { + if err := c.compileValue(ctx, v, sch, r, q); err != nil { return nil, err } compiled++ @@ -213,7 +227,7 @@ func (c *Compiler) doCompile(up urlPtr) (*Schema, error) { return c.schemas[up], nil } -func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error { +func (c *Compiler) compileValue(ctx context.Context, v any, sch *Schema, r *root, q *queue) error { res := r.resource(sch.up.ptr) sch.DraftVersion = res.dialect.draft.version @@ -239,7 +253,7 @@ func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error { case bool: sch.Bool = &v case map[string]any: - if err := c.compileObject(v, sch, r, q); err != nil { + if err := c.compileObject(ctx, v, sch, r, q); err != nil { return err } } @@ -261,7 +275,7 @@ func (c *Compiler) compileValue(v any, sch *Schema, r *root, q *queue) error { return nil } -func (c *Compiler) compileObject(obj map[string]any, sch *Schema, r *root, q *queue) error { +func (c *Compiler) compileObject(ctx context.Context, obj map[string]any, sch *Schema, r *root, q *queue) error { if len(obj) == 0 { b := true sch.Bool = &b @@ -275,7 +289,7 @@ func (c *Compiler) compileObject(obj map[string]any, sch *Schema, r *root, q *qu res: r.resource(sch.up.ptr), q: q, } - return oc.compile(sch) + return oc.compile(ctx, sch) } // queue -- diff --git a/draft_test.go b/draft_test.go index 7b20c48..1ad75ff 100644 --- a/draft_test.go +++ b/draft_test.go @@ -1,6 +1,7 @@ package jsonschema import ( + "context" "reflect" "strings" "testing" @@ -77,7 +78,7 @@ func TestDraft_collectIds(t *testing.T) { resources: map[jsonPointer]*resource{}, subschemasProcessed: map[jsonPointer]struct{}{}, } - if err := rr.collectResources(&r, doc, u, jsonPointer(""), dialect{Draft4, nil}); err != nil { + if err := rr.collectResources(context.Background(), &r, doc, u, jsonPointer(""), dialect{Draft4, nil}); err != nil { t.Fatal(err) } @@ -124,7 +125,7 @@ func TestDraft_collectAnchors(t *testing.T) { resources: map[jsonPointer]*resource{}, subschemasProcessed: map[jsonPointer]struct{}{}, } - if err := rr.collectResources(&r, doc, u, jsonPointer(""), dialect{Draft2020, nil}); err != nil { + if err := rr.collectResources(context.Background(), &r, doc, u, jsonPointer(""), dialect{Draft2020, nil}); err != nil { t.Fatal(err) } diff --git a/example_http_test.go b/example_http_test.go index 82e094e..48f748b 100644 --- a/example_http_test.go +++ b/example_http_test.go @@ -1,6 +1,7 @@ package jsonschema_test import ( + "context" "crypto/tls" "fmt" "log" @@ -13,9 +14,15 @@ import ( type HTTPURLLoader http.Client -func (l *HTTPURLLoader) Load(url string) (any, error) { +func (l *HTTPURLLoader) Load(ctx context.Context, url string) (any, error) { client := (*http.Client)(l) - resp, err := client.Get(url) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) if err != nil { return nil, err } @@ -44,14 +51,14 @@ func Example_fromHTTPS() { schemaURL := "https://raw.githubusercontent.com/santhosh-tekuri/boon/main/tests/examples/schema.json" instanceFile := "./testdata/examples/instance.json" - loader := jsonschema.SchemeURLLoader{ + loader := jsonschema.SchemeURLLoaderContext{ "file": jsonschema.FileLoader{}, "http": newHTTPURLLoader(false), "https": newHTTPURLLoader(false), } c := jsonschema.NewCompiler() - c.UseLoader(loader) + c.UseLoaderContext(loader) sch, err := c.Compile(schemaURL) if err != nil { log.Fatal(err) diff --git a/invalid_schemas_test.go b/invalid_schemas_test.go index a8b714a..efbd3c6 100644 --- a/invalid_schemas_test.go +++ b/invalid_schemas_test.go @@ -1,6 +1,7 @@ package jsonschema_test import ( + "context" "encoding/json" "fmt" "os" @@ -34,11 +35,11 @@ func TestInvalidSchemas(t *testing.T) { t.Log(test.Description) url := "http://invalid-schemas.com/schema.json" c := jsonschema.NewCompiler() - loader := jsonschema.SchemeURLLoader{ + loader := jsonschema.SchemeURLLoaderContext{ "file": jsonschema.FileLoader{}, "http": invalidRemotes(test.Remotes), } - c.UseLoader(loader) + c.UseLoaderContext(loader) if err := c.AddResource(url, test.Schema); err != nil { t.Fatalf("addResource failed: %v", err) } @@ -66,7 +67,7 @@ func TestInvalidSchemas(t *testing.T) { type invalidRemotes map[string]any -func (l invalidRemotes) Load(url string) (any, error) { +func (l invalidRemotes) Load(ctx context.Context, url string) (any, error) { if v, ok := l[url]; ok { return v, nil } diff --git a/loader.go b/loader.go index ce0170e..159e730 100644 --- a/loader.go +++ b/loader.go @@ -1,6 +1,7 @@ package jsonschema import ( + "context" "embed" "encoding/json" "errors" @@ -15,17 +16,32 @@ import ( ) // URLLoader knows how to load json from given url. +// +// Deprecated: use URLLoaderContext type URLLoader interface { // Load loads json from given absolute url. Load(url string) (any, error) } +// URLLoaderContext knows how to load json from given url. +type URLLoaderContext interface { + // Load loads json from given absolute url. + Load(ctx context.Context, url string) (any, error) +} + // -- +// contextAdapterLoader is adapter to convert [URLLoader] to [URLLoaderContext] +type contextAdapterLoader struct{ loader URLLoader } + +func (l contextAdapterLoader) Load(ctx context.Context, url string) (any, error) { + return l.loader.Load(url) +} + // FileLoader loads json file url. type FileLoader struct{} -func (l FileLoader) Load(url string) (any, error) { +func (l FileLoader) Load(ctx context.Context, url string) (any, error) { path, err := l.ToFile(url) if err != nil { return nil, err @@ -59,6 +75,8 @@ func (l FileLoader) ToFile(url string) (string, error) { // SchemeURLLoader delegates to other [URLLoaders] // based on url scheme. +// +// Deprecated: use SchemeURLLoaderContext type SchemeURLLoader map[string]URLLoader func (l SchemeURLLoader) Load(url string) (any, error) { @@ -73,6 +91,22 @@ func (l SchemeURLLoader) Load(url string) (any, error) { return ll.Load(url) } +// SchemeURLLoader delegates to other [URLLoadersContext] +// based on url scheme. +type SchemeURLLoaderContext map[string]URLLoaderContext + +func (l SchemeURLLoaderContext) Load(ctx context.Context, url string) (any, error) { + u, err := gourl.Parse(url) + if err != nil { + return nil, err + } + ll, ok := l[u.Scheme] + if !ok { + return nil, &UnsupportedURLSchemeError{u.String()} + } + return ll.Load(ctx, url) +} + // -- //go:embed metaschemas @@ -128,7 +162,7 @@ func loadMeta(url string) (any, error) { type defaultLoader struct { docs map[url]any // docs loaded so far - loader URLLoader + loader URLLoaderContext } func (l *defaultLoader) add(url url, doc any) bool { @@ -139,7 +173,7 @@ func (l *defaultLoader) add(url url, doc any) bool { return true } -func (l *defaultLoader) load(url url) (any, error) { +func (l *defaultLoader) load(ctx context.Context, url url) (any, error) { if doc, ok := l.docs[url]; ok { return doc, nil } @@ -154,7 +188,7 @@ func (l *defaultLoader) load(url url) (any, error) { if l.loader == nil { return nil, &LoadURLError{url.String(), errors.New("no URLLoader set")} } - doc, err = l.loader.Load(url.String()) + doc, err = l.loader.Load(ctx, url.String()) if err != nil { return nil, &LoadURLError{URL: url.String(), Err: err} } @@ -162,7 +196,7 @@ func (l *defaultLoader) load(url url) (any, error) { return doc, nil } -func (l *defaultLoader) getDraft(up urlPtr, doc any, defaultDraft *Draft, cycle map[url]struct{}) (*Draft, error) { +func (l *defaultLoader) getDraft(ctx context.Context, up urlPtr, doc any, defaultDraft *Draft, cycle map[url]struct{}) (*Draft, error) { obj, ok := doc.(map[string]any) if !ok { return defaultDraft, nil @@ -186,14 +220,14 @@ func (l *defaultLoader) getDraft(up urlPtr, doc any, defaultDraft *Draft, cycle return nil, &MetaSchemaCycleError{schUrl.String()} } cycle[schUrl] = struct{}{} - doc, err := l.load(schUrl) + doc, err := l.load(ctx, schUrl) if err != nil { return nil, err } - return l.getDraft(urlPtr{schUrl, ""}, doc, defaultDraft, cycle) + return l.getDraft(ctx, urlPtr{schUrl, ""}, doc, defaultDraft, cycle) } -func (l *defaultLoader) getMetaVocabs(doc any, draft *Draft, vocabularies map[string]*Vocabulary) ([]string, error) { +func (l *defaultLoader) getMetaVocabs(ctx context.Context, doc any, draft *Draft, vocabularies map[string]*Vocabulary) ([]string, error) { obj, ok := doc.(map[string]any) if !ok { return nil, nil @@ -210,7 +244,7 @@ func (l *defaultLoader) getMetaVocabs(doc any, draft *Draft, vocabularies map[st return nil, &ParseURLError{sch, err} } schUrl := url(sch) - doc, err := l.load(schUrl) + doc, err := l.load(ctx, schUrl) if err != nil { return nil, err } diff --git a/objcompiler.go b/objcompiler.go index f1494b1..b7c95a5 100644 --- a/objcompiler.go +++ b/objcompiler.go @@ -1,6 +1,7 @@ package jsonschema import ( + "context" "encoding/json" "fmt" "math/big" @@ -16,7 +17,7 @@ type objCompiler struct { q *queue } -func (c *objCompiler) compile(s *Schema) error { +func (c *objCompiler) compile(ctx context.Context, s *Schema) error { // id -- if id := c.res.dialect.draft.getID(c.obj); id != "" { s.ID = id @@ -40,7 +41,7 @@ func (c *objCompiler) compile(s *Schema) error { s.Anchor = c.string("$anchor") } - if err := c.compileDraft4(s); err != nil { + if err := c.compileDraft4(ctx, s); err != nil { return err } if s.DraftVersion >= 6 { @@ -54,12 +55,12 @@ func (c *objCompiler) compile(s *Schema) error { } } if s.DraftVersion >= 2019 { - if err := c.compileDraft2019(s); err != nil { + if err := c.compileDraft2019(ctx, s); err != nil { return err } } if s.DraftVersion >= 2020 { - if err := c.compileDraft2020(s); err != nil { + if err := c.compileDraft2020(ctx, s); err != nil { return err } } @@ -83,11 +84,11 @@ func (c *objCompiler) compile(s *Schema) error { return nil } -func (c *objCompiler) compileDraft4(s *Schema) error { +func (c *objCompiler) compileDraft4(ctx context.Context, s *Schema) error { var err error if c.hasVocab("core") { - if s.Ref, err = c.enqueueRef("$ref"); err != nil { + if s.Ref, err = c.enqueueRef(ctx, "$ref"); err != nil { return err } if s.DraftVersion < 2019 && s.Ref != nil { @@ -262,11 +263,11 @@ func (c *objCompiler) compileDraft7(s *Schema) error { return nil } -func (c *objCompiler) compileDraft2019(s *Schema) error { +func (c *objCompiler) compileDraft2019(ctx context.Context, s *Schema) error { var err error if c.hasVocab("core") { - if s.RecursiveRef, err = c.enqueueRef("$recursiveRef"); err != nil { + if s.RecursiveRef, err = c.enqueueRef(ctx, "$recursiveRef"); err != nil { return err } s.RecursiveAnchor = c.boolean("$recursiveAnchor") @@ -314,9 +315,9 @@ func (c *objCompiler) compileDraft2019(s *Schema) error { return nil } -func (c *objCompiler) compileDraft2020(s *Schema) error { +func (c *objCompiler) compileDraft2020(ctx context.Context, s *Schema) error { if c.hasVocab("core") { - sch, err := c.enqueueRef("$dynamicRef") + sch, err := c.enqueueRef(ctx, "$dynamicRef") if err != nil { return err } @@ -350,7 +351,7 @@ func (c *objCompiler) enqueuePtr(ptr jsonPointer) *Schema { return c.c.enqueue(c.q, up) } -func (c *objCompiler) enqueueRef(pname string) (*Schema, error) { +func (c *objCompiler) enqueueRef(ctx context.Context, pname string) (*Schema, error) { ref := c.strVal(pname) if ref == nil { return nil, nil @@ -372,7 +373,7 @@ func (c *objCompiler) enqueueRef(pname string) (*Schema, error) { } // remote ref - up_, err := c.c.roots.resolveFragment(*uf) + up_, err := c.c.roots.resolveFragment(ctx, *uf) if err != nil { return nil, err } diff --git a/roots.go b/roots.go index a8d0ef0..8731769 100644 --- a/roots.go +++ b/roots.go @@ -1,6 +1,7 @@ package jsonschema import ( + "context" "fmt" "strings" ) @@ -27,25 +28,25 @@ func newRoots() *roots { } } -func (rr *roots) orLoad(u url) (*root, error) { +func (rr *roots) orLoad(ctx context.Context, u url) (*root, error) { if r, ok := rr.roots[u]; ok { return r, nil } - doc, err := rr.loader.load(u) + doc, err := rr.loader.load(ctx, u) if err != nil { return nil, err } - return rr.addRoot(u, doc) + return rr.addRoot(ctx, u, doc) } -func (rr *roots) addRoot(u url, doc any) (*root, error) { +func (rr *roots) addRoot(ctx context.Context, u url, doc any) (*root, error) { r := &root{ url: u, doc: doc, resources: map[jsonPointer]*resource{}, subschemasProcessed: map[jsonPointer]struct{}{}, } - if err := rr.collectResources(r, doc, u, "", dialect{rr.defaultDraft, nil}); err != nil { + if err := rr.collectResources(ctx, r, doc, u, "", dialect{rr.defaultDraft, nil}); err != nil { return nil, err } if !strings.HasPrefix(u.String(), "http://json-schema.org/") && @@ -59,26 +60,26 @@ func (rr *roots) addRoot(u url, doc any) (*root, error) { return r, nil } -func (rr *roots) resolveFragment(uf urlFrag) (urlPtr, error) { - r, err := rr.orLoad(uf.url) +func (rr *roots) resolveFragment(ctx context.Context, uf urlFrag) (urlPtr, error) { + r, err := rr.orLoad(ctx, uf.url) if err != nil { return urlPtr{}, err } return r.resolveFragment(uf.frag) } -func (rr *roots) collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error { +func (rr *roots) collectResources(ctx context.Context, r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error { if _, ok := r.subschemasProcessed[schPtr]; ok { return nil } - if err := rr._collectResources(r, sch, base, schPtr, fallback); err != nil { + if err := rr._collectResources(ctx, r, sch, base, schPtr, fallback); err != nil { return err } r.subschemasProcessed[schPtr] = struct{}{} return nil } -func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error { +func (rr *roots) _collectResources(ctx context.Context, r *root, sch any, base url, schPtr jsonPointer, fallback dialect) error { obj, ok := sch.(map[string]any) if !ok { if schPtr.isEmpty() { @@ -97,7 +98,7 @@ func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointe } } - draft, err := rr.loader.getDraft(urlPtr{r.url, schPtr}, sch, fallback.draft, map[url]struct{}{}) + draft, err := rr.loader.getDraft(ctx, urlPtr{r.url, schPtr}, sch, fallback.draft, map[url]struct{}{}) if err != nil { return err } @@ -135,7 +136,7 @@ func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointe } if !found { if hasSchema { - vocabs, err := rr.loader.getMetaVocabs(sch, draft, rr.vocabularies) + vocabs, err := rr.loader.getMetaVocabs(ctx, sch, draft, rr.vocabularies) if err != nil { return err } @@ -182,7 +183,7 @@ func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointe } } for ptr, v := range subschemas { - if err := rr.collectResources(r, v, base, ptr, baseRes.dialect); err != nil { + if err := rr.collectResources(ctx, r, v, base, ptr, baseRes.dialect); err != nil { return err } } @@ -190,8 +191,8 @@ func (rr *roots) _collectResources(r *root, sch any, base url, schPtr jsonPointe return nil } -func (rr *roots) ensureSubschema(up urlPtr) error { - r, err := rr.orLoad(up.url) +func (rr *roots) ensureSubschema(ctx context.Context, up urlPtr) error { + r, err := rr.orLoad(ctx, up.url) if err != nil { return err } @@ -203,7 +204,7 @@ func (rr *roots) ensureSubschema(up urlPtr) error { return err } rClone := r.clone() - if err := rr.addSubschema(rClone, up.ptr); err != nil { + if err := rr.addSubschema(ctx, rClone, up.ptr); err != nil { return err } if err := rr.validate(rClone, v, up.ptr); err != nil { @@ -213,14 +214,14 @@ func (rr *roots) ensureSubschema(up urlPtr) error { return nil } -func (rr *roots) addSubschema(r *root, ptr jsonPointer) error { +func (rr *roots) addSubschema(ctx context.Context, r *root, ptr jsonPointer) error { v, err := (&urlPtr{r.url, ptr}).lookup(r.doc) if err != nil { return err } base := r.resource(ptr) baseURL := base.id - if err := rr.collectResources(r, v, baseURL, ptr, base.dialect); err != nil { + if err := rr.collectResources(ctx, r, v, baseURL, ptr, base.dialect); err != nil { return err } diff --git a/suite_test.go b/suite_test.go index 6518be1..d02c39c 100644 --- a/suite_test.go +++ b/suite_test.go @@ -1,6 +1,7 @@ package jsonschema_test import ( + "context" "encoding/json" "errors" "fmt" @@ -54,11 +55,11 @@ func testFile(t *testing.T, suite, fpath string, draft *jsonschema.Draft) { c.AssertFormat() c.AssertContent() } - loader := jsonschema.SchemeURLLoader{ + loader := jsonschema.SchemeURLLoaderContext{ "file": jsonschema.FileLoader{}, "http": suiteRemotes(suite), } - c.UseLoader(loader) + c.UseLoaderContext(loader) if err := c.AddResource(url, group.Schema); err != nil { t.Fatalf("add resource failed: %v", err) @@ -152,7 +153,7 @@ func TestSuites(t *testing.T) { type suiteRemotes string -func (rl suiteRemotes) Load(url string) (any, error) { +func (rl suiteRemotes) Load(ctx context.Context, url string) (any, error) { if rem, ok := strings.CutPrefix(url, "http://localhost:1234/"); ok { f, err := os.Open(path.Join(string(rl), "remotes", rem)) if err != nil {