Skip to content

Commit 8aae309

Browse files
committed
Add openapi3 externalDocs validation
1 parent ed20aa7 commit 8aae309

File tree

7 files changed

+171
-16
lines changed

7 files changed

+171
-16
lines changed

openapi3/external_docs.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package openapi3
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
48
"github.com/getkin/kin-openapi/jsoninfo"
59
)
610

@@ -19,3 +23,13 @@ func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
1923
func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
2024
return jsoninfo.UnmarshalStrictStruct(data, e)
2125
}
26+
27+
func (e *ExternalDocs) Validate(ctx context.Context) error {
28+
if e.URL == "" {
29+
return fmt.Errorf("url is required")
30+
}
31+
if _, err := url.Parse(e.URL); err != nil {
32+
return fmt.Errorf("url is incorrect: %w", err)
33+
}
34+
return nil
35+
}

openapi3/external_docs_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package openapi3
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestExternalDocs_Validate(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
extDocs *ExternalDocs
14+
expectedErr string
15+
}{
16+
{
17+
name: "url is missing",
18+
extDocs: &ExternalDocs{},
19+
expectedErr: "url is required",
20+
},
21+
{
22+
name: "url is incorrect",
23+
extDocs: &ExternalDocs{URL: "ht tps://example.com"},
24+
expectedErr: `url is incorrect: parse "ht tps://example.com": first path segment in URL cannot contain colon`,
25+
},
26+
{
27+
name: "ok",
28+
extDocs: &ExternalDocs{URL: "https://example.com"},
29+
},
30+
}
31+
for i := range tests {
32+
tt := tests[i]
33+
t.Run(tt.name, func(t *testing.T) {
34+
err := tt.extDocs.Validate(context.Background())
35+
if tt.expectedErr != "" {
36+
require.EqualError(t, err, tt.expectedErr)
37+
} else {
38+
require.NoError(t, err)
39+
}
40+
})
41+
}
42+
}

openapi3/openapi3.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,23 @@ func (value *T) Validate(ctx context.Context) error {
101101
}
102102
}
103103

104+
{
105+
wrap := func(e error) error { return fmt.Errorf("invalid tags: %v", e) }
106+
if v := value.Tags; v != nil {
107+
if err := v.Validate(ctx); err != nil {
108+
return wrap(err)
109+
}
110+
}
111+
}
112+
113+
{
114+
wrap := func(e error) error { return fmt.Errorf("invalid external docs: %v", e) }
115+
if v := value.ExternalDocs; v != nil {
116+
if err := v.Validate(ctx); err != nil {
117+
return wrap(err)
118+
}
119+
}
120+
}
121+
104122
return nil
105123
}

openapi3/openapi3_test.go

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,9 @@ func spec() *T {
351351
}
352352

353353
func TestValidation(t *testing.T) {
354+
version := `
355+
openapi: 3.0.2
356+
`
354357
info := `
355358
info:
356359
title: "Hello World REST APIs"
@@ -378,9 +381,17 @@ paths:
378381
200:
379382
description: "Get a single greeting object"
380383
`
381-
spec := `
382-
openapi: 3.0.2
383-
` + info + paths + `
384+
externalDocs := `
385+
externalDocs:
386+
url: https://root-ext-docs.com
387+
`
388+
tags := `
389+
tags:
390+
- name: "pet"
391+
externalDocs:
392+
url: https://tags-ext-docs.com
393+
`
394+
spec := version + info + paths + externalDocs + tags + `
384395
components:
385396
schemas:
386397
GreetingObject:
@@ -396,23 +407,58 @@ components:
396407
type: string
397408
`
398409

399-
tests := map[string]string{
400-
spec: "",
401-
strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty string",
402-
strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty string",
403-
strings.Replace(spec, info, ``, 1): "invalid info: must be an object",
404-
strings.Replace(spec, paths, ``, 1): "invalid paths: must be an object",
410+
tests := []struct {
411+
name string
412+
spec string
413+
expectedErr string
414+
}{
415+
{
416+
name: "no errors",
417+
spec: spec,
418+
},
419+
{
420+
name: "version is missing",
421+
spec: strings.Replace(spec, version, "", 1),
422+
expectedErr: "value of openapi must be a non-empty string",
423+
},
424+
{
425+
name: "version is empty string",
426+
spec: strings.Replace(spec, version, "openapi: ''", 1),
427+
expectedErr: "value of openapi must be a non-empty string",
428+
},
429+
{
430+
name: "info section is missing",
431+
spec: strings.Replace(spec, info, ``, 1),
432+
expectedErr: "invalid info: must be an object",
433+
},
434+
{
435+
name: "paths section is missing",
436+
spec: strings.Replace(spec, paths, ``, 1),
437+
expectedErr: "invalid paths: must be an object",
438+
},
439+
{
440+
name: "externalDocs section is invalid",
441+
spec: strings.Replace(spec, externalDocs,
442+
strings.ReplaceAll(externalDocs, "url: https://root-ext-docs.com", "url: ''"), 1),
443+
expectedErr: "invalid external docs: url is required",
444+
},
445+
{
446+
name: "tags section is invalid",
447+
spec: strings.Replace(spec, tags,
448+
strings.ReplaceAll(tags, "url: https://tags-ext-docs.com", "url: ''"), 1),
449+
expectedErr: "invalid tags: invalid external docs: url is required",
450+
},
405451
}
406-
407-
for spec, expectedErr := range tests {
408-
t.Run(expectedErr, func(t *testing.T) {
452+
for i := range tests {
453+
tt := tests[i]
454+
t.Run(tt.name, func(t *testing.T) {
409455
doc := &T{}
410-
err := yaml.Unmarshal([]byte(spec), &doc)
456+
err := yaml.Unmarshal([]byte(tt.spec), &doc)
411457
require.NoError(t, err)
412458

413459
err = doc.Validate(context.Background())
414-
if expectedErr != "" {
415-
require.EqualError(t, err, expectedErr)
460+
if tt.expectedErr != "" {
461+
require.EqualError(t, err, tt.expectedErr)
416462
} else {
417463
require.NoError(t, err)
418464
}

openapi3/operation.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openapi3
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"strconv"
78

89
"github.com/getkin/kin-openapi/jsoninfo"
@@ -138,5 +139,10 @@ func (value *Operation) Validate(ctx context.Context) error {
138139
} else {
139140
return errors.New("value of responses must be an object")
140141
}
142+
if v := value.ExternalDocs; v != nil {
143+
if err := v.Validate(ctx); err != nil {
144+
return fmt.Errorf("invalid external docs: %w", err)
145+
}
146+
}
141147
return nil
142148
}

openapi3/schema.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
729729
}
730730
}
731731

732+
if v := schema.ExternalDocs; v != nil {
733+
if err = v.Validate(ctx); err != nil {
734+
return fmt.Errorf("invalid external docs: %w", err)
735+
}
736+
}
737+
732738
return
733739
}
734740

openapi3/tag.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package openapi3
22

3-
import "github.com/getkin/kin-openapi/jsoninfo"
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/getkin/kin-openapi/jsoninfo"
8+
)
49

510
// Tags is specified by OpenAPI/Swagger 3.0 standard.
611
type Tags []*Tag
@@ -14,6 +19,15 @@ func (tags Tags) Get(name string) *Tag {
1419
return nil
1520
}
1621

22+
func (tags Tags) Validate(ctx context.Context) error {
23+
for _, v := range tags {
24+
if err := v.Validate(ctx); err != nil {
25+
return err
26+
}
27+
}
28+
return nil
29+
}
30+
1731
// Tag is specified by OpenAPI/Swagger 3.0 standard.
1832
type Tag struct {
1933
ExtensionProps
@@ -29,3 +43,12 @@ func (t *Tag) MarshalJSON() ([]byte, error) {
2943
func (t *Tag) UnmarshalJSON(data []byte) error {
3044
return jsoninfo.UnmarshalStrictStruct(data, t)
3145
}
46+
47+
func (t *Tag) Validate(ctx context.Context) error {
48+
if v := t.ExternalDocs; v != nil {
49+
if err := v.Validate(ctx); err != nil {
50+
return fmt.Errorf("invalid external docs: %w", err)
51+
}
52+
}
53+
return nil
54+
}

0 commit comments

Comments
 (0)