WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions integration/swagger_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package integration_tests

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/speakeasy-api/sdk-gen-config/workflow"
"github.com/stretchr/testify/require"
)

func TestAutomaticSwaggerConversion(t *testing.T) {
t.Parallel()
temp := setupTestDir(t)

// Create workflow file that uses the Swagger 2.0 document
workflowFile := &workflow.Workflow{
Version: workflow.WorkflowVersion,
Sources: make(map[string]workflow.Source),
Targets: make(map[string]workflow.Target),
}

// Copy the Swagger 2.0 test file
swaggerPath := "swagger.yaml"
err := copyFile("resources/swagger.yaml", filepath.Join(temp, swaggerPath))
require.NoError(t, err)

outputPath := filepath.Join(temp, "output.yaml")
workflowFile.Sources["swagger-source"] = workflow.Source{
Inputs: []workflow.Document{
{
Location: workflow.LocationString(swaggerPath),
},
},
Output: &outputPath,
}

err = os.MkdirAll(filepath.Join(temp, ".speakeasy"), 0o755)
require.NoError(t, err)
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)

// Read the output file and verify it was converted to OpenAPI 3.0
content, err := os.ReadFile(outputPath)
require.NoError(t, err, "No readable file %s exists", outputPath)

contentStr := string(content)

// Verify it's OpenAPI 3.0, not Swagger 2.0
require.Contains(t, contentStr, "openapi: 3.", "Output should be OpenAPI 3.x")
require.NotContains(t, contentStr, "swagger: \"2.0\"", "Output should not contain Swagger 2.0 declaration")
require.NotContains(t, contentStr, "swagger: '2.0'", "Output should not contain Swagger 2.0 declaration")

// Verify some paths from the original Swagger doc are preserved
require.True(t, strings.Contains(contentStr, "/pet") ||
strings.Contains(contentStr, "\"/pet\""), "Should contain /pet path")
require.True(t, strings.Contains(contentStr, "/store/inventory") ||
strings.Contains(contentStr, "\"/store/inventory\""), "Should contain /store/inventory path")
}
21 changes: 18 additions & 3 deletions integration/workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/speakeasy-api/speakeasy/internal/utils"
"os"
"os/exec"
"path/filepath"
Expand All @@ -14,6 +13,8 @@ import (
"sync"
"testing"

"github.com/speakeasy-api/speakeasy/internal/utils"

"github.com/speakeasy-api/speakeasy/cmd"
"github.com/speakeasy-api/versioning-reports/versioning"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -308,8 +309,10 @@ func execute(t *testing.T, wd string, args ...string) Runnable {
// executeI is a helper function to execute the main.go file inline. It can help when debugging integration tests
// We should not use it on multiple tests at once as they will share memory: this can create issues.
// so we leave it around as a little helper method: swap out execute for executeI and debug breakpoints work
var mutex sync.Mutex
var rootCmd = cmd.CmdForTest(version, artifactArch)
var (
mutex sync.Mutex
rootCmd = cmd.CmdForTest(version, artifactArch)
)

func executeI(t *testing.T, wd string, args ...string) Runnable {
mutex.Lock()
Expand Down Expand Up @@ -479,6 +482,18 @@ func TestSpecWorkflows(t *testing.T) {
"/pet/findByTags",
},
},
{
name: "test automatic swagger 2.0 conversion",
inputDocs: []string{
"swagger.yaml",
},
out: "output.yaml",
expectedPaths: []string{
"/pet",
"/store/inventory",
"/user/login",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
53 changes: 53 additions & 0 deletions internal/run/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/speakeasy-api/speakeasy/internal/utils"
"github.com/speakeasy-api/speakeasy/internal/validation"
"github.com/speakeasy-api/speakeasy/internal/workflowTracking"
"github.com/speakeasy-api/speakeasy/pkg/transform"
)

type SourceResultCallback func(sourceRes *SourceResult, sourceStep SourceStepID) error
Expand Down Expand Up @@ -161,6 +162,16 @@ func (w *Workflow) RunSource(ctx context.Context, parentStep *workflowTracking.W
currentDocument = sourceRes.OverlayResult.Location
}

// Automatically convert Swagger 2.0 documents to OpenAPI 3.0
// Note: This is handled here rather than as a transformation type in source.Transformations
// as we don't want to expose this as a controllable transformation in a workflow file
if !frozenSource {
currentDocument, err = maybeConvertSwagger(ctx, rootStep, currentDocument, logger)
if err != nil {
return "", nil, err
}
}

if len(source.Transformations) > 0 && !frozenSource {
w.OnSourceResult(sourceRes, SourceStepTransform)
currentDocument, err = NewTransform(rootStep, source).Do(ctx, currentDocument)
Expand Down Expand Up @@ -324,6 +335,10 @@ func getTempApplyPath(path string) string {
return filepath.Join(workflow.GetTempDir(), fmt.Sprintf("applied_%s%s", randStringBytes(10), filepath.Ext(path)))
}

func getTempConvertedPath(path string) string {
return filepath.Join(workflow.GetTempDir(), fmt.Sprintf("converted_%s%s", randStringBytes(10), filepath.Ext(path)))
}

// Returns true if any of the source inputs are remote.
func workflowSourceHasRemoteInputs(source workflow.Source) bool {
for _, input := range source.Inputs {
Expand Down Expand Up @@ -409,3 +424,41 @@ func maybeReformatDocument(ctx context.Context, documentPath string, rootStep *w

return documentPath, false, nil
}

// maybeConvertSwagger checks if a document is Swagger 2.0 and automatically converts it to OpenAPI 3.0
func maybeConvertSwagger(ctx context.Context, rootStep *workflowTracking.WorkflowStep, documentPath string, logger log.Logger) (string, error) {
isSwagger, err := schemas.IsSwaggerDocument(ctx, documentPath)
if err != nil {
logger.Warnf("failed to check if document is Swagger: %s", err.Error())
return documentPath, nil
}

if !isSwagger {
return documentPath, nil
}

convertStep := rootStep.NewSubstep("Converting Swagger 2.0 to OpenAPI 3.0")
logger.Infof("Detected Swagger 2.0 document, automatically converting to OpenAPI 3.0...")

convertedPath := getTempConvertedPath(documentPath)
if err := os.MkdirAll(filepath.Dir(convertedPath), os.ModePerm); err != nil {
convertStep.Fail()
return "", fmt.Errorf("failed to create temp directory: %w", err)
}

convertedFile, err := os.Create(convertedPath)
if err != nil {
convertStep.Fail()
return "", fmt.Errorf("failed to create converted file: %w", err)
}
defer convertedFile.Close()

yamlOut := utils.HasYAMLExt(documentPath)
if err := transform.ConvertSwagger(ctx, documentPath, yamlOut, convertedFile); err != nil {
convertStep.Fail()
return "", fmt.Errorf("failed to convert Swagger to OpenAPI: %w", err)
}

convertStep.Succeed()
return convertedPath, nil
}
36 changes: 36 additions & 0 deletions internal/schemas/swagger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package schemas

import (
"context"
"encoding/json"
"fmt"
"os"

"gopkg.in/yaml.v3"
)

// IsSwaggerDocument checks if a document is a Swagger 2.0 specification
func IsSwaggerDocument(ctx context.Context, schemaPath string) (bool, error) {
data, err := os.ReadFile(schemaPath)
if err != nil {
return false, fmt.Errorf("failed to read document: %w", err)
}

// Try to parse as YAML first (works for both YAML and JSON)
var doc map[string]interface{}
if err := yaml.Unmarshal(data, &doc); err != nil {
// If YAML parsing fails, try JSON
if err := json.Unmarshal(data, &doc); err != nil {
return false, fmt.Errorf("failed to parse document: %w", err)
}
}

// Check for the "swagger" field which indicates Swagger 2.0
if swagger, ok := doc["swagger"]; ok {
if swaggerStr, ok := swagger.(string); ok && swaggerStr == "2.0" {
return true, nil
}
}

return false, nil
}
48 changes: 48 additions & 0 deletions internal/schemas/swagger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package schemas

import (
"context"
"testing"
)

func TestIsSwaggerDocument(t *testing.T) {
tests := []struct {
name string
path string
expected bool
wantErr bool
}{
{
name: "Swagger 2.0 YAML file",
path: "../../integration/resources/swagger.yaml",
expected: true,
wantErr: false,
},
{
name: "OpenAPI 3.0 YAML file",
path: "../../integration/resources/converted.yaml",
expected: false,
wantErr: false,
},
{
name: "Non-existent file",
path: "../../integration/resources/nonexistent.yaml",
expected: false,
wantErr: true,
},
}

ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := IsSwaggerDocument(ctx, tt.path)
if (err != nil) != tt.wantErr {
t.Errorf("IsSwaggerDocument() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.expected {
t.Errorf("IsSwaggerDocument() = %v, expected %v", got, tt.expected)
}
})
}
}
12 changes: 11 additions & 1 deletion pkg/transform/convertSwagger.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ import (

// ConvertSwagger upgrades a Swagger 2.0 document to OpenAPI 3.0 using the speakeasy-api/openapi library
func ConvertSwagger(ctx context.Context, schemaPath string, yamlOut bool, w io.Writer) error {
// Read the Swagger 2.0 document
data, err := os.ReadFile(schemaPath)
if err != nil {
return fmt.Errorf("failed to read swagger document: %w", err)
}

return ConvertSwaggerFromReader(ctx, bytes.NewReader(data), schemaPath, w, yamlOut)
}

// ConvertSwaggerFromReader upgrades a Swagger 2.0 document to OpenAPI 3.0 from an io.Reader
func ConvertSwaggerFromReader(ctx context.Context, r io.Reader, schemaPath string, w io.Writer, yamlOut bool) error {
// Read all data from the reader
data, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("failed to read swagger document: %w", err)
}

// Unmarshal using swagger.Unmarshal (returns swagger doc, validation errors, and error)
swaggerDoc, validationErrs, err := swagger.Unmarshal(ctx, bytes.NewReader(data))
if err != nil {
Expand Down
Loading