Skip to content

Commit 8c1cca1

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

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: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"github.com/getkin/kin-openapi/openapi2"
910
"io"
1011
"io/fs"
1112
"os"
1213
"path"
1314
"path/filepath"
1415
"slices"
16+
"strconv"
1517
"strings"
1618
"time"
1719
"unicode/utf8"
1820

21+
"github.com/getkin/kin-openapi/openapi2conv"
1922
"github.com/getkin/kin-openapi/openapi3"
2023
"github.com/gptscript-ai/gptscript/pkg/assemble"
2124
"github.com/gptscript-ai/gptscript/pkg/builtin"
@@ -24,6 +27,8 @@ import (
2427
"github.com/gptscript-ai/gptscript/pkg/parser"
2528
"github.com/gptscript-ai/gptscript/pkg/system"
2629
"github.com/gptscript-ai/gptscript/pkg/types"
30+
"gopkg.in/yaml.v3"
31+
kyaml "sigs.k8s.io/yaml"
2732
)
2833

2934
const CacheTimeout = time.Hour
@@ -135,9 +140,38 @@ func loadOpenAPI(prg *types.Program, data []byte) *openapi3.T {
135140
prg.OpenAPICache = map[string]any{}
136141
}
137142

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

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

pkg/loader/loader_test.go

Lines changed: 65 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,68 @@ 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+
47+
numOpenAPITools := func(set types.ToolSet) int {
48+
num := 0
49+
for _, v := range set {
50+
if v.IsOpenAPI() {
51+
num++
52+
}
53+
}
54+
return num
55+
}
56+
57+
prgv3 := types.Program{
58+
ToolSet: types.ToolSet{},
59+
}
60+
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
61+
require.NoError(t, err)
62+
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
63+
require.NoError(t, err, "failed to read openapi v3")
64+
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")
65+
66+
prgv2json := types.Program{
67+
ToolSet: types.ToolSet{},
68+
}
69+
datav2, err := os.ReadFile("testdata/openapi_v2.json")
70+
require.NoError(t, err)
71+
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
72+
require.NoError(t, err, "failed to read openapi v2")
73+
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")
74+
75+
prgv2yaml := types.Program{
76+
ToolSet: types.ToolSet{},
77+
}
78+
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
79+
require.NoError(t, err)
80+
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
81+
require.NoError(t, err, "failed to read openapi v2 (yaml)")
82+
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")
83+
84+
require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
85+
}
86+
2287
func TestHelloWorld(t *testing.T) {
2388
prg, err := Program(context.Background(),
2489
"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)