diff --git a/document_test.go b/document_test.go index 4db12d4..b3e857c 100644 --- a/document_test.go +++ b/document_test.go @@ -1191,3 +1191,61 @@ func TestDocument_AdvanceCallbackReferences(t *testing.T) { assert.Empty(t, buf.String()) } + +func BenchmarkLoadDocTwice(b *testing.B) { + for i := 0; i < b.N; i++ { + + spec, err := os.ReadFile("test_specs/speakeasy-test.yaml") + require.NoError(b, err) + + doc, err := NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BasePath: "./test_specs", + IgnorePolymorphicCircularReferences: true, + IgnoreArrayCircularReferences: true, + AllowFileReferences: true, + }) + require.NoError(b, err) + + _, errs := doc.BuildV3Model() + require.Empty(b, errs) + + doc, err = NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BasePath: "./test_specs", + IgnorePolymorphicCircularReferences: true, + IgnoreArrayCircularReferences: true, + AllowFileReferences: true, + }) + require.NoError(b, err) + + _, errs = doc.BuildV3Model() + require.Empty(b, errs) + + } +} + +func TestDocument_LoadDocTwice(t *testing.T) { + spec, err := os.ReadFile("test_specs/speakeasy-test.yaml") + require.NoError(t, err) + + doc, err := NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BasePath: "./test_specs", + IgnorePolymorphicCircularReferences: true, + IgnoreArrayCircularReferences: true, + AllowFileReferences: true, + }) + require.NoError(t, err) + + _, errs := doc.BuildV3Model() + require.Empty(t, errs) + + doc, err = NewDocumentWithConfiguration(spec, &datamodel.DocumentConfiguration{ + BasePath: "./test_specs", + IgnorePolymorphicCircularReferences: true, + IgnoreArrayCircularReferences: true, + AllowFileReferences: true, + }) + require.NoError(t, err) + + _, errs = doc.BuildV3Model() + require.Empty(t, errs) +} diff --git a/index/index_model.go b/index/index_model.go index c0c4f14..d9d9ad2 100644 --- a/index/index_model.go +++ b/index/index_model.go @@ -4,14 +4,15 @@ package index import ( - "github.com/pb33f/libopenapi/datamodel" - "golang.org/x/sync/syncmap" "io/fs" "log/slog" "net/http" "net/url" "sync" + "github.com/pb33f/libopenapi/datamodel" + "golang.org/x/sync/syncmap" + "gopkg.in/yaml.v3" ) @@ -235,7 +236,7 @@ type SpecIndex struct { allRefSchemaDefinitions []*Reference // all schemas found that are references. allInlineSchemaDefinitions []*Reference // all schemas found in document outside of components (openapi) or definitions (swagger). allInlineSchemaObjectDefinitions []*Reference // all schemas that are objects found in document outside of components (openapi) or definitions (swagger). - allComponentSchemaDefinitions map[string]*Reference // all schemas found in components (openapi) or definitions (swagger). + allComponentSchemaDefinitions *syncmap.Map // all schemas found in components (openapi) or definitions (swagger). securitySchemesNode *yaml.Node // components/securitySchemes node allSecuritySchemes map[string]*Reference // all security schemes / definitions. requestBodiesNode *yaml.Node // components/requestBodies node diff --git a/index/index_utils.go b/index/index_utils.go index 8703c43..e3fe0dd 100644 --- a/index/index_utils.go +++ b/index/index_utils.go @@ -4,8 +4,10 @@ package index import ( - "gopkg.in/yaml.v3" "strings" + "sync" + + "gopkg.in/yaml.v3" ) func isHttpMethod(val string) bool { @@ -52,7 +54,7 @@ func boostrapIndexCollections(rootNode *yaml.Node, index *SpecIndex) { index.linksRefs = make(map[string]map[string][]*Reference) index.callbackRefs = make(map[string]*Reference) index.externalSpecIndex = make(map[string]*SpecIndex) - index.allComponentSchemaDefinitions = make(map[string]*Reference) + index.allComponentSchemaDefinitions = &sync.Map{} index.allParameters = make(map[string]*Reference) index.allSecuritySchemes = make(map[string]*Reference) index.allRequestBodies = make(map[string]*Reference) diff --git a/index/search_index.go b/index/search_index.go index 1260e8f..996f7ea 100644 --- a/index/search_index.go +++ b/index/search_index.go @@ -13,8 +13,10 @@ import ( type ContextKey string -const CurrentPathKey ContextKey = "currentPath" -const FoundIndexKey ContextKey = "foundIndex" +const ( + CurrentPathKey ContextKey = "currentPath" + FoundIndexKey ContextKey = "foundIndex" +) func (index *SpecIndex) SearchIndexForReferenceByReference(fullRef *Reference) (*Reference, *SpecIndex) { r, idx, _ := index.SearchIndexForReferenceByReferenceWithContext(context.Background(), fullRef) @@ -33,7 +35,6 @@ func (index *SpecIndex) SearchIndexForReferenceWithContext(ctx context.Context, } func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx context.Context, searchRef *Reference) (*Reference, *SpecIndex, context.Context) { - if index.cache != nil { if v, ok := index.cache.Load(searchRef.FullDefinition); ok { return v.(*Reference), v.(*Reference).Index, context.WithValue(ctx, CurrentPathKey, v.(*Reference).RemoteLocation) @@ -74,12 +75,10 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex refAlt = fmt.Sprintf("%s#/%s", absPath, uri[1]) } - } else { if filepath.IsAbs(uri[0]) { roloLookup = uri[0] } else { - if strings.HasPrefix(uri[0], "http") { roloLookup = ref } else { @@ -107,9 +106,11 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex return r, r.Index, context.WithValue(ctx, CurrentPathKey, r.RemoteLocation) } - if r, ok := index.allComponentSchemaDefinitions[refAlt]; ok { + if r, ok := index.allComponentSchemaDefinitions.Load(refAlt); ok { + ref := r.(*Reference) + index.cache.Store(refAlt, r) - return r, r.Index, context.WithValue(ctx, CurrentPathKey, r.RemoteLocation) + return ref, ref.Index, context.WithValue(ctx, CurrentPathKey, ref.RemoteLocation) } // check the rolodex for the reference. @@ -184,5 +185,4 @@ func (index *SpecIndex) SearchIndexForReferenceByReferenceWithContext(ctx contex index.logger.Error("unable to locate reference anywhere in the rolodex", "reference", ref) } return nil, index, ctx - } diff --git a/index/spec_index.go b/index/spec_index.go index cc35039..5a0d055 100644 --- a/index/spec_index.go +++ b/index/spec_index.go @@ -14,15 +14,16 @@ package index import ( "fmt" - "github.com/pb33f/libopenapi/utils" - "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" - "golang.org/x/sync/syncmap" - "gopkg.in/yaml.v3" "log/slog" "os" "sort" "strings" "sync" + + "github.com/pb33f/libopenapi/utils" + "github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath" + "golang.org/x/sync/syncmap" + "gopkg.in/yaml.v3" ) // NewSpecIndexWithConfig will create a new index of an OpenAPI or Swagger spec. It uses the same logic as NewSpecIndex @@ -284,7 +285,7 @@ func (index *SpecIndex) GetAllReferenceSchemas() []*Reference { // GetAllComponentSchemas will return all schemas defined in the components section of the document. func (index *SpecIndex) GetAllComponentSchemas() map[string]*Reference { - return index.allComponentSchemaDefinitions + return syncMapToMap[string, *Reference](index.allComponentSchemaDefinitions) } // GetAllSecuritySchemes will return all security schemes / definitions found in the document. diff --git a/index/utility_methods.go b/index/utility_methods.go index 0da56d5..aaac649 100644 --- a/index/utility_methods.go +++ b/index/utility_methods.go @@ -34,7 +34,7 @@ func (index *SpecIndex) extractDefinitionsAndSchemas(schemasNode *yaml.Node, pat ParentNode: schemasNode, RequiredRefProperties: extractDefinitionRequiredRefProperties(schemasNode, map[string][]string{}, fullDef, index), } - index.allComponentSchemaDefinitions[def] = ref + index.allComponentSchemaDefinitions.Store(def, ref) } } @@ -308,7 +308,6 @@ func (index *SpecIndex) extractComponentExamples(examplesNode *yaml.Node, pathPr } func (index *SpecIndex) extractComponentSecuritySchemes(securitySchemesNode *yaml.Node, pathPrefix string) { - var name string for i, schema := range securitySchemesNode.Content { if i%2 == 0 { @@ -398,8 +397,7 @@ func (index *SpecIndex) scanOperationParams(params []*yaml.Node, pathItemNode *y }) } else { if paramRef != nil { - index.paramOpRefs[pathItemNode.Value][method][paramRefName] = - append(index.paramOpRefs[pathItemNode.Value][method][paramRefName], paramRef) + index.paramOpRefs[pathItemNode.Value][method][paramRefName] = append(index.paramOpRefs[pathItemNode.Value][method][paramRefName], paramRef) } } @@ -466,13 +464,11 @@ func (index *SpecIndex) scanOperationParams(params []*yaml.Node, pathItemNode *y Path: path, }) } else { - index.paramOpRefs[pathItemNode.Value][method][ref.Name] = - append(index.paramOpRefs[pathItemNode.Value][method][ref.Name], ref) + index.paramOpRefs[pathItemNode.Value][method][ref.Name] = append(index.paramOpRefs[pathItemNode.Value][method][ref.Name], ref) } } } else { - index.paramOpRefs[pathItemNode.Value][method][ref.Name] = - append(index.paramOpRefs[pathItemNode.Value][method][ref.Name], ref) + index.paramOpRefs[pathItemNode.Value][method][ref.Name] = append(index.paramOpRefs[pathItemNode.Value][method][ref.Name], ref) } continue } @@ -489,7 +485,6 @@ func runIndexFunction(funcs []func() int, wg *sync.WaitGroup) { } func GenerateCleanSpecConfigBaseURL(baseURL *url.URL, dir string, includeFile bool) string { - cleanedPath := baseURL.Path // not cleaned yet! // create a slice of path segments from existing path @@ -533,7 +528,17 @@ func GenerateCleanSpecConfigBaseURL(baseURL *url.URL, dir string, includeFile bo } else { p = cleanedPath } - } return strings.TrimSuffix(p, "/") } + +func syncMapToMap[K comparable, V any](sm *sync.Map) map[K]V { + m := make(map[K]V) + + sm.Range(func(key, value interface{}) bool { + m[key.(K)] = value.(V) + return true + }) + + return m +}