diff --git a/bundler/bundler.go b/bundler/bundler.go index 75b9c61b..7bef2678 100644 --- a/bundler/bundler.go +++ b/bundler/bundler.go @@ -10,7 +10,7 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel" - "github.com/pb33f/libopenapi/datamodel/high/v3" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi/index" ) @@ -56,6 +56,7 @@ func BundleDocument(model *v3.Document) ([]byte, error) { func bundle(model *v3.Document, inline bool) ([]byte, error) { rolodex := model.Rolodex + compact := func(idx *index.SpecIndex, root bool) { mappedReferences := idx.GetMappedReferences() sequencedReferences := idx.GetRawReferencesSequenced() @@ -78,6 +79,7 @@ func bundle(model *v3.Document, inline bool) ([]byte, error) { sequenced.Node.Content = mappedReference.Node.Content continue } + if mappedReference != nil && mappedReference.Circular { if idx.GetLogger() != nil { idx.GetLogger().Warn("[bundler] skipping circular reference", diff --git a/bundler/bundler_test.go b/bundler/bundler_test.go index 56ee4d0b..6ab8bfbe 100644 --- a/bundler/bundler_test.go +++ b/bundler/bundler_test.go @@ -6,8 +6,12 @@ package bundler import ( "bytes" "errors" + "fmt" "log" "log/slog" + "net/http" + "net/http/httptest" + "net/url" "os" "os/exec" "path/filepath" @@ -101,6 +105,72 @@ func TestBundleDocument_Circular(t *testing.T) { assert.Len(t, logEntries, 0) } +func TestBundleDocument_MinimalRemoteRefsBundledLocally(t *testing.T) { + specBytes, err := os.ReadFile("../test_specs/minimal_remote_refs/openapi.yaml") + require.NoError(t, err) + + require.NoError(t, err) + + config := &datamodel.DocumentConfiguration{ + AllowFileReferences: true, + AllowRemoteReferences: false, + BundleInlineRefs: false, + BasePath: "../test_specs/minimal_remote_refs", + BaseURL: nil, + } + require.NoError(t, err) + + bytes, e := BundleBytes(specBytes, config) + assert.NoError(t, e) + assert.Contains(t, string(bytes), "Name of the account", "should contain all reference targets") +} + +func TestBundleDocument_MinimalRemoteRefsBundledRemotely(t *testing.T) { + baseURL, err := url.Parse("https://raw.githubusercontent.com/felixjung/libopenapi/authed-remote/test_specs/minimal_remote_refs") + + refBytes, err := os.ReadFile("../test_specs/minimal_remote_refs/schemas/components.openapi.yaml") + require.NoError(t, err) + + wantURL := fmt.Sprintf("%s/%s", baseURL.String(), "schemas/components.openapi.yaml") + + newRemoteHandlerFunc := func() utils.RemoteURLHandler { + handler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() != wantURL { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Write(refBytes) + } + + return func(url string) (*http.Response, error) { + req := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + handler(w, req) + + return w.Result(), nil + } + } + + specBytes, err := os.ReadFile("../test_specs/minimal_remote_refs/openapi.yaml") + require.NoError(t, err) + + require.NoError(t, err) + + config := &datamodel.DocumentConfiguration{ + BaseURL: baseURL, + AllowFileReferences: false, + AllowRemoteReferences: true, + BundleInlineRefs: false, + RemoteURLHandler: newRemoteHandlerFunc(), + } + require.NoError(t, err) + + bytes, e := BundleBytes(specBytes, config) + assert.NoError(t, e) + assert.Contains(t, string(bytes), "Name of the account", "should contain all reference targets") +} + func TestBundleBytes(t *testing.T) { digi, _ := os.ReadFile("../test_specs/circular-tests.yaml") diff --git a/datamodel/document_config.go b/datamodel/document_config.go index 7f374e4f..caa6e6e1 100644 --- a/datamodel/document_config.go +++ b/datamodel/document_config.go @@ -4,11 +4,12 @@ package datamodel import ( - "github.com/pb33f/libopenapi/utils" "io/fs" "log/slog" "net/url" "os" + + "github.com/pb33f/libopenapi/utils" ) // DocumentConfiguration is used to configure the document creation process. It was added in v0.6.0 to allow @@ -18,6 +19,8 @@ import ( // any non-local (local being the specification, not the file system) references, will be ignored. type DocumentConfiguration struct { // The BaseURL will be the root from which relative references will be resolved from if they can't be found locally. + // Make sure it does not point to a file as relative paths will be blindly added to the end of the + // BaseURL's path. // Schema must be set to "http/https". BaseURL *url.URL diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index 0a6b6eaa..61a81df1 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -87,7 +87,6 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S for _, collection := range collections { found = collection() if found != nil && found[rv] != nil { - // if this is a ref node, we need to keep diving // until we hit something that isn't a ref. if jh, _, _ := utils.IsNodeRefValue(found[rv].Node); jh { @@ -106,50 +105,76 @@ func LocateRefNodeWithContext(ctx context.Context, root *yaml.Node, idx *index.S } } - // perform a search for the reference in the index - // extract the correct root + // Obtain the absolute filepath/URL of the spec in which we are trying to + // resolve the reference value [rv] from. It's either available from the + // index or passed down through context. specPath := idx.GetSpecAbsolutePath() if ctx.Value(index.CurrentPathKey) != nil { specPath = ctx.Value(index.CurrentPathKey).(string) } + // explodedRefValue contains both the path to the file containing the + // reference value at index 0 and the path within that file to a specific + // sub-schema, should it exist, at index 1. explodedRefValue := strings.Split(rv, "#") if len(explodedRefValue) == 2 { + // The ref points to a component within either this file or another file. if !strings.HasPrefix(explodedRefValue[0], "http") { + // The ref is not an absolute URL. if !filepath.IsAbs(explodedRefValue[0]) { + // The ref is not an absolute local file path. if strings.HasPrefix(specPath, "http") { + // The schema containing the ref is itself a remote file. u, _ := url.Parse(specPath) + // p is the directory the referenced file is expected to be in. p := "" if u.Path != "" && explodedRefValue[0] != "" { + // We are using the path of the resolved URL from the rolodex to + // obtain the "folder" or base of the file URL. p = filepath.Dir(u.Path) } if p != "" && explodedRefValue[0] != "" { + // We are resolving the relative URL against the absolute URL of + // the spec containing the reference. u.Path = utils.ReplaceWindowsDriveWithLinuxPath(filepath.Join(p, explodedRefValue[0])) } u.Fragment = "" + // Turn the reference value [rv] into the absolute filepath/URL we + // resolved. rv = fmt.Sprintf("%s#%s", u.String(), explodedRefValue[1]) - } else { + // The schema containing the ref is a local file or doesn't have an + // absolute URL. if specPath != "" { + // We have _some_ path for the schema containing the reference. var abs string if explodedRefValue[0] == "" { + // Reference is made within the schema file, so we are using the + // same absolute local filepath. abs = specPath } else { // break off any fragments from the spec path sp := strings.Split(specPath, "#") + // Create a clean (absolute?) path to the file containing the + // referenced value. abs, _ = filepath.Abs(filepath.Join(filepath.Dir(sp[0]), explodedRefValue[0])) } rv = fmt.Sprintf("%s#%s", abs, explodedRefValue[1]) } else { - // check for a config baseURL and use that if it exists. - if idx.GetConfig().BaseURL != nil { + // We don't have a path for the schema we are trying to resolve + // relative references from. This likely happens when the schema + // is the root schema, i.e. the file given to libopenapi as entry. + // + // check for a config BaseURL and use that if it exists. + if idx.GetConfig().BaseURL != nil { u := *idx.GetConfig().BaseURL p := "" if u.Path != "" { - p = filepath.Dir(u.Path) + p = u.Path } - u.Path = filepath.Join(p, explodedRefValue[0]) + + u.Path = utils.ReplaceWindowsDriveWithLinuxPath(filepath.Join(p, explodedRefValue[0])) rv = fmt.Sprintf("%s#%s", u.String(), explodedRefValue[1]) } } diff --git a/datamodel/low/extraction_functions_test.go b/datamodel/low/extraction_functions_test.go index b5950511..cd501c94 100644 --- a/datamodel/low/extraction_functions_test.go +++ b/datamodel/low/extraction_functions_test.go @@ -880,7 +880,7 @@ func TestExtractArray_BadRefPropsTupe(t *testing.T) { assert.NoError(t, mErr) idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) - yml = `limes: + yml = `limes: $ref: '#/components/parameters/cakes'` var cNode yaml.Node @@ -1867,6 +1867,34 @@ func TestLocateRefNode_NoExplode_NoSpecPath(t *testing.T) { assert.NotNil(t, c) } +func TestLocateRefNode_Explode_NoSpecPath(t *testing.T) { + no := yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + { + Kind: yaml.ScalarNode, + Value: "$ref", + }, + { + Kind: yaml.ScalarNode, + Value: "components/schemas/thing.yaml#/components/schemas/thing", + }, + }, + } + + cf := index.CreateClosedAPIIndexConfig() + u, _ := url.Parse("http://smilfghfhfhfhfhes.com/bikes") + cf.BaseURL = u + idx := index.NewSpecIndexWithConfig(&no, cf) + ctx := context.Background() + + n, i, e, c := LocateRefNodeWithContext(ctx, &no, idx) + assert.Nil(t, n) + assert.NotNil(t, i) + assert.NotNil(t, e) + assert.NotNil(t, c) +} + func TestLocateRefNode_DoARealLookup(t *testing.T) { lookup := "/root.yaml#/components/schemas/Burger" if runtime.GOOS == "windows" { @@ -2078,7 +2106,7 @@ func TestArray_NotRefNotArray(t *testing.T) { assert.NoError(t, mErr) idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateClosedAPIIndexConfig()) - yml = `limes: + yml = `limes: not: array` var cNode yaml.Node diff --git a/datamodel/low/v3/create_document.go b/datamodel/low/v3/create_document.go index 20ac4941..19ea28f3 100644 --- a/datamodel/low/v3/create_document.go +++ b/datamodel/low/v3/create_document.go @@ -3,7 +3,9 @@ package v3 import ( "context" "errors" + "net/url" "path/filepath" + "strings" "sync" "time" @@ -43,7 +45,7 @@ func createDocument(info *datamodel.SpecInfo, config *datamodel.DocumentConfigur idxConfig.IgnoreArrayCircularReferences = config.IgnoreArrayCircularReferences idxConfig.IgnorePolymorphicCircularReferences = config.IgnorePolymorphicCircularReferences idxConfig.AvoidCircularReferenceCheck = true - idxConfig.BaseURL = config.BaseURL + idxConfig.BaseURL = urlWithoutTrailingSlash(config.BaseURL) idxConfig.BasePath = config.BasePath idxConfig.SpecFilePath = config.SpecFilePath idxConfig.Logger = config.Logger @@ -319,3 +321,13 @@ func extractWebhooks(ctx context.Context, info *datamodel.SpecInfo, doc *Documen } return nil } + +func urlWithoutTrailingSlash(u *url.URL) *url.URL { + if u == nil { + return nil + } + + u.Path, _ = strings.CutSuffix(u.Path, "/") + + return u +} diff --git a/datamodel/low/v3/create_document_test.go b/datamodel/low/v3/create_document_test.go index d039bb9d..09ff0823 100644 --- a/datamodel/low/v3/create_document_test.go +++ b/datamodel/low/v3/create_document_test.go @@ -881,3 +881,48 @@ func ExampleCreateDocument() { fmt.Print(document.Info.Value.Contact.Value.Email.Value) // Output: apiteam@swagger.io } + +func TestURLWithoutTrailingSlash(t *testing.T) { + tc := []struct { + name string + url string + want string + }{ + { + name: "url with no path", + url: "https://example.com", + want: "https://example.com", + }, + { + name: "nil pointer", + url: "", + }, + { + name: "URL with path not ending in slash", + url: "https://example.com/some/path", + want: "https://example.com/some/path", + }, + { + name: "URL with path ending in slash", + url: "https://example.com/some/path/", + want: "https://example.com/some/path", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + u, _ := url.Parse(tt.url) + if tt.url == "" { + u = nil + } + + got := urlWithoutTrailingSlash(u) + + if u == nil { + assert.Nil(t, got) + return + } + assert.Equal(t, tt.want, got.String()) + }) + } +} diff --git a/document.go b/document.go index 7fe190bb..9b877a65 100644 --- a/document.go +++ b/document.go @@ -305,7 +305,9 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { if d.config == nil { d.config = &datamodel.DocumentConfiguration{ AllowFileReferences: false, + BasePath: "", AllowRemoteReferences: false, + BaseURL: nil, } } @@ -335,6 +337,7 @@ func (d *document) BuildV3Model() (*DocumentModel[v3high.Document], []error) { Model: *highDoc, Index: lowDoc.Index, } + return d.highOpenAPI3Model, errs } diff --git a/document_test.go b/document_test.go index 17a3adb1..e51b8451 100644 --- a/document_test.go +++ b/document_test.go @@ -6,11 +6,14 @@ import ( "bytes" "fmt" "log/slog" + "net/http" + "net/url" "os" "runtime" "strconv" "strings" "testing" + "time" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high/base" @@ -216,7 +219,7 @@ func TestDocument_RenderAndReload_ChangeCheck_Burgershop(t *testing.T) { // compare documents compReport, errs := CompareDocuments(doc, newDoc) - // should noth be nil. + // should not be nil. assert.Nil(t, errs) assert.Nil(t, errs) assert.NotNil(t, rend) @@ -450,7 +453,7 @@ func TestDocument_Render_ChangeCheck_Burgershop(t *testing.T) { // compare documents compReport, errs := CompareDocuments(doc, newDoc) - // should noth be nil. + // should not be nil. assert.Nil(t, errs) assert.NotNil(t, rend) assert.Nil(t, compReport) @@ -1354,6 +1357,44 @@ func TestDocument_TestNestedFiles(t *testing.T) { require.Empty(t, errs) } +func TestDocument_MinimalRemoteRefs(t *testing.T) { + newRemoteHandlerFunc := func() utils.RemoteURLHandler { + c := &http.Client{ + Timeout: time.Second * 120, + } + + return func(url string) (*http.Response, error) { + resp, err := c.Get(url) + if err != nil { + return nil, fmt.Errorf("fetch remote ref: %v", err) + } + + return resp, nil + } + } + + spec, err := os.ReadFile("test_specs/minimal_remote_refs/openapi.yaml") + require.NoError(t, err) + + baseURL, err := url.Parse("https://raw.githubusercontent.com/felixjung/libopenapi/authed-remote/test_specs/minimal_remote_refs") + require.NoError(t, err) + + doc, err := NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BaseURL: baseURL, + AllowFileReferences: false, + AllowRemoteReferences: true, + RemoteURLHandler: newRemoteHandlerFunc(), + }) + require.NoError(t, err) + + d, errs := doc.BuildV3Model() + require.Empty(t, errs) + + o, err := d.Model.Render() + require.NoError(t, err) + fmt.Println(string(o)) +} + func TestDocument_Issue264(t *testing.T) { openAPISpec := `{"openapi":"3.0.0","info":{"title":"dummy","version":"1.0.0"},"paths":{"/dummy":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"number","format":"decimal","multipleOf":0.01,"minimum":-999.99}}}}}},"responses":{"200":{"description":"OK"}}}}}}` diff --git a/go.mod b/go.mod index 3a0ef178..58a6c85b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/stretchr/testify v1.9.0 github.com/vmware-labs/yaml-jsonpath v0.3.2 github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd - golang.org/x/net v0.0.0-20220225172249-27dd8689420f gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index cf0aa6f5..71ffd910 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097 h1:f5nA5Ys8RXqFXtKc0XofVRiuwNTuJzPIwTmbjLz9vj8= github.com/dprotaso/go-yit v0.0.0-20240618133044-5a0af90af097/go.mod h1:FTAVyH6t+SlS97rv6EXRVuBDLkQqcIe/xQw9f4IFUI4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -65,8 +63,6 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= diff --git a/index/extract_refs.go b/index/extract_refs.go index 930c113a..7bb9d915 100644 --- a/index/extract_refs.go +++ b/index/extract_refs.go @@ -9,11 +9,11 @@ import ( "net/url" "os" "path/filepath" + "slices" "strings" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "slices" ) // ExtractRefs will return a deduplicated slice of references for every unique ref found in the document. @@ -56,6 +56,7 @@ func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, fullDefinitionPath = fmt.Sprintf("%s#/%s", index.specAbsolutePath, strings.Join(loc, "/")) _, jsonPath = utils.ConvertComponentIdIntoFriendlyPathSearch(definitionPath) } + ref := &Reference{ ParentNode: parent, FullDefinition: fullDefinitionPath, @@ -245,6 +246,7 @@ func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, var componentName string var fullDefinitionPath string if len(uri) == 2 { + // Check if we are dealing with a ref to a local definition. if uri[0] == "" { fullDefinitionPath = fmt.Sprintf("%s#/%s", index.specAbsolutePath, uri[1]) componentName = value @@ -423,6 +425,7 @@ func (index *SpecIndex) ExtractRefs(node, parent *yaml.Node, seenPath []string, continue } + // This sets the ref in the path using the full URL and sub-path. index.allRefs[fullDefinitionPath] = ref found = append(found, ref) } @@ -712,5 +715,6 @@ func (index *SpecIndex) ExtractComponentsFromRefs(refs []*Reference) []*Referenc index.allMappedRefsSequenced = append(index.allMappedRefsSequenced, mappedRefsInSequence[m]) } } + return found } diff --git a/index/rolodex.go b/index/rolodex.go index ca2ebab9..b0b40735 100644 --- a/index/rolodex.go +++ b/index/rolodex.go @@ -6,7 +6,6 @@ package index import ( "errors" "fmt" - "gopkg.in/yaml.v3" "io" "io/fs" "log/slog" @@ -19,6 +18,8 @@ import ( "strings" "sync" "time" + + "gopkg.in/yaml.v3" ) // CanBeIndexed is an interface that allows a file to be indexed. @@ -159,6 +160,7 @@ func (r *Rolodex) SetRootNode(node *yaml.Node) { func (r *Rolodex) AddExternalIndex(idx *SpecIndex, location string) { r.indexLock.Lock() + r.indexes = append(r.indexes, idx) if r.indexMap[location] == nil { r.indexMap[location] = idx } @@ -166,7 +168,6 @@ func (r *Rolodex) AddExternalIndex(idx *SpecIndex, location string) { } func (r *Rolodex) AddIndex(idx *SpecIndex) { - r.indexes = append(r.indexes, idx) if idx != nil { p := idx.specAbsolutePath r.AddExternalIndex(idx, p) @@ -193,7 +194,8 @@ func (r *Rolodex) IndexTheRolodex() error { var indexBuildQueue []*SpecIndex indexRolodexFile := func( - location string, fs fs.FS, + location string, + fs fs.FS, doneChan chan bool, errChan chan error, indexChan chan *SpecIndex) { @@ -231,6 +233,7 @@ func (r *Rolodex) IndexTheRolodex() error { if lfs, ok := fs.(RolodexFS); ok { wait := false + for _, f := range lfs.GetFiles() { if idxFile, ko := f.(CanBeIndexed); ko { wg.Add(1) @@ -306,7 +309,6 @@ func (r *Rolodex) IndexTheRolodex() error { // if there is a base path but no SpecFilePath, then we need to set the root spec config to point to a theoretical root.yaml // which does not exist, but is used to formulate the absolute path to root references correctly. if r.indexConfig.BasePath != "" && r.indexConfig.BaseURL == nil { - basePath := r.indexConfig.BasePath if !filepath.IsAbs(basePath) { basePath, _ = filepath.Abs(basePath) @@ -321,6 +323,8 @@ func (r *Rolodex) IndexTheRolodex() error { } } + // Here we take the root node and also build the index for it. + // This involves extracting references. index := NewSpecIndexWithConfig(r.rootNode, r.indexConfig) resolver := NewResolver(index) diff --git a/index/rolodex_file_loader.go b/index/rolodex_file_loader.go index b5164eb0..17827824 100644 --- a/index/rolodex_file_loader.go +++ b/index/rolodex_file_loader.go @@ -124,7 +124,6 @@ func (l *LocalFS) Open(name string) (fs.File, error) { if idxError != nil && idx == nil { extractedFile.readingErrors = append(l.readingErrors, idxError) } else { - // for each index, we need a resolver resolver := NewResolver(idx) idx.resolver = resolver diff --git a/index/rolodex_remote_loader.go b/index/rolodex_remote_loader.go index ab6a8e65..f5f34fe5 100644 --- a/index/rolodex_remote_loader.go +++ b/index/rolodex_remote_loader.go @@ -14,12 +14,12 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" - "sync" ) const ( @@ -441,11 +441,6 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) { i.logger.Debug("[rolodex remote loaded] successfully loaded file", "file", absolutePath) } - processingWaiter.file = remoteFile - processingWaiter.done = true - - // remove from processing - i.ProcessingFiles.Delete(remoteParsedURL.Path) i.Files.Store(absolutePath, remoteFile) idx, idxError := remoteFile.Index(&copiedCfg) @@ -462,5 +457,10 @@ func (i *RemoteFS) Open(remoteURL string) (fs.File, error) { i.rolodex.AddExternalIndex(idx, remoteParsedURL.String()) } } + + // remove from processing + processingWaiter.file = remoteFile + processingWaiter.done = true + i.ProcessingFiles.Delete(remoteParsedURL.Path) return remoteFile, errors.Join(i.remoteErrors...) } diff --git a/orderedmap/orderedmap_test.go b/orderedmap/orderedmap_test.go index b1637872..d73fa343 100644 --- a/orderedmap/orderedmap_test.go +++ b/orderedmap/orderedmap_test.go @@ -271,6 +271,11 @@ func TestFirst(t *testing.T) { require.Nil(t, pair) }) + t.Run("Nil map", func(t *testing.T) { + var m orderedmap.Map[string, int] + require.Nil(t, m.First()) + }) + t.Run("Single item", func(t *testing.T) { m := orderedmap.New[string, int]() m.Set("key", 1) diff --git a/test_specs/minimal_remote_refs/openapi.yaml b/test_specs/minimal_remote_refs/openapi.yaml new file mode 100644 index 00000000..6e2fc34a --- /dev/null +++ b/test_specs/minimal_remote_refs/openapi.yaml @@ -0,0 +1,36 @@ +--- +openapi: 3.1.0 +info: + description: Example API spec + version: v1 + title: Example + contact: + name: Example + email: example@example.com + url: www.example.com + license: + name: Example + url: www.example.com +tags: + - name: Account + description: Account +servers: + - url: https:// +paths: + /api/v1/Accounts: + get: + summary: TODO + description: TODO + security: + - BearerAuth: [] + tags: + - Account + operationId: listAccounts + responses: + "200": + $ref: ./schemas/components.openapi.yaml#/components/responses/ListAccounts +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer diff --git a/test_specs/minimal_remote_refs/schemas/components.openapi.yaml b/test_specs/minimal_remote_refs/schemas/components.openapi.yaml new file mode 100644 index 00000000..f4dd599f --- /dev/null +++ b/test_specs/minimal_remote_refs/schemas/components.openapi.yaml @@ -0,0 +1,39 @@ +--- +openapi: 3.1.0 +info: + description: Example API component definitions + version: v1 + title: Example + contact: + name: Example + email: example@example.com + url: www.example.com + license: + name: Example + url: www.example.com +components: + schemas: + Account: + type: object + properties: + name: + type: string + description: > + Name of the account + responses: + ListAccounts: + description: List all accounts. + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + description: > + The accounts. + $ref: "#/components/schemas/Account" + total: + type: integer + description: Total number of accounts.