Skip to content

Commit da00a17

Browse files
committed
add: support for OpenAPI v2 by in-memory conversion to v3
1 parent 4a35d81 commit da00a17

File tree

7 files changed

+522
-4
lines changed

7 files changed

+522
-4
lines changed

go.mod

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

3537
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
@@ -142,9 +148,38 @@ func loadOpenAPI(prg *types.Program, data []byte) *openapi3.T {
142148
prg.OpenAPICache = map[string]any{}
143149
}
144150

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

150185
prg.OpenAPICache[openAPICacheKey] = openAPIDocument
@@ -399,3 +434,42 @@ func input(ctx context.Context, cache *cache.Client, base *source, name string)
399434

400435
return nil, fmt.Errorf("can not load tools path=%s name=%s", base.Path, name)
401436
}
437+
438+
// isOpenAPI checks if the data is an OpenAPI definition and returns the version if it is.
439+
func isOpenAPI(data []byte) (int, bool) {
440+
var fragment struct {
441+
Paths map[string]any `json:"paths,omitempty"`
442+
Swagger string `json:"swagger,omitempty"`
443+
OpenAPI string `json:"openapi,omitempty"`
444+
}
445+
446+
if err := json.Unmarshal(data, &fragment); err != nil {
447+
if err := yaml.Unmarshal(data, &fragment); err != nil {
448+
return 0, false
449+
}
450+
}
451+
if len(fragment.Paths) == 0 {
452+
return 0, false
453+
}
454+
455+
if v, _, _ := strings.Cut(fragment.OpenAPI, "."); v != "" {
456+
ver, err := strconv.Atoi(v)
457+
if err != nil {
458+
log.Debugf("invalid OpenAPI version: openapi=%q", fragment.OpenAPI)
459+
return 0, false
460+
}
461+
return ver, true
462+
}
463+
464+
if v, _, _ := strings.Cut(fragment.Swagger, "."); v != "" {
465+
ver, err := strconv.Atoi(v)
466+
if err != nil {
467+
log.Debugf("invalid Swagger version: swagger=%q", fragment.Swagger)
468+
return 0, false
469+
}
470+
return ver, true
471+
}
472+
473+
log.Debugf("no OpenAPI version found in input data: openapi=%q, swagger=%q", fragment.OpenAPI, fragment.Swagger)
474+
return 0, false
475+
}

pkg/loader/loader_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"encoding/json"
66
"testing"
77

8+
"os"
9+
10+
"github.com/gptscript-ai/gptscript/pkg/types"
11+
812
"github.com/hexops/autogold/v2"
913
"github.com/stretchr/testify/require"
1014
)
@@ -17,6 +21,67 @@ func toString(obj any) string {
1721
return string(s)
1822
}
1923

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