Skip to content

Commit 931e906

Browse files
committed
add: support for OpenAPI v2 by in-memory conversion to v3
1 parent f664d57 commit 931e906

File tree

7 files changed

+521
-4
lines changed

7 files changed

+521
-4
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ require (
2929
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
3030
golang.org/x/sync v0.7.0
3131
golang.org/x/term v0.19.0
32+
gopkg.in/yaml.v3 v3.0.1
33+
sigs.k8s.io/yaml v1.4.0
3234
)
3335

3436
require (
@@ -80,7 +82,6 @@ require (
8082
golang.org/x/sys v0.19.0 // indirect
8183
golang.org/x/text v0.14.0 // indirect
8284
golang.org/x/tools v0.20.0 // indirect
83-
gopkg.in/yaml.v3 v3.0.1 // indirect
8485
gotest.tools/v3 v3.5.1 // indirect
8586
mvdan.cc/gofumpt v0.6.0 // indirect
8687
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,5 @@ mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
488488
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
489489
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
490490
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
491+
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
492+
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

pkg/loader/loader.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import (
1212
"path"
1313
"path/filepath"
1414
"slices"
15+
"strconv"
1516
"strings"
1617
"time"
1718
"unicode/utf8"
1819

20+
"github.com/getkin/kin-openapi/openapi2"
21+
22+
"github.com/getkin/kin-openapi/openapi2conv"
1923
"github.com/getkin/kin-openapi/openapi3"
2024
"github.com/gptscript-ai/gptscript/pkg/assemble"
2125
"github.com/gptscript-ai/gptscript/pkg/builtin"
@@ -24,6 +28,8 @@ import (
2428
"github.com/gptscript-ai/gptscript/pkg/parser"
2529
"github.com/gptscript-ai/gptscript/pkg/system"
2630
"github.com/gptscript-ai/gptscript/pkg/types"
31+
"gopkg.in/yaml.v3"
32+
kyaml "sigs.k8s.io/yaml"
2733
)
2834

2935
const CacheTimeout = time.Hour
@@ -135,9 +141,38 @@ func loadOpenAPI(prg *types.Program, data []byte) *openapi3.T {
135141
prg.OpenAPICache = map[string]any{}
136142
}
137143

138-
openAPIDocument, err = openapi3.NewLoader().LoadFromData(data)
139-
if err != nil || openAPIDocument.Paths.Len() == 0 {
140-
openAPIDocument = nil
144+
if ver, ok := isOpenAPI(data); ok {
145+
switch ver {
146+
case 2:
147+
// Convert OpenAPI v2 to v3
148+
jsondata := data
149+
if !json.Valid(data) {
150+
jsondata, err = kyaml.YAMLToJSON(data)
151+
if err != nil {
152+
return nil
153+
}
154+
}
155+
156+
doc := &openapi2.T{}
157+
if err := doc.UnmarshalJSON(jsondata); err != nil {
158+
return nil
159+
}
160+
161+
openAPIDocument, err = openapi2conv.ToV3(doc)
162+
if err != nil {
163+
return nil
164+
}
165+
case 3:
166+
// Use OpenAPI v3 as is
167+
openAPIDocument, err = openapi3.NewLoader().LoadFromData(data)
168+
if err != nil {
169+
return nil
170+
}
171+
default:
172+
return nil
173+
}
174+
} else {
175+
return nil
141176
}
142177

143178
prg.OpenAPICache[openAPICacheKey] = openAPIDocument
@@ -402,3 +437,42 @@ func SplitToolRef(targetToolName string) (toolName, subTool string) {
402437
return strings.Join(fields[idx+1:], " "),
403438
strings.Join(fields[:idx], " ")
404439
}
440+
441+
// isOpenAPI checks if the data is an OpenAPI definition and returns the version if it is.
442+
func isOpenAPI(data []byte) (int, bool) {
443+
var fragment struct {
444+
Paths map[string]any `json:"paths,omitempty"`
445+
Swagger string `json:"swagger,omitempty"`
446+
OpenAPI string `json:"openapi,omitempty"`
447+
}
448+
449+
if err := json.Unmarshal(data, &fragment); err != nil {
450+
if err := yaml.Unmarshal(data, &fragment); err != nil {
451+
return 0, false
452+
}
453+
}
454+
if len(fragment.Paths) == 0 {
455+
return 0, false
456+
}
457+
458+
if v, _, _ := strings.Cut(fragment.OpenAPI, "."); v != "" {
459+
ver, err := strconv.Atoi(v)
460+
if err != nil {
461+
log.Debugf("invalid OpenAPI version: openapi=%q", fragment.OpenAPI)
462+
return 0, false
463+
}
464+
return ver, true
465+
}
466+
467+
if v, _, _ := strings.Cut(fragment.Swagger, "."); v != "" {
468+
ver, err := strconv.Atoi(v)
469+
if err != nil {
470+
log.Debugf("invalid Swagger version: swagger=%q", fragment.Swagger)
471+
return 0, false
472+
}
473+
return ver, true
474+
}
475+
476+
log.Debugf("no OpenAPI version found in input data: openapi=%q, swagger=%q", fragment.OpenAPI, fragment.Swagger)
477+
return 0, false
478+
}

pkg/loader/loader_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package loader
33
import (
44
"context"
55
"encoding/json"
6+
"os"
67
"strings"
78
"testing"
89

10+
"github.com/gptscript-ai/gptscript/pkg/types"
11+
912
"github.com/gptscript-ai/gptscript/pkg/openai"
1013
"github.com/hexops/autogold/v2"
1114
"github.com/stretchr/testify/require"
@@ -19,6 +22,67 @@ func toString(obj any) string {
1922
return string(s)
2023
}
2124

25+
func TestIsOpenAPI(t *testing.T) {
26+
datav2, err := os.ReadFile("testdata/openapi_v2.yaml")
27+
require.NoError(t, err)
28+
v, ok := isOpenAPI(datav2)
29+
require.True(t, ok)
30+
require.Equal(t, 2, v, "(yaml) expected openapi v2")
31+
32+
datav2, err = os.ReadFile("testdata/openapi_v2.json")
33+
require.NoError(t, err)
34+
v, ok = isOpenAPI(datav2)
35+
require.True(t, ok)
36+
require.Equal(t, 2, v, "(json) expected openapi v2")
37+
38+
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
39+
require.NoError(t, err)
40+
v, ok = isOpenAPI(datav3)
41+
require.True(t, ok)
42+
require.Equal(t, 3, v, "(json) expected openapi v3")
43+
}
44+
45+
func TestLoadOpenAPI(t *testing.T) {
46+
numOpenAPITools := func(set types.ToolSet) int {
47+
num := 0
48+
for _, v := range set {
49+
if v.IsOpenAPI() {
50+
num++
51+
}
52+
}
53+
return num
54+
}
55+
56+
prgv3 := types.Program{
57+
ToolSet: types.ToolSet{},
58+
}
59+
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
60+
require.NoError(t, err)
61+
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
62+
require.NoError(t, err, "failed to read openapi v3")
63+
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")
64+
65+
prgv2json := types.Program{
66+
ToolSet: types.ToolSet{},
67+
}
68+
datav2, err := os.ReadFile("testdata/openapi_v2.json")
69+
require.NoError(t, err)
70+
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
71+
require.NoError(t, err, "failed to read openapi v2")
72+
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")
73+
74+
prgv2yaml := types.Program{
75+
ToolSet: types.ToolSet{},
76+
}
77+
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
78+
require.NoError(t, err)
79+
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
80+
require.NoError(t, err, "failed to read openapi v2 (yaml)")
81+
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")
82+
83+
require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
84+
}
85+
2286
func TestHelloWorld(t *testing.T) {
2387
prg, err := Program(context.Background(),
2488
"https://raw.githubusercontent.com/ibuildthecloud/test/bafe5a62174e8a0ea162277dcfe3a2ddb7eea928/example/sub/tool.gpt",

pkg/loader/testdata/openapi_v2.json

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"version": "1.0.0",
5+
"title": "Swagger Petstore",
6+
"license": {
7+
"name": "MIT"
8+
}
9+
},
10+
"host": "petstore.swagger.io",
11+
"basePath": "/v1",
12+
"schemes": [
13+
"http"
14+
],
15+
"consumes": [
16+
"application/json"
17+
],
18+
"produces": [
19+
"application/json"
20+
],
21+
"paths": {
22+
"/pets": {
23+
"get": {
24+
"summary": "List all pets",
25+
"operationId": "listPets",
26+
"tags": [
27+
"pets"
28+
],
29+
"parameters": [
30+
{
31+
"name": "limit",
32+
"in": "query",
33+
"description": "How many items to return at one time (max 100)",
34+
"required": false,
35+
"type": "integer",
36+
"format": "int32"
37+
}
38+
],
39+
"responses": {
40+
"200": {
41+
"description": "An paged array of pets",
42+
"headers": {
43+
"x-next": {
44+
"type": "string",
45+
"description": "A link to the next page of responses"
46+
}
47+
},
48+
"schema": {
49+
"$ref": "#/definitions/Pets"
50+
}
51+
},
52+
"default": {
53+
"description": "unexpected error",
54+
"schema": {
55+
"$ref": "#/definitions/Error"
56+
}
57+
}
58+
}
59+
},
60+
"post": {
61+
"summary": "Create a pet",
62+
"operationId": "createPets",
63+
"tags": [
64+
"pets"
65+
],
66+
"responses": {
67+
"201": {
68+
"description": "Null response"
69+
},
70+
"default": {
71+
"description": "unexpected error",
72+
"schema": {
73+
"$ref": "#/definitions/Error"
74+
}
75+
}
76+
}
77+
}
78+
},
79+
"/pets/{petId}": {
80+
"get": {
81+
"summary": "Info for a specific pet",
82+
"operationId": "showPetById",
83+
"tags": [
84+
"pets"
85+
],
86+
"parameters": [
87+
{
88+
"name": "petId",
89+
"in": "path",
90+
"required": true,
91+
"description": "The id of the pet to retrieve",
92+
"type": "string"
93+
}
94+
],
95+
"responses": {
96+
"200": {
97+
"description": "Expected response to a valid request",
98+
"schema": {
99+
"$ref": "#/definitions/Pets"
100+
}
101+
},
102+
"default": {
103+
"description": "unexpected error",
104+
"schema": {
105+
"$ref": "#/definitions/Error"
106+
}
107+
}
108+
}
109+
}
110+
}
111+
},
112+
"definitions": {
113+
"Pet": {
114+
"required": [
115+
"id",
116+
"name"
117+
],
118+
"properties": {
119+
"id": {
120+
"type": "integer",
121+
"format": "int64"
122+
},
123+
"name": {
124+
"type": "string"
125+
},
126+
"tag": {
127+
"type": "string"
128+
}
129+
}
130+
},
131+
"Pets": {
132+
"type": "array",
133+
"items": {
134+
"$ref": "#/definitions/Pet"
135+
}
136+
},
137+
"Error": {
138+
"required": [
139+
"code",
140+
"message"
141+
],
142+
"properties": {
143+
"code": {
144+
"type": "integer",
145+
"format": "int32"
146+
},
147+
"message": {
148+
"type": "string"
149+
}
150+
}
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)