Skip to content

Commit 930d7c5

Browse files
authored
feat(openapi): allow external $refs (#26)
1 parent 399994c commit 930d7c5

File tree

8 files changed

+169
-15
lines changed

8 files changed

+169
-15
lines changed

external/manager.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ package external
22

33
import (
44
"fmt"
5+
"os"
6+
"os/exec"
7+
"path"
8+
"path/filepath"
9+
"slices"
10+
"strings"
11+
512
"github.com/hashicorp/go-hclog"
613
goplugin "github.com/hashicorp/go-plugin"
714
"github.com/imposter-project/imposter-go/external/shared"
815
"github.com/imposter-project/imposter-go/internal/config"
916
"github.com/imposter-project/imposter-go/internal/version"
1017
"github.com/imposter-project/imposter-go/pkg/logger"
1118
"gopkg.in/yaml.v3"
12-
"os"
13-
"os/exec"
14-
"path"
15-
"path/filepath"
16-
"slices"
17-
"strings"
1819
)
1920

2021
var pluginMap map[string]goplugin.Plugin

internal/config/multi_document_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ resources:
3636
name: "multiple documents with OpenAPI and SwaggerUI",
3737
yamlContent: `plugin: openapi
3838
specFile: api.json
39+
config:
40+
externalBaseURL: "http://example.com"
3941
resources:
4042
- path: /pets
4143
method: GET
@@ -58,6 +60,16 @@ config:
5860
if configs[0].SpecFile != "api.json" {
5961
t.Errorf("Expected specFile 'api.json', got '%s'", configs[0].SpecFile)
6062
}
63+
64+
// Verify plugin config content can be unmarshalled
65+
var plugin0Config map[string]interface{}
66+
if err := configs[0].PluginConfig.Decode(&plugin0Config); err != nil {
67+
t.Errorf("Failed to unmarshal plugin config: %v", err)
68+
}
69+
70+
if plugin0Config["externalBaseURL"] != "http://example.com" {
71+
t.Errorf("Expected externalBaseURL 'http://example.com', got '%v'", plugin0Config["externalBaseURL"])
72+
}
6173
if len(configs[0].Resources) != 1 {
6274
t.Errorf("Expected 1 resource in first config, got %d", len(configs[0].Resources))
6375
}
@@ -73,13 +85,13 @@ config:
7385
}
7486

7587
// Verify plugin config content can be unmarshaled
76-
var pluginConfig map[string]interface{}
77-
if err := configs[1].PluginConfig.Decode(&pluginConfig); err != nil {
88+
var plugin1Config map[string]interface{}
89+
if err := configs[1].PluginConfig.Decode(&plugin1Config); err != nil {
7890
t.Errorf("Failed to unmarshal plugin config: %v", err)
7991
}
8092

81-
if specUrl, ok := pluginConfig["specUrl"].(string); !ok || specUrl != "http://localhost:8080/system/openapi" {
82-
t.Errorf("Expected specUrl 'http://localhost:8080/system/openapi', got '%v'", pluginConfig["specUrl"])
93+
if specUrl, ok := plugin1Config["specUrl"].(string); !ok || specUrl != "http://localhost:8080/system/openapi" {
94+
t.Errorf("Expected specUrl 'http://localhost:8080/system/openapi', got '%v'", plugin1Config["specUrl"])
8395
}
8496
},
8597
},

plugin/openapi/openapi.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ package openapi
22

33
import (
44
"fmt"
5+
"net/http"
6+
"net/url"
7+
"os"
8+
"sort"
9+
"strings"
10+
511
"github.com/imposter-project/imposter-go/internal/config"
612
"github.com/imposter-project/imposter-go/internal/exchange"
713
"github.com/imposter-project/imposter-go/pkg/logger"
814
"github.com/pb33f/libopenapi"
915
validator "github.com/pb33f/libopenapi-validator"
1016
"github.com/pb33f/libopenapi-validator/errors"
17+
"github.com/pb33f/libopenapi/datamodel"
1118
"github.com/pb33f/libopenapi/datamodel/high/base"
12-
"net/http"
13-
"os"
14-
"sort"
15-
"strings"
1619
)
1720

1821
// OpenAPIVersion represents the version of OpenAPI being used
@@ -46,7 +49,8 @@ type Operation struct {
4649
}
4750

4851
type parserOptions struct {
49-
stripServerPath bool
52+
stripServerPath bool
53+
externalReferenceBaseURL string
5054
}
5155

5256
type OpenAPIParser interface {
@@ -80,6 +84,17 @@ func newOpenAPIParser(specFile string, validate bool, opts parserOptions) (OpenA
8084
return nil, fmt.Errorf("cannot create new document: %e", err)
8185
}
8286

87+
if opts.externalReferenceBaseURL != "" {
88+
u, err := url.Parse(opts.externalReferenceBaseURL)
89+
if err != nil {
90+
return nil, fmt.Errorf("cannot parse external reference URL: %e", err)
91+
}
92+
93+
document.SetConfiguration(&datamodel.DocumentConfiguration{BaseURL: u})
94+
95+
logger.Infof("external base URL set to: %s", u.String())
96+
}
97+
8398
var oasValidator *validator.Validator
8499
if validate {
85100
highLevelValidator, validatorErrs := validator.NewValidator(document)

plugin/openapi/parser_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package openapi
22

33
import (
4+
"net/http"
5+
"net/http/httptest"
46
"os"
57
"path/filepath"
68
"testing"
@@ -63,3 +65,50 @@ func TestNewOpenAPIParser(t *testing.T) {
6365
})
6466
}
6567
}
68+
69+
func TestOpenAPIParser_ExternalURLRefsAreParsed(t *testing.T) {
70+
schemaJSON := `{
71+
"User": {
72+
"properties": {
73+
"id": {
74+
"type": "integer",
75+
"format": "int64",
76+
"example": 10
77+
},
78+
"username": {
79+
"type": "string",
80+
"example": "theUser"
81+
},
82+
"firstName": {
83+
"type": "string",
84+
"example": "John"
85+
},
86+
"lastName": {
87+
"type": "string",
88+
"example": "James"
89+
}
90+
}
91+
}
92+
}`
93+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
94+
if r.URL.Path == "/schemas/user.json" {
95+
w.Header().Set("Content-Type", "application/json")
96+
_, _ = w.Write([]byte(schemaJSON))
97+
return
98+
}
99+
http.NotFound(w, r)
100+
}))
101+
defer ts.Close()
102+
103+
workingDir, _ := os.Getwd()
104+
specFile := filepath.Join(workingDir, "testdata/externalRef/users.yaml")
105+
106+
parser, err := newOpenAPIParser(specFile, false, parserOptions{
107+
externalReferenceBaseURL: ts.URL + "/",
108+
})
109+
110+
assert.NoError(t, err)
111+
assert.NotNil(t, parser)
112+
assert.Equal(t, OpenAPI30, parser.GetVersion())
113+
assert.Len(t, parser.GetOperations(), 2)
114+
}

plugin/openapi/plugin.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package openapi
22

33
import (
44
"fmt"
5+
56
"github.com/imposter-project/imposter-go/plugin/rest"
67

78
"github.com/imposter-project/imposter-go/internal/config"
@@ -26,6 +27,16 @@ func NewPluginHandler(cfg *config.Config, imposterConfig *config.ImposterConfig)
2627
opts := parserOptions{
2728
stripServerPath: cfg.StripServerPath,
2829
}
30+
31+
if !cfg.PluginConfig.IsZero() {
32+
var pluginConfig map[string]interface{}
33+
if err := cfg.PluginConfig.Decode(&pluginConfig); err != nil {
34+
return nil, fmt.Errorf("failed to unmarshal plugin config: %w", err)
35+
}
36+
37+
opts.externalReferenceBaseURL = pluginConfig["externalBaseURL"].(string)
38+
}
39+
2940
validate := cfg.Validation != nil && (cfg.Validation.IsRequestValidationEnabled() || cfg.Validation.IsResponseValidationEnabled())
3041
parser, err := newOpenAPIParser(specFile, validate, opts)
3142
if err != nil {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
plugin: openapi
2+
specFile: users-external-ref.yaml
3+
4+
config:
5+
externalBaseURL: http://example.com
6+
7+
resources:
8+
- path: /users
9+
method: GET
10+
response:
11+
statusCode: 200
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
openapi: 3.0.2
2+
info:
3+
title: External URL references test
4+
description: A test document
5+
version: 1.0.0
6+
paths:
7+
/users:
8+
get:
9+
summary: Get all users
10+
responses:
11+
'200':
12+
description: Successful operation
13+
content:
14+
application/json:
15+
schema:
16+
$ref: 'http://example.com/schemas/user.json'
17+
/users/{id}:
18+
get:
19+
summary: Get a user
20+
responses:
21+
'200':
22+
description: Successful operation
23+
content:
24+
application/json:
25+
schema:
26+
$ref: '#/components/schemas/User'
27+
28+
components:
29+
schemas:
30+
User:
31+
properties:
32+
id:
33+
type: integer
34+
format: int64
35+
example: 10
36+
username:
37+
type: string
38+
example: theUser
39+
firstName:
40+
type: string
41+
example: John
42+
lastName:
43+
type: string
44+
example: James

tools/validate_config/current-format-schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
"description": "OpenAPI specification file (YAML or JSON)",
1818
"type": "string"
1919
},
20+
"externalBaseURL": {
21+
"type": "string"
22+
},
2023
"wsdlFile": {
2124
"type": "string"
2225
},
@@ -83,6 +86,14 @@
8386
},
8487
"validation": { "$ref": "shared-definitions.json#/definitions/validation" }
8588
},
89+
"if": {
90+
"plugin": {
91+
"type": { "const": "openapi" }
92+
}
93+
},
94+
"then": {
95+
"required": ["specFile"]
96+
},
8697
"additionalProperties": false
8798
}
8899
}

0 commit comments

Comments
 (0)