mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-07 20:47:45 +00:00
Create document_interation_benchmark_test.go
This commit is contained in:
326
document_interation_benchmark_test.go
Normal file
326
document_interation_benchmark_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package libopenapi_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pb33f/libopenapi"
|
||||
"github.com/pb33f/libopenapi/datamodel"
|
||||
"github.com/pb33f/libopenapi/datamodel/high/base"
|
||||
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
|
||||
"github.com/pb33f/libopenapi/orderedmap"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type loopFrameBenchmark struct {
|
||||
Type string
|
||||
Restricted bool
|
||||
}
|
||||
|
||||
type contextBenchmark struct {
|
||||
visited []string
|
||||
stack []loopFrameBenchmark
|
||||
}
|
||||
|
||||
func Benchmark_Docusign_Document_Iteration(b *testing.B) {
|
||||
// Setup code: read the spec file
|
||||
spec, err := os.ReadFile("test_specs/docusignv3.1.json")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
|
||||
config := &datamodel.DocumentConfiguration{
|
||||
BasePath: "./test_specs",
|
||||
IgnorePolymorphicCircularReferences: true,
|
||||
IgnoreArrayCircularReferences: true,
|
||||
AllowFileReferences: true,
|
||||
}
|
||||
|
||||
b.ResetTimer() // Reset the timer after setup
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
// Code to benchmark
|
||||
|
||||
doc, err := libopenapi.NewDocumentWithConfiguration(spec, config)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create new document: %v", err)
|
||||
}
|
||||
|
||||
m, errs := doc.BuildV3Model()
|
||||
if len(errs) > 0 {
|
||||
b.Fatalf("Failed to build V3 model with errors: %v", errs)
|
||||
}
|
||||
|
||||
for path, pathItem := range m.Model.Paths.PathItems.FromOldest() {
|
||||
// Optional logging
|
||||
if b.N == 1 || testing.Verbose() {
|
||||
b.Logf("Path: %s", path)
|
||||
}
|
||||
|
||||
iterateOperationsBenchmark(b, pathItem.GetOperations())
|
||||
}
|
||||
|
||||
for path, pathItem := range m.Model.Webhooks.FromOldest() {
|
||||
if b.N == 1 || testing.Verbose() {
|
||||
b.Logf("Webhook Path: %s", path)
|
||||
}
|
||||
|
||||
iterateOperationsBenchmark(b, pathItem.GetOperations())
|
||||
}
|
||||
|
||||
for name, schemaProxy := range m.Model.Components.Schemas.FromOldest() {
|
||||
if b.N == 1 || testing.Verbose() {
|
||||
b.Logf("Schema Name: %s", name)
|
||||
}
|
||||
|
||||
handleSchemaBenchmark(b, schemaProxy, contextBenchmark{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func iterateOperationsBenchmark(b *testing.B, ops *orderedmap.Map[string, *v3.Operation]) {
|
||||
for method, op := range ops.FromOldest() {
|
||||
b.Log(method)
|
||||
|
||||
for i, param := range op.Parameters {
|
||||
b.Log("param", i, param.Name)
|
||||
|
||||
if param.Schema != nil {
|
||||
handleSchemaBenchmark(b, param.Schema, contextBenchmark{})
|
||||
}
|
||||
}
|
||||
|
||||
if op.RequestBody != nil {
|
||||
b.Log("request body")
|
||||
|
||||
for contentType, mediaType := range op.RequestBody.Content.FromOldest() {
|
||||
b.Log(contentType)
|
||||
|
||||
if mediaType.Schema != nil {
|
||||
handleSchemaBenchmark(b, mediaType.Schema, contextBenchmark{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if orderedmap.Len(op.Responses.Codes) > 0 {
|
||||
b.Log("responses")
|
||||
}
|
||||
|
||||
for code, response := range op.Responses.Codes.FromOldest() {
|
||||
b.Log(code)
|
||||
|
||||
for contentType, mediaType := range response.Content.FromOldest() {
|
||||
b.Log(contentType)
|
||||
|
||||
if mediaType.Schema != nil {
|
||||
handleSchemaBenchmark(b, mediaType.Schema, contextBenchmark{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if orderedmap.Len(op.Responses.Codes) > 0 {
|
||||
b.Log("callbacks")
|
||||
}
|
||||
|
||||
for callbackName, callback := range op.Callbacks.FromOldest() {
|
||||
b.Log(callbackName)
|
||||
|
||||
for name, pathItem := range callback.Expression.FromOldest() {
|
||||
b.Log(name)
|
||||
|
||||
iterateOperationsBenchmark(b, pathItem.GetOperations())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleSchemaBenchmark(b *testing.B, schProxy *base.SchemaProxy, ctx contextBenchmark) {
|
||||
if checkCircularReferenceBenchmark(b, &ctx, schProxy) {
|
||||
return
|
||||
}
|
||||
|
||||
sch, err := schProxy.BuildSchema()
|
||||
require.NoError(b, err)
|
||||
|
||||
typ, subTypes := getResolvedType(sch)
|
||||
|
||||
b.Log("schema", typ, subTypes)
|
||||
|
||||
if len(sch.Enum) > 0 {
|
||||
switch typ {
|
||||
case "string":
|
||||
return
|
||||
case "integer":
|
||||
return
|
||||
default:
|
||||
// handle as base type
|
||||
}
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "allOf":
|
||||
fallthrough
|
||||
case "anyOf":
|
||||
fallthrough
|
||||
case "oneOf":
|
||||
if len(subTypes) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
handleAllOfAnyOfOneOfBenchmark(b, sch, ctx)
|
||||
case "array":
|
||||
handleArrayBenchmark(b, sch, ctx)
|
||||
case "object":
|
||||
handleObject(b, sch, ctx)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getResolvedType(sch *base.Schema) (string, []string) {
|
||||
subTypes := []string{}
|
||||
|
||||
for _, t := range sch.Type {
|
||||
if t == "" { // treat empty type as any
|
||||
subTypes = append(subTypes, "any")
|
||||
} else if t != "null" {
|
||||
subTypes = append(subTypes, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sch.AllOf) > 0 {
|
||||
return "allOf", nil
|
||||
}
|
||||
|
||||
if len(sch.AnyOf) > 0 {
|
||||
return "anyOf", nil
|
||||
}
|
||||
|
||||
if len(sch.OneOf) > 0 {
|
||||
return "oneOf", nil
|
||||
}
|
||||
|
||||
if len(subTypes) == 0 {
|
||||
if len(sch.Enum) > 0 {
|
||||
return "string", nil
|
||||
}
|
||||
|
||||
if orderedmap.Len(sch.Properties) > 0 {
|
||||
return "object", nil
|
||||
}
|
||||
|
||||
if sch.AdditionalProperties != nil {
|
||||
return "object", nil
|
||||
}
|
||||
|
||||
if sch.Items != nil {
|
||||
return "array", nil
|
||||
}
|
||||
|
||||
return "any", nil
|
||||
}
|
||||
|
||||
if len(subTypes) == 1 {
|
||||
return subTypes[0], nil
|
||||
}
|
||||
|
||||
return "oneOf", subTypes
|
||||
}
|
||||
|
||||
func handleAllOfAnyOfOneOfBenchmark(b *testing.B, sch *base.Schema, ctx contextBenchmark) {
|
||||
var schemas []*base.SchemaProxy
|
||||
|
||||
switch {
|
||||
case len(sch.AllOf) > 0:
|
||||
schemas = sch.AllOf
|
||||
case len(sch.AnyOf) > 0:
|
||||
schemas = sch.AnyOf
|
||||
ctx.stack = append(ctx.stack, loopFrameBenchmark{Type: "anyOf", Restricted: len(sch.AnyOf) == 1})
|
||||
case len(sch.OneOf) > 0:
|
||||
schemas = sch.OneOf
|
||||
ctx.stack = append(ctx.stack, loopFrameBenchmark{Type: "oneOf", Restricted: len(sch.OneOf) == 1})
|
||||
}
|
||||
|
||||
for _, s := range schemas {
|
||||
handleSchemaBenchmark(b, s, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func handleArrayBenchmark(b *testing.B, sch *base.Schema, ctx contextBenchmark) {
|
||||
ctx.stack = append(ctx.stack, loopFrameBenchmark{Type: "array", Restricted: sch.MinItems != nil && *sch.MinItems > 0})
|
||||
|
||||
if sch.Items != nil && sch.Items.IsA() {
|
||||
handleSchemaBenchmark(b, sch.Items.A, ctx)
|
||||
}
|
||||
|
||||
if sch.Contains != nil {
|
||||
handleSchemaBenchmark(b, sch.Contains, ctx)
|
||||
}
|
||||
|
||||
if sch.PrefixItems != nil {
|
||||
for _, s := range sch.PrefixItems {
|
||||
handleSchemaBenchmark(b, s, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleObject(b *testing.B, sch *base.Schema, ctx contextBenchmark) {
|
||||
for name, schemaProxy := range sch.Properties.FromOldest() {
|
||||
ctx.stack = append(ctx.stack, loopFrameBenchmark{Type: "object", Restricted: slices.Contains(sch.Required, name)})
|
||||
handleSchemaBenchmark(b, schemaProxy, ctx)
|
||||
}
|
||||
|
||||
if sch.AdditionalProperties != nil && sch.AdditionalProperties.IsA() {
|
||||
handleSchemaBenchmark(b, sch.AdditionalProperties.A, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCircularReferenceBenchmark(b *testing.B, ctx *contextBenchmark, schProxy *base.SchemaProxy) bool {
|
||||
loopRef := getSimplifiedRef(schProxy.GetReference())
|
||||
|
||||
if loopRef != "" {
|
||||
if slices.Contains(ctx.visited, loopRef) {
|
||||
isRestricted := true
|
||||
containsObject := false
|
||||
|
||||
for _, v := range ctx.stack {
|
||||
if v.Type == "object" {
|
||||
containsObject = true
|
||||
}
|
||||
|
||||
if v.Type == "array" && !v.Restricted {
|
||||
isRestricted = false
|
||||
} else if !v.Restricted {
|
||||
isRestricted = false
|
||||
}
|
||||
}
|
||||
|
||||
if !containsObject {
|
||||
isRestricted = true
|
||||
}
|
||||
|
||||
require.False(b, isRestricted, "circular reference: %s", append(ctx.visited, loopRef))
|
||||
return true
|
||||
}
|
||||
|
||||
ctx.visited = append(ctx.visited, loopRef)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getSimplifiedRef will return the reference without the preceding file path
|
||||
// caveat is that if a spec has the same ref in two different files they include this may identify them incorrectly
|
||||
// but currently a problem anyway as libopenapi when returning references from an external file won't include the file path
|
||||
// for a local reference with that file and so we might fail to distinguish between them that way.
|
||||
// The fix needed is for libopenapi to also track which file the reference is in so we can always prefix them with the file path
|
||||
func getSimplifiedRef(ref string) string {
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
refParts := strings.Split(ref, "#/")
|
||||
return "#/" + refParts[len(refParts)-1]
|
||||
}
|
||||
Reference in New Issue
Block a user