diff --git a/go.mod b/go.mod index fd1429ce..2ec4ed3d 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/speakeasy-api/huh v1.1.2 github.com/speakeasy-api/openapi-generation/v2 v2.452.0 github.com/speakeasy-api/openapi-overlay v0.9.0 - github.com/speakeasy-api/sdk-gen-config v1.26.0 + github.com/speakeasy-api/sdk-gen-config v1.27.2 github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.14.11 github.com/speakeasy-api/speakeasy-core v0.16.0 github.com/speakeasy-api/speakeasy-proxy v0.0.2 diff --git a/go.sum b/go.sum index 25c1d958..ac5ff74c 100644 --- a/go.sum +++ b/go.sum @@ -508,8 +508,8 @@ github.com/speakeasy-api/openapi-generation/v2 v2.452.0 h1:NTUddl7zBsAElDcvLdxnl github.com/speakeasy-api/openapi-generation/v2 v2.452.0/go.mod h1:pBDHLrszZpze4DmS/zE2tWqrZw4Dv/mjbuVZ/xULE40= github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= -github.com/speakeasy-api/sdk-gen-config v1.26.0 h1:ry8fvak8+aRo7DNVfV9gISOnjlRb2A3tRxQ5aixRxWQ= -github.com/speakeasy-api/sdk-gen-config v1.26.0/go.mod h1:e9PjnCRHGa4K4EFKVU+kKmihOZjJ2V4utcU+274+bnQ= +github.com/speakeasy-api/sdk-gen-config v1.27.2 h1:UU/yPyoxhN6gGYJ/MPQZtOvCFl/5hnyKsq9P053cXY8= +github.com/speakeasy-api/sdk-gen-config v1.27.2/go.mod h1:e9PjnCRHGa4K4EFKVU+kKmihOZjJ2V4utcU+274+bnQ= github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.14.11 h1:3lVkbqhH5Q8NBXyPH2yt7ilEQ7Mmy3WLdd+A5OB7Km8= github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.14.11/go.mod h1:b4fiZ1Wid0JHwwiYqhaPifDwjmC15uiN7A8Cmid+9kw= github.com/speakeasy-api/speakeasy-core v0.16.0 h1:UpMEUFQ76rQyTFQ9JTv8EsX9V5DEr1ePJbdvw9+nJ68= diff --git a/integration/resources/renameOperationOverlay.yaml b/integration/resources/renameOperationOverlay.yaml new file mode 100644 index 00000000..3cd9bd1a --- /dev/null +++ b/integration/resources/renameOperationOverlay.yaml @@ -0,0 +1,14 @@ +overlay: 1.0.0 +info: + title: Change operationId for findByTags + version: 0.0.0 +actions: + - target: $["paths"]["/pet/findByTags"]["get"]["operationId"] + update: findByTagsNew + - target: $["paths"]["/pet/findByTags"]["get"] + update: + x-codeSamples: + - lang: go + label: updatePet + source: |- + package main \ No newline at end of file diff --git a/integration/workflow_test.go b/integration/workflow_test.go index ecc5ccdd..59df3cd6 100644 --- a/integration/workflow_test.go +++ b/integration/workflow_test.go @@ -233,6 +233,7 @@ func execute(t *testing.T, wd string, args ...string) Runnable { execCmd := exec.Command("go", append([]string{"run", mainGo}, args...)...) execCmd.Env = os.Environ() execCmd.Dir = wd + // store stdout and stderr in a buffer and output it all in one go if there's a failure out := bytes.Buffer{} execCmd.Stdout = &out @@ -277,11 +278,13 @@ func (c *cmdRunner) Run() error { func TestSpecWorkflows(t *testing.T) { tests := []struct { - name string - inputDocs []string - overlays []string - out string - expectedPaths []string + name string + inputDocs []string + overlays []string + transformations []workflow.Transformation + out string + expectedPaths []string + unexpectedPaths []string }{ { name: "overlay with local document", @@ -323,6 +326,80 @@ func TestSpecWorkflows(t *testing.T) { }, out: "output.yaml", }, + { + name: "test simple transformation", + inputDocs: []string{"spec.yaml"}, + transformations: []workflow.Transformation{ + { + FilterOperations: &workflow.FilterOperationsOptions{ + Operations: "findPetsByTags", + }, + }, + }, + out: "output.yaml", + expectedPaths: []string{ + "/pet/findByTags", + }, + unexpectedPaths: []string{ + "/pet/findByStatus", + }, + }, + { + name: "test merge with transformation", + inputDocs: []string{"part1.yaml", "part2.yaml"}, + transformations: []workflow.Transformation{ + { + FilterOperations: &workflow.FilterOperationsOptions{ + Operations: "getInventory", + }, + }, + }, + out: "output.yaml", + expectedPaths: []string{ + "/store/inventory", + }, + unexpectedPaths: []string{ + "/store/order", + }, + }, + { + name: "test overlay with transformation", + inputDocs: []string{"spec.yaml"}, + overlays: []string{"renameOperationOverlay.yaml"}, + transformations: []workflow.Transformation{ + { + FilterOperations: &workflow.FilterOperationsOptions{ + Operations: "findByTagsNew", + }, + }, + }, + out: "output.yaml", + expectedPaths: []string{ + "/pet/findByTags", + }, + unexpectedPaths: []string{ + "/pet/findByStatus", + }, + }, + { + name: "test merge, overlay, and transformation", + inputDocs: []string{"part1.yaml", "part2.yaml"}, + overlays: []string{"renameOperationOverlay.yaml"}, + transformations: []workflow.Transformation{ + { + FilterOperations: &workflow.FilterOperationsOptions{ + Operations: "findByTagsNew", + }, + }, + }, + out: "output.yaml", + expectedPaths: []string{ + "/pet/findByTags", + }, + unexpectedPaths: []string{ + "/pet/findByStatus", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -340,6 +417,7 @@ func TestSpecWorkflows(t *testing.T) { if isLocalFileReference(inputDoc) { err := copyFile(fmt.Sprintf("resources/%s", inputDoc), fmt.Sprintf("%s/%s", temp, inputDoc)) require.NoError(t, err) + inputDoc = filepath.Join(temp, inputDoc) } inputs = append(inputs, workflow.Document{ Location: workflow.LocationString(inputDoc), @@ -350,6 +428,7 @@ func TestSpecWorkflows(t *testing.T) { if isLocalFileReference(overlay) { err := copyFile(fmt.Sprintf("resources/%s", overlay), fmt.Sprintf("%s/%s", temp, overlay)) require.NoError(t, err) + overlay = filepath.Join(temp, overlay) } overlays = append(overlays, workflow.Overlay{ Document: &workflow.Document{ @@ -357,10 +436,13 @@ func TestSpecWorkflows(t *testing.T) { }, }) } + + outputFull := filepath.Join(temp, tt.out) workflowFile.Sources["first-source"] = workflow.Source{ - Inputs: inputs, - Overlays: overlays, - Output: &tt.out, + Inputs: inputs, + Overlays: overlays, + Transformations: tt.transformations, + Output: &outputFull, } err := os.MkdirAll(filepath.Join(temp, ".speakeasy"), 0o755) @@ -371,11 +453,12 @@ func TestSpecWorkflows(t *testing.T) { err = workflow.Save(temp, workflowFile) require.NoError(t, err) args := []string{"run", "-s", "all", "--pinned", "--skip-compile"} + cmdErr := execute(t, temp, args...).Run() require.NoError(t, cmdErr) content, err := os.ReadFile(filepath.Join(temp, tt.out)) - require.NoError(t, err, "No readable file %s exists", tt.out) + require.NoError(t, err, "No readable file %s exists", filepath.Join(temp, tt.out)) if len(tt.overlays) > 0 { if !strings.Contains(string(content), "x-codeSamples") { @@ -390,6 +473,14 @@ func TestSpecWorkflows(t *testing.T) { } } } + + if len(tt.unexpectedPaths) > 0 { + for _, path := range tt.unexpectedPaths { + if strings.Contains(string(content), path) { + t.Errorf("Unexpected path %s found in output document", path) + } + } + } }) } } diff --git a/internal/run/frozenSource.go b/internal/run/frozenSource.go index 20005f24..25c1f166 100644 --- a/internal/run/frozenSource.go +++ b/internal/run/frozenSource.go @@ -24,7 +24,7 @@ func NewFrozenSource(w *Workflow, parentStep *workflowTracking.WorkflowStep, sou } } -func (f FrozenSource) Do(ctx context.Context, _, _ string) (string, error) { +func (f FrozenSource) Do(ctx context.Context, _ string) (string, error) { mergeStep := f.parentStep.NewSubstep("Download OAS from lockfile") // Check lockfile exists, produce an error if not diff --git a/internal/run/merge.go b/internal/run/merge.go index a1d9b5a4..bc4a072e 100644 --- a/internal/run/merge.go +++ b/internal/run/merge.go @@ -30,13 +30,10 @@ func NewMerge(w *Workflow, parentStep *workflowTracking.WorkflowStep, source wor } } -func (m Merge) Do(ctx context.Context, _, outputLocation string) (string, error) { +func (m Merge) Do(ctx context.Context, _ string) (string, error) { mergeStep := m.parentStep.NewSubstep("Merge Documents") mergeLocation := m.source.GetTempMergeLocation() - if len(m.source.Overlays) == 0 { - mergeLocation = outputLocation - } log.From(ctx).Infof("Merging %d schemas into %s...", len(m.source.Inputs), mergeLocation) diff --git a/internal/run/minimumViableSpec.go b/internal/run/minimumViableSpec.go index 29b7928f..dc8a77b5 100644 --- a/internal/run/minimumViableSpec.go +++ b/internal/run/minimumViableSpec.go @@ -3,89 +3,39 @@ package run import ( "context" "fmt" + "github.com/AlekSi/pointer" "github.com/speakeasy-api/openapi-generation/v2/pkg/errors" "github.com/speakeasy-api/sdk-gen-config/workflow" - "github.com/speakeasy-api/speakeasy-core/openapi" "github.com/speakeasy-api/speakeasy/internal/charm/styles" - "github.com/speakeasy-api/speakeasy/internal/download" "github.com/speakeasy-api/speakeasy/internal/studio/modifications" - "github.com/speakeasy-api/speakeasy/internal/transform" "github.com/speakeasy-api/speakeasy/internal/workflowTracking" - "os" - "path/filepath" + "strings" ) func (w *Workflow) retryWithMinimumViableSpec(ctx context.Context, parentStep *workflowTracking.WorkflowStep, sourceID, targetID string, vErrs []error) (string, *SourceResult, error) { - invalidOperationToErr := make(map[string]error) + var invalidOperations []string for _, err := range vErrs { vErr := errors.GetValidationErr(err) - for _, op := range vErr.AffectedOperationIDs { - invalidOperationToErr[op] = err // TODO: support multiple errors per operation? + if vErr.Severity == errors.SeverityError { + for _, op := range vErr.AffectedOperationIDs { + invalidOperations = append(invalidOperations, op) + } } } substep := parentStep.NewSubstep("Retrying with minimum viable document") source := w.workflow.Sources[sourceID] - baseLocation := source.Inputs[0].Location.Resolve() - workingDir := workflow.GetTempDir() - - // This is intended to only be used from quickstart, we must assume a singular input document - if len(source.Inputs)+len(source.Overlays) > 1 { - return "", nil, fmt.Errorf("multiple inputs are not supported for minimum viable spec") - } - - tempBase := fmt.Sprintf("downloaded_%s%s", randStringBytes(10), filepath.Ext(baseLocation)) - - if source.Inputs[0].IsRemote() { - outResolved, err := download.ResolveRemoteDocument(ctx, source.Inputs[0], tempBase) - if err != nil { - return "", nil, fmt.Errorf("failed to download remote document: %w", err) - } - - baseLocation = outResolved - } - - overlayOut := filepath.Join(workingDir, fmt.Sprintf("mvs_overlay_%s.yaml", randStringBytes(10))) - if err := os.MkdirAll(workingDir, os.ModePerm); err != nil { - return "", nil, err - } - overlayFile, err := os.Create(overlayOut) - if err != nil { - return "", nil, fmt.Errorf("failed to create overlay file: %w", err) - } - defer overlayFile.Close() - - failedRetry := false - defer func() { - os.Remove(overlayOut) - os.Remove(filepath.Join(workingDir, tempBase)) - if failedRetry { - source.Overlays = []workflow.Overlay{} - w.workflow.Sources[sourceID] = source - } - }() - - _, _, model, err := openapi.LoadDocument(ctx, source.Inputs[0].Location.Resolve()) - if err != nil { - return "", nil, fmt.Errorf("failed to load document: %w", err) - } - - overlay := transform.BuildRemoveInvalidOperationsOverlay(model, invalidOperationToErr) - - overlayPath, err := modifications.GetOverlayPath(workingDir) - if err != nil { - return "", nil, fmt.Errorf("failed to get overlay path: %w", err) - } - - if _, err = modifications.UpsertOverlay(overlayPath, &source, overlay); err != nil { - return "", nil, fmt.Errorf("failed to upsert overlay: %w", err) - } + source.Transformations = append(source.Transformations, workflow.Transformation{ + FilterOperations: &workflow.FilterOperationsOptions{ + Operations: strings.Join(invalidOperations, ","), + Exclude: pointer.ToBool(true), + }, + }) w.workflow.Sources[sourceID] = source sourcePath, sourceRes, err := w.RunSource(ctx, substep, sourceID, targetID) if err != nil { - failedRetry = true return "", nil, fmt.Errorf("failed to re-run source: %w", err) } diff --git a/internal/run/overlay.go b/internal/run/overlay.go index b18ab162..8748cd84 100644 --- a/internal/run/overlay.go +++ b/internal/run/overlay.go @@ -17,29 +17,23 @@ import ( ) type Overlay struct { - workflow *Workflow - parentStep *workflowTracking.WorkflowStep - source workflow.Source - outputLocation string - ruleset string + parentStep *workflowTracking.WorkflowStep + source workflow.Source } var _ SourceStep = Overlay{} -func NewOverlay(w *Workflow, parentStep *workflowTracking.WorkflowStep, source workflow.Source, outputLocation, ruleset string) Overlay { +func NewOverlay(parentStep *workflowTracking.WorkflowStep, source workflow.Source) Overlay { return Overlay{ - workflow: w, - parentStep: parentStep, - source: source, - outputLocation: outputLocation, - ruleset: ruleset, + parentStep: parentStep, + source: source, } } -func (o Overlay) Do(ctx context.Context, inputPath, outputPath string) (string, error) { +func (o Overlay) Do(ctx context.Context, inputPath string) (string, error) { overlayStep := o.parentStep.NewSubstep("Applying Overlays") - overlayLocation := outputPath + overlayLocation := o.source.GetTempOverlayLocation() log.From(ctx).Infof("Applying %d overlays into %s...", len(o.source.Overlays), overlayLocation) diff --git a/internal/run/source.go b/internal/run/source.go index 4d73732b..bc7cbbb8 100644 --- a/internal/run/source.go +++ b/internal/run/source.go @@ -44,7 +44,7 @@ type LintingError struct { } type SourceStep interface { - Do(ctx context.Context, inputPath, outputPath string) (string, error) + Do(ctx context.Context, inputPath string) (string, error) } func (e *LintingError) Error() string { @@ -83,21 +83,21 @@ func (w *Workflow) RunSource(ctx context.Context, parentStep *workflowTracking.W var currentDocument string if w.FrozenWorkflowLock { - currentDocument, err = NewFrozenSource(w, rootStep, sourceID).Do(ctx, "unused", "unused") + currentDocument, err = NewFrozenSource(w, rootStep, sourceID).Do(ctx, "unused") if err != nil { return "", nil, err } } else if len(source.Inputs) == 1 { var singleLocation *string // The output location should be the resolved location - if len(source.Overlays) == 0 { + if source.IsSingleInput() { singleLocation = &outputLocation } currentDocument, err = schemas.ResolveDocument(ctx, source.Inputs[0], singleLocation, rootStep) if err != nil { return "", nil, err } - if len(source.Overlays) == 0 { + if source.IsSingleInput() { // In registry bundles specifically we cannot know the exact file output location before pulling the bundle down if source.Inputs[0].IsSpeakeasyRegistry() { outputLocation = currentDocument @@ -110,7 +110,7 @@ func (w *Workflow) RunSource(ctx context.Context, parentStep *workflowTracking.W } } } else { - currentDocument, err = NewMerge(w, rootStep, source, rulesetToUse).Do(ctx, currentDocument, outputLocation) + currentDocument, err = NewMerge(w, rootStep, source, rulesetToUse).Do(ctx, currentDocument) if err != nil { return "", nil, err } @@ -122,12 +122,23 @@ func (w *Workflow) RunSource(ctx context.Context, parentStep *workflowTracking.W } if len(source.Overlays) > 0 && !w.FrozenWorkflowLock { - currentDocument, err = NewOverlay(w, rootStep, source, outputLocation, rulesetToUse).Do(ctx, currentDocument, outputLocation) + currentDocument, err = NewOverlay(rootStep, source).Do(ctx, currentDocument) if err != nil { return "", nil, err } } + if len(source.Transformations) > 0 && !w.FrozenWorkflowLock { + currentDocument, err = NewTransform(rootStep, source).Do(ctx, currentDocument) + if err != nil { + return "", nil, err + } + } + + if err := os.Rename(currentDocument, outputLocation); err != nil { + return "", nil, fmt.Errorf("failed to rename %s to %s: %w", currentDocument, outputLocation, err) + } + currentDocument = outputLocation sourceRes.OutputPath = currentDocument if !w.SkipLinting { diff --git a/internal/run/transform.go b/internal/run/transform.go new file mode 100644 index 00000000..534a7bb9 --- /dev/null +++ b/internal/run/transform.go @@ -0,0 +1,100 @@ +package run + +import ( + "bytes" + "context" + "fmt" + "github.com/speakeasy-api/sdk-gen-config/workflow" + "github.com/speakeasy-api/speakeasy/internal/log" + "github.com/speakeasy-api/speakeasy/internal/transform" + "github.com/speakeasy-api/speakeasy/internal/utils" + "github.com/speakeasy-api/speakeasy/internal/workflowTracking" + "io" + "os" + "path/filepath" +) + +type Transform struct { + parentStep *workflowTracking.WorkflowStep + source workflow.Source +} + +var _ SourceStep = Transform{} + +func NewTransform(parentStep *workflowTracking.WorkflowStep, source workflow.Source) Transform { + return Transform{ + parentStep: parentStep, + source: source, + } +} + +func (t Transform) Do(ctx context.Context, inputPath string) (string, error) { + transformStep := t.parentStep.NewSubstep("Applying Transformations") + + outputPath := t.source.GetTempTransformLocation() + + log.From(ctx).Infof("Applying %d transformations and writing to %s...", len(t.source.Transformations), outputPath) + + if err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm); err != nil { + return "", err + } + + yamlOut := utils.HasYAMLExt(outputPath) + + var in io.Reader + in, err := os.Open(inputPath) + if err != nil { + return "", err + } + + var out bytes.Buffer + for _, transformation := range t.source.Transformations { + out = bytes.Buffer{} + + if transformation.Cleanup != nil { + transformStep.NewSubstep("Cleaning up document") + + if err := transform.CleanupFromReader(ctx, in, inputPath, &out, yamlOut); err != nil { + return "", err + } + } else if transformation.RemoveUnused != nil { + transformStep.NewSubstep("Removing unused nodes") + + if err := transform.RemoveUnusedFromReader(ctx, in, inputPath, &out, yamlOut); err != nil { + return "", err + } + } else if transformation.FilterOperations != nil { + operations := transformation.FilterOperations.ParseOperations() + include := true + if transformation.FilterOperations.Include != nil { + include = *transformation.FilterOperations.Include + } else if transformation.FilterOperations.Exclude != nil { + include = !*transformation.FilterOperations.Exclude + } + + inOutString := "down to" + if !include { + inOutString = "out" + } + transformStep.NewSubstep(fmt.Sprintf("Filtering %s %d operations", inOutString, len(operations))) + + if err := transform.FilterOperationsFromReader(ctx, in, inputPath, operations, include, &out, yamlOut); err != nil { + return "", err + } + } + + in = &out + } + + outFile, err := os.Create(outputPath) + defer outFile.Close() + if err != nil { + return "", err + } + if _, err := io.Copy(outFile, &out); err != nil { + return "", err + } + + transformStep.Succeed() + return outputPath, nil +} diff --git a/internal/transform/cleanup.go b/internal/transform/cleanup.go index d78a982d..a5261783 100644 --- a/internal/transform/cleanup.go +++ b/internal/transform/cleanup.go @@ -22,6 +22,16 @@ func CleanupDocument(ctx context.Context, schemaPath string, yamlOut bool, w io. }.Do(ctx) } +func CleanupFromReader(ctx context.Context, schema io.Reader, schemaPath string, w io.Writer, yamlOut bool) error { + return transformer[interface{}]{ + r: schema, + schemaPath: schemaPath, + transformFn: Cleanup, + w: w, + jsonOut: !yamlOut, + }.Do(ctx) +} + func Cleanup(ctx context.Context, doc libopenapi.Document, model *libopenapi.DocumentModel[v3.Document], _ interface{}) (libopenapi.Document, *libopenapi.DocumentModel[v3.Document], error) { pathItems := model.Model.Paths.PathItems var pathsToDelete []string diff --git a/internal/transform/filterOperations.go b/internal/transform/filterOperations.go index af00c8a3..d4f087e2 100644 --- a/internal/transform/filterOperations.go +++ b/internal/transform/filterOperations.go @@ -30,6 +30,21 @@ func FilterOperations(ctx context.Context, schemaPath string, includeOps []strin }.Do(ctx) } +func FilterOperationsFromReader(ctx context.Context, schema io.Reader, schemaPath string, includeOps []string, include bool, w io.Writer, yamlOut bool) error { + return transformer[args]{ + r: schema, + schemaPath: schemaPath, + transformFn: filterOperations, + w: w, + jsonOut: !yamlOut, + args: args{ + includeOps: includeOps, + include: include, + schemaPath: schemaPath, + }, + }.Do(ctx) +} + type args struct { includeOps []string include bool diff --git a/internal/transform/removeUnused.go b/internal/transform/removeUnused.go index d5dbd3b5..fe09b0db 100644 --- a/internal/transform/removeUnused.go +++ b/internal/transform/removeUnused.go @@ -21,6 +21,16 @@ func RemoveUnused(ctx context.Context, schemaPath string, yamlOut bool, w io.Wri }.Do(ctx) } +func RemoveUnusedFromReader(ctx context.Context, schema io.Reader, schemaPath string, w io.Writer, yamlOut bool) error { + return transformer[interface{}]{ + r: schema, + schemaPath: schemaPath, + transformFn: RemoveOrphans, + w: w, + jsonOut: !yamlOut, + }.Do(ctx) +} + func RemoveOrphans(ctx context.Context, doc libopenapi.Document, _ *libopenapi.DocumentModel[v3.Document], _ interface{}) (libopenapi.Document, *libopenapi.DocumentModel[v3.Document], error) { logger := log.From(ctx) diff --git a/internal/transform/transformer.go b/internal/transform/transformer.go index 92351488..5215755e 100644 --- a/internal/transform/transformer.go +++ b/internal/transform/transformer.go @@ -7,9 +7,11 @@ import ( "github.com/speakeasy-api/speakeasy-core/openapi" "github.com/speakeasy-api/speakeasy/internal/schemas" "io" + "os" ) type transformer[Args interface{}] struct { + r io.Reader schemaPath string jsonOut bool transformFn func(ctx context.Context, doc libopenapi.Document, model *libopenapi.DocumentModel[v3.Document], args Args) (libopenapi.Document, *libopenapi.DocumentModel[v3.Document], error) @@ -18,7 +20,19 @@ type transformer[Args interface{}] struct { } func (t transformer[Args]) Do(ctx context.Context) error { - _, doc, model, err := openapi.LoadDocument(ctx, t.schemaPath) + if t.r == nil { + var err error + t.r, err = os.Open(t.schemaPath) + if err != nil { + return err + } + } + + schemaBytes, err := io.ReadAll(t.r) + if err != nil { + return err + } + doc, model, err := openapi.Load(schemaBytes, t.schemaPath) if err != nil { return err }