├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ └── code.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── bool_or_schema.go ├── bool_or_schema_test.go ├── callback.go ├── components.go ├── components_test.go ├── contact.go ├── discriminator.go ├── encoding.go ├── example.go ├── extensions.go ├── extensions_test.go ├── external-docs.go ├── go.mod ├── go.sum ├── header.go ├── info.go ├── license.go ├── link.go ├── media_type.go ├── oauth-flow.go ├── oauth-flows.go ├── openapi.go ├── operation.go ├── parameter.go ├── parser.go ├── parser_test.go ├── path_item.go ├── paths.go ├── ref.go ├── ref_test.go ├── request_body.go ├── response.go ├── responses.go ├── schema.go ├── schema_test.go ├── security-requirement.go ├── security-scheme.go ├── server.go ├── server_variable.go ├── single_or_array.go ├── single_or_array_test.go ├── tag.go ├── testdata ├── api-with-examples.json ├── api-with-examples.yaml ├── callback-example.json ├── callback-example.yaml ├── link-example.json ├── link-example.yaml ├── non-oauth-scopes.json ├── non-oauth-scopes.yaml ├── petstore-expanded.json ├── petstore-expanded.yaml ├── petstore.json ├── petstore.yaml ├── uspto.json ├── uspto.yaml ├── webhook-example.json └── webhook-example.yaml ├── type_formats.go ├── types.go ├── validation.go ├── validation_options.go ├── validation_test.go ├── webhooks.go └── xml.go /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | component_management: 3 | default_rules: 4 | statuses: 5 | - type: project 6 | target: auto 7 | branches: 8 | - main 9 | individual_components: 10 | - component_id: spec 11 | name: Specification 12 | paths: 13 | - spec/** 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: weekly 11 | groups: 12 | golangci-lint: 13 | patterns: 14 | - "golangci*" 15 | ci: 16 | patterns: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/code.yaml: -------------------------------------------------------------------------------- 1 | name: Code 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | - v* 10 | pull_request: 11 | schedule: 12 | - cron: '38 5 * * 3' 13 | 14 | concurrency: 15 | group: ${{ format('{0}-{1}', github.workflow, github.head_ref) }} 16 | cancel-in-progress: true 17 | 18 | permissions: read-all 19 | 20 | jobs: 21 | Lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4.2.2 # immutable action, safe to use the versions 25 | - uses: actions/setup-go@v5.5.0 # immutable action, safe to use the versions 26 | with: 27 | go-version-file: go.mod 28 | - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 29 | with: 30 | version: latest 31 | 32 | UnitTestJob: 33 | runs-on: ubuntu-latest 34 | strategy: 35 | matrix: 36 | go: 37 | - "1.22" 38 | - "1.23" 39 | - "1.24" 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4.2.2 # immutable action, safe to use the versions 43 | - name: Install Go 44 | uses: actions/setup-go@v5.5.0 # immutable action, safe to use the versions 45 | with: 46 | go-version: ${{ matrix.go }} 47 | - run: go install github.com/jstemmer/go-junit-report/v2@latest 48 | - run: go test -race -cover -coverprofile=coverage.out -covermode=atomic ./... 49 | - run: go test -json 2>&1 | go-junit-report -parser gojson > junit.xml 50 | if: always() 51 | - name: Upload coverage reports to Codecov 52 | if: ${{ !cancelled() }} 53 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 54 | env: 55 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 56 | - name: Upload test results to Codecov 57 | if: ${{ !cancelled() }} 58 | uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 59 | env: 60 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 61 | 62 | UnitTests: 63 | if: ${{ always() }} 64 | needs: UnitTestJob 65 | runs-on: ubuntu-latest 66 | steps: 67 | - name: Check status 68 | if: ${{ needs.UnitTestJob.result != 'success' }} 69 | run: exit 1 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .github/Brewfile.lock.json 18 | .idea 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: none 5 | 6 | enable: # keep in ascending order 7 | - asasalint 8 | - asciicheck 9 | - bodyclose 10 | - canonicalheader 11 | - containedctx 12 | - contextcheck 13 | - copyloopvar 14 | - decorder 15 | - dogsled 16 | - dupword 17 | - durationcheck 18 | - err113 19 | - errcheck 20 | - errchkjson 21 | - errname 22 | - errorlint 23 | - exptostd 24 | - fatcontext 25 | - gocheckcompilerdirectives 26 | - gochecksumtype 27 | - goconst 28 | - gocritic 29 | - goprintffuncname 30 | - gosec 31 | - gosmopolitan 32 | - govet 33 | - iface 34 | - inamedparam 35 | - ineffassign 36 | - intrange 37 | - makezero 38 | - mirror 39 | - misspell 40 | - musttag 41 | - nestif 42 | - nilerr 43 | - nilnesserr 44 | - nilnil 45 | - noctx 46 | - nolintlint 47 | - perfsprint 48 | - prealloc 49 | - predeclared 50 | - reassign 51 | - recvcheck 52 | - sloglint 53 | - staticcheck 54 | - tagalign 55 | - testableexamples 56 | - testifylint 57 | - testpackage 58 | - thelper 59 | - tparallel 60 | - unconvert 61 | - unparam 62 | - unused 63 | - usestdlibvars 64 | - usetesting 65 | - wastedassign 66 | - whitespace 67 | 68 | settings: 69 | misspell: 70 | locale: US 71 | nestif: 72 | min-complexity: 12 73 | goconst: 74 | min-len: 3 75 | min-occurrences: 3 76 | prealloc: 77 | for-loops: true 78 | gocritic: 79 | enabled-tags: 80 | - diagnostic 81 | - experimental 82 | - opinionated 83 | - performance 84 | - style 85 | disabled-checks: 86 | - whyNoLint 87 | 88 | exclusions: 89 | warn-unused: true 90 | rules: 91 | - path: _test\.go 92 | linters: 93 | - gosec 94 | 95 | formatters: 96 | enable: 97 | - gci 98 | - gofumpt 99 | settings: 100 | gci: 101 | sections: 102 | - standard 103 | - default 104 | - prefix(github.com/sv-tools/openapi) 105 | gofumpt: 106 | extra-rules: true 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SV Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI v3.1 Specification 2 | 3 | [![Code Analysis](https://github.com/sv-tools/openapi/actions/workflows/code.yaml/badge.svg)](https://github.com/sv-tools/openapi/actions/workflows/code.yaml) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/sv-tools/openapi.svg)](https://pkg.go.dev/github.com/sv-tools/openapi) 5 | [![codecov](https://codecov.io/gh/sv-tools/openapi/branch/main/graph/badge.svg?token=0XVOTDR1CW)](https://codecov.io/gh/sv-tools/openapi) 6 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/sv-tools/openapi?style=flat)](https://github.com/sv-tools/openapi/releases) 7 | 8 | The implementation of OpenAPI v3.1 Specification for Go using generics. 9 | 10 | ```shell 11 | go get github.com/sv-tools/openapi 12 | ``` 13 | 14 | ## Supported Go versions: 15 | 16 | * v1.24 17 | * v1.23 18 | * v1.22 19 | 20 | ## Versions: 21 | 22 | * v0 - **Deprecated**. The initial version with the full implementation of the v3.1 Specification using generics. See `v0` branch. 23 | * v1 - The current version with the in-place validation of the specification. 24 | * The minimal version of Go is `v1.22`. 25 | * Everything have been moved to root folder. So, the import path is `github.com/sv-tools/openapi`. 26 | * Added `Validator` struct for validation of the specification and the data. 27 | * `Validator.ValidateSpec()` method validates the specification. 28 | * `Validator.ValidateData()` method validates the data. 29 | * `Validator.ValidateDataAsJSON()` method validates the data by converting it into `map[string]any` type first using `json.Marshal` and `json.Unmarshal`. 30 | **WARNING**: the function is slow due to double conversion. 31 | * Added `ParseObject` function to create `SchemaBuilder` by parsing an object. 32 | The function supports `json`, `yaml` and `openapi` field tags for the structs. 33 | * Use OpenAPI `v3.1.1` by default. 34 | 35 | ## Features 36 | 37 | * The official v3.0 and v3.1 [examples](https://github.com/OAI/OpenAPI-Specification/tree/main/examples) are tested. 38 | In most cases v3.0 specification can be converted to v3.1 by changing the version's parameter only. 39 | ```diff 40 | @@ -1,4 +1,4 @@ 41 | -openapi: "3.0.0" 42 | +openapi: "3.1.0" 43 | ``` 44 | 45 | **NOTE**: The descriptions of most structures and their fields are taken from the official documentations. 46 | 47 | ## Links 48 | 49 | * OpenAPI Specification: and 50 | * JSON Schema: and 51 | * The list of most popular alternatives: 52 | 53 | ## License 54 | 55 | MIT licensed. See the bundled [LICENSE](LICENSE) file for more details. 56 | -------------------------------------------------------------------------------- /bool_or_schema.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // BoolOrSchema handles Boolean or Schema type. 10 | // 11 | // It MUST be used as a pointer, 12 | // otherwise the `false` can be omitted by json or yaml encoders in case of `omitempty` tag is set. 13 | type BoolOrSchema struct { 14 | Schema *RefOrSpec[Schema] 15 | Allowed bool 16 | } 17 | 18 | // UnmarshalJSON implements json.Unmarshaler interface. 19 | func (o *BoolOrSchema) UnmarshalJSON(data []byte) error { 20 | if json.Unmarshal(data, &o.Allowed) == nil { 21 | o.Schema = nil 22 | return nil 23 | } 24 | if err := json.Unmarshal(data, &o.Schema); err != nil { 25 | return err 26 | } 27 | o.Allowed = true 28 | return nil 29 | } 30 | 31 | // MarshalJSON implements json.Marshaler interface. 32 | func (o *BoolOrSchema) MarshalJSON() ([]byte, error) { 33 | var v any 34 | if o.Schema != nil { 35 | v = o.Schema 36 | } else { 37 | v = o.Allowed 38 | } 39 | return json.Marshal(&v) 40 | } 41 | 42 | // UnmarshalYAML implements yaml.Unmarshaler interface. 43 | func (o *BoolOrSchema) UnmarshalYAML(node *yaml.Node) error { 44 | if node.Decode(&o.Allowed) == nil { 45 | o.Schema = nil 46 | return nil 47 | } 48 | if err := node.Decode(&o.Schema); err != nil { 49 | return err 50 | } 51 | o.Allowed = true 52 | return nil 53 | } 54 | 55 | // MarshalYAML implements yaml.Marshaler interface. 56 | func (o *BoolOrSchema) MarshalYAML() (any, error) { 57 | var v any 58 | if o.Schema != nil { 59 | v = o.Schema 60 | } else { 61 | v = o.Allowed 62 | } 63 | 64 | return v, nil 65 | } 66 | 67 | func (o *BoolOrSchema) validateSpec(path string, validator *Validator) []*validationError { 68 | var errs []*validationError 69 | if o.Schema != nil { 70 | errs = append(errs, o.Schema.validateSpec(path, validator)...) 71 | } 72 | return errs 73 | } 74 | 75 | func NewBoolOrSchema(v any) *BoolOrSchema { 76 | switch v := v.(type) { 77 | case bool: 78 | return &BoolOrSchema{Allowed: v} 79 | case *RefOrSpec[Schema]: 80 | return &BoolOrSchema{Schema: v} 81 | case *SchemaBuilder: 82 | return &BoolOrSchema{Schema: v.Build()} 83 | default: 84 | return nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /bool_or_schema_test.go: -------------------------------------------------------------------------------- 1 | package openapi_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/sv-tools/openapi" 11 | ) 12 | 13 | type testAD struct { 14 | AP *openapi.BoolOrSchema `json:"ap,omitempty" yaml:"ap,omitempty"` 15 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 16 | } 17 | 18 | func TestAdditionalPropertiesJSON(t *testing.T) { 19 | for _, tt := range []struct { 20 | name string 21 | data string 22 | nilAP bool 23 | allowed bool 24 | nilSchema bool 25 | }{ 26 | { 27 | name: "no AP", 28 | data: `{"name": "foo"}`, 29 | nilAP: true, 30 | }, 31 | { 32 | name: "false", 33 | data: `{"name": "foo", "ap": false}`, 34 | nilAP: false, 35 | allowed: false, 36 | nilSchema: true, 37 | }, 38 | { 39 | name: "true", 40 | data: `{"name": "foo", "ap": true}`, 41 | nilAP: false, 42 | allowed: true, 43 | nilSchema: true, 44 | }, 45 | { 46 | name: "schema", 47 | data: `{"name": "foo", "ap": {"title": "bar", "description": "test"}}`, 48 | nilAP: false, 49 | allowed: true, 50 | nilSchema: false, 51 | }, 52 | } { 53 | t.Run(tt.name, func(t *testing.T) { 54 | t.Run("json", func(t *testing.T) { 55 | var v testAD 56 | require.NoError(t, json.Unmarshal([]byte(tt.data), &v)) 57 | require.Equal(t, "foo", v.Name) 58 | if tt.nilAP { 59 | require.Nil(t, v.AP) 60 | } else { 61 | require.NotNil(t, v.AP) 62 | require.Equal(t, tt.allowed, v.AP.Allowed) 63 | require.Equal(t, tt.nilSchema, v.AP.Schema == nil) 64 | } 65 | newJson, err := json.Marshal(&v) 66 | require.NoError(t, err) 67 | require.JSONEq(t, tt.data, string(newJson)) 68 | }) 69 | 70 | t.Run("yaml", func(t *testing.T) { 71 | var v testAD 72 | require.NoError(t, yaml.Unmarshal([]byte(tt.data), &v)) 73 | require.Equal(t, "foo", v.Name) 74 | if tt.nilAP { 75 | require.Nil(t, v.AP) 76 | } else { 77 | require.NotNil(t, v.AP) 78 | require.Equal(t, tt.allowed, v.AP.Allowed) 79 | require.Equal(t, tt.nilSchema, v.AP.Schema == nil) 80 | } 81 | newYaml, err := yaml.Marshal(&v) 82 | require.NoError(t, err) 83 | require.YAMLEq(t, tt.data, string(newYaml)) 84 | }) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /callback.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // Callback is a map of possible out-of band callbacks related to the parent operation. 10 | // Each value in the map is a Path Item Object that describes a set of requests that may be initiated by 11 | // the API provider and the expected responses. 12 | // The key value used to identify the path item object is an expression, evaluated at runtime, 13 | // that identifies a URL to use for the callback operation. 14 | // To describe incoming requests from the API provider independent from another API call, use the webhooks field. 15 | // 16 | // https://spec.openapis.org/oas/v3.1.1#callback-object 17 | // 18 | // Example: 19 | // 20 | // myCallback: 21 | // '{$request.query.queryUrl}': 22 | // post: 23 | // requestBody: 24 | // description: Callback payload 25 | // content: 26 | // 'application/json': 27 | // schema: 28 | // $ref: '#/components/schemas/SomePayload' 29 | // responses: 30 | // '200': 31 | // description: callback successfully processed 32 | type Callback struct { 33 | Paths map[string]*RefOrSpec[Extendable[PathItem]] 34 | } 35 | 36 | // MarshalJSON implements json.Marshaler interface. 37 | func (o *Callback) MarshalJSON() ([]byte, error) { 38 | return json.Marshal(&o.Paths) 39 | } 40 | 41 | // UnmarshalJSON implements json.Unmarshaler interface. 42 | func (o *Callback) UnmarshalJSON(data []byte) error { 43 | return json.Unmarshal(data, &o.Paths) 44 | } 45 | 46 | // MarshalYAML implements yaml.Marshaler interface. 47 | func (o *Callback) MarshalYAML() (any, error) { 48 | return o.Paths, nil 49 | } 50 | 51 | // UnmarshalYAML implements yaml.Unmarshaler interface. 52 | func (o *Callback) UnmarshalYAML(node *yaml.Node) error { 53 | return node.Decode(&o.Paths) 54 | } 55 | 56 | func (o *Callback) validateSpec(location string, validator *Validator) []*validationError { 57 | var errs []*validationError 58 | for k, v := range o.Paths { 59 | errs = append(errs, v.validateSpec(joinLoc(location, k), validator)...) 60 | } 61 | return errs 62 | } 63 | 64 | func (o *Callback) Add(expression string, item *RefOrSpec[Extendable[PathItem]]) *Callback { 65 | if o.Paths == nil { 66 | o.Paths = make(map[string]*RefOrSpec[Extendable[PathItem]], 1) 67 | } 68 | o.Paths[expression] = item 69 | return o 70 | } 71 | 72 | type CallbackBuilder struct { 73 | spec *RefOrSpec[Extendable[Callback]] 74 | } 75 | 76 | func NewCallbackBuilder() *CallbackBuilder { 77 | return &CallbackBuilder{ 78 | spec: NewRefOrExtSpec[Callback](&Callback{ 79 | Paths: make(map[string]*RefOrSpec[Extendable[PathItem]]), 80 | }), 81 | } 82 | } 83 | 84 | func (b *CallbackBuilder) Build() *RefOrSpec[Extendable[Callback]] { 85 | return b.spec 86 | } 87 | 88 | func (b *CallbackBuilder) Extensions(v map[string]any) *CallbackBuilder { 89 | b.spec.Spec.Extensions = v 90 | return b 91 | } 92 | 93 | func (b *CallbackBuilder) AddExt(name string, value any) *CallbackBuilder { 94 | b.spec.Spec.AddExt(name, value) 95 | return b 96 | } 97 | 98 | func (b *CallbackBuilder) Paths(paths map[string]*RefOrSpec[Extendable[PathItem]]) *CallbackBuilder { 99 | b.spec.Spec.Spec.Paths = paths 100 | return b 101 | } 102 | 103 | func (b *CallbackBuilder) AddPathItem(expression string, item *RefOrSpec[Extendable[PathItem]]) *CallbackBuilder { 104 | b.spec.Spec.Spec.Add(expression, item) 105 | return b 106 | } 107 | -------------------------------------------------------------------------------- /components_test.go: -------------------------------------------------------------------------------- 1 | package openapi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/sv-tools/openapi" 9 | ) 10 | 11 | func TestComponents_Add(t *testing.T) { 12 | for _, tt := range []struct { 13 | name string 14 | create func(tb testing.TB) (string, any) 15 | check func(tb testing.TB, c *openapi.Components) 16 | }{ 17 | { 18 | name: "schema ref or spec", 19 | create: func(tb testing.TB) (string, any) { 20 | tb.Helper() 21 | 22 | o := openapi.NewSchemaBuilder().Title("test").Build() 23 | return "testSchema", o 24 | }, 25 | check: func(tb testing.TB, c *openapi.Components) { 26 | tb.Helper() 27 | 28 | require.Len(tb, c.Schemas, 1) 29 | require.NotNil(tb, c.Schemas["testSchema"]) 30 | require.NotNil(tb, c.Schemas["testSchema"].Spec) 31 | require.Equal(tb, "test", c.Schemas["testSchema"].Spec.Title) 32 | }, 33 | }, 34 | { 35 | name: "response spec", 36 | create: func(tb testing.TB) (string, any) { 37 | tb.Helper() 38 | 39 | o := openapi.NewResponseBuilder().Description("test").Build() 40 | return "testResponse", o 41 | }, 42 | check: func(tb testing.TB, c *openapi.Components) { 43 | tb.Helper() 44 | 45 | require.Len(tb, c.Responses, 1) 46 | require.NotNil(tb, c.Responses["testResponse"]) 47 | require.NotNil(tb, c.Responses["testResponse"].Spec) 48 | require.NotNil(tb, c.Responses["testResponse"].Spec.Spec) 49 | require.Equal(tb, "test", c.Responses["testResponse"].Spec.Spec.Description) 50 | }, 51 | }, 52 | { 53 | name: "parameter spec", 54 | create: func(tb testing.TB) (string, any) { 55 | tb.Helper() 56 | 57 | o := openapi.NewParameterBuilder().Description("test").Build() 58 | return "testParameter", o 59 | }, 60 | check: func(tb testing.TB, c *openapi.Components) { 61 | tb.Helper() 62 | 63 | require.Len(tb, c.Parameters, 1) 64 | require.NotNil(tb, c.Parameters["testParameter"]) 65 | require.NotNil(tb, c.Parameters["testParameter"].Spec) 66 | require.NotNil(tb, c.Parameters["testParameter"].Spec.Spec) 67 | require.Equal(tb, "test", c.Parameters["testParameter"].Spec.Spec.Description) 68 | }, 69 | }, 70 | { 71 | name: "examples spec", 72 | create: func(tb testing.TB) (string, any) { 73 | tb.Helper() 74 | 75 | o := openapi.NewExampleBuilder().Description("test").Build() 76 | return "testExamples", o 77 | }, 78 | check: func(tb testing.TB, c *openapi.Components) { 79 | tb.Helper() 80 | 81 | require.Len(tb, c.Examples, 1) 82 | require.NotNil(tb, c.Examples["testExamples"]) 83 | require.NotNil(tb, c.Examples["testExamples"].Spec) 84 | require.NotNil(tb, c.Examples["testExamples"].Spec.Spec) 85 | require.Equal(tb, "test", c.Examples["testExamples"].Spec.Spec.Description) 86 | }, 87 | }, 88 | { 89 | name: "request body spec", 90 | create: func(tb testing.TB) (string, any) { 91 | tb.Helper() 92 | 93 | o := openapi.NewRequestBodyBuilder().Description("test").Build() 94 | return "testRequestBodies", o 95 | }, 96 | check: func(tb testing.TB, c *openapi.Components) { 97 | tb.Helper() 98 | 99 | require.Len(tb, c.RequestBodies, 1) 100 | require.NotNil(tb, c.RequestBodies["testRequestBodies"]) 101 | require.NotNil(tb, c.RequestBodies["testRequestBodies"].Spec) 102 | require.NotNil(tb, c.RequestBodies["testRequestBodies"].Spec.Spec) 103 | require.Equal(tb, "test", c.RequestBodies["testRequestBodies"].Spec.Spec.Description) 104 | }, 105 | }, 106 | { 107 | name: "headers spec", 108 | create: func(tb testing.TB) (string, any) { 109 | tb.Helper() 110 | 111 | o := openapi.NewHeaderBuilder().Description("test").Build() 112 | return "testHeader", o 113 | }, 114 | check: func(tb testing.TB, c *openapi.Components) { 115 | tb.Helper() 116 | 117 | require.Len(tb, c.Headers, 1) 118 | require.NotNil(tb, c.Headers["testHeader"]) 119 | require.NotNil(tb, c.Headers["testHeader"].Spec) 120 | require.NotNil(tb, c.Headers["testHeader"].Spec.Spec) 121 | require.Equal(tb, "test", c.Headers["testHeader"].Spec.Spec.Description) 122 | }, 123 | }, 124 | { 125 | name: "security schemes spec", 126 | create: func(tb testing.TB) (string, any) { 127 | tb.Helper() 128 | 129 | o := &openapi.SecurityScheme{ 130 | Description: "test", 131 | } 132 | return "testSecurityScheme", openapi.NewRefOrExtSpec[openapi.SecurityScheme](o) 133 | }, 134 | check: func(tb testing.TB, c *openapi.Components) { 135 | tb.Helper() 136 | 137 | require.Len(tb, c.SecuritySchemes, 1) 138 | require.NotNil(tb, c.SecuritySchemes["testSecurityScheme"]) 139 | require.NotNil(tb, c.SecuritySchemes["testSecurityScheme"].Spec) 140 | require.NotNil(tb, c.SecuritySchemes["testSecurityScheme"].Spec.Spec) 141 | require.Equal(tb, "test", c.SecuritySchemes["testSecurityScheme"].Spec.Spec.Description) 142 | }, 143 | }, 144 | { 145 | name: "link spec", 146 | create: func(tb testing.TB) (string, any) { 147 | tb.Helper() 148 | 149 | o := openapi.NewLinkBuilder().Description("test").Build() 150 | return "testLink", o 151 | }, 152 | check: func(tb testing.TB, c *openapi.Components) { 153 | tb.Helper() 154 | 155 | require.Len(tb, c.Links, 1) 156 | require.NotNil(tb, c.Links["testLink"]) 157 | require.NotNil(tb, c.Links["testLink"].Spec) 158 | require.NotNil(tb, c.Links["testLink"].Spec.Spec) 159 | require.Equal(tb, "test", c.Links["testLink"].Spec.Spec.Description) 160 | }, 161 | }, 162 | { 163 | name: "callback spec", 164 | create: func(tb testing.TB) (string, any) { 165 | tb.Helper() 166 | 167 | o := openapi.NewCallbackBuilder().AddPathItem( 168 | "testPath", 169 | openapi.NewPathItemBuilder().Description("test").Build(), 170 | ).Build() 171 | return "testCallback", o 172 | }, 173 | check: func(tb testing.TB, c *openapi.Components) { 174 | tb.Helper() 175 | 176 | require.Len(tb, c.Callbacks, 1) 177 | require.NotNil(tb, c.Callbacks["testCallback"]) 178 | require.NotNil(tb, c.Callbacks["testCallback"].Spec) 179 | require.NotNil(tb, c.Callbacks["testCallback"].Spec.Spec) 180 | paths := c.Callbacks["testCallback"].Spec.Spec.Paths 181 | require.Len(tb, paths, 1) 182 | require.NotNil(tb, paths["testPath"]) 183 | require.NotNil(tb, paths["testPath"].Spec) 184 | require.NotNil(tb, paths["testPath"].Spec.Spec) 185 | require.Equal(tb, "test", paths["testPath"].Spec.Spec.Description) 186 | }, 187 | }, 188 | { 189 | name: "path item spec", 190 | create: func(tb testing.TB) (string, any) { 191 | tb.Helper() 192 | 193 | o := openapi.NewPathItemBuilder().Description("test").Build() 194 | return "testPathItem", o 195 | }, 196 | check: func(tb testing.TB, c *openapi.Components) { 197 | tb.Helper() 198 | 199 | require.Len(tb, c.Paths, 1) 200 | require.NotNil(tb, c.Paths["testPathItem"]) 201 | require.NotNil(tb, c.Paths["testPathItem"].Spec) 202 | require.NotNil(tb, c.Paths["testPathItem"].Spec.Spec) 203 | require.Equal(tb, "test", c.Paths["testPathItem"].Spec.Spec.Description) 204 | }, 205 | }, 206 | } { 207 | t.Run(tt.name, func(t *testing.T) { 208 | name, obj := tt.create(t) 209 | tt.check(t, (&openapi.Components{}).Add(name, obj)) 210 | }) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /contact.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Contact information for the exposed API. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#contact-object 6 | // 7 | // Example: 8 | // 9 | // name: API Support 10 | // url: https://www.example.com/support 11 | // email: support@example.com 12 | type Contact struct { 13 | // The identifying name of the contact person/organization. 14 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 15 | // The URL pointing to the contact information. 16 | // This MUST be in the form of a URL. 17 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 18 | // The email address of the contact person/organization. 19 | // This MUST be in the form of an email address. 20 | Email string `json:"email,omitempty" yaml:"email,omitempty"` 21 | } 22 | 23 | func (o *Contact) validateSpec(location string, _ *Validator) []*validationError { 24 | var errs []*validationError 25 | if err := checkURL(o.URL); err != nil { 26 | errs = append(errs, newValidationError(joinLoc(location, "url"), err)) 27 | } 28 | if err := checkEmail(o.Email); err != nil { 29 | errs = append(errs, newValidationError(joinLoc(location, "email"), err)) 30 | } 31 | return errs 32 | } 33 | 34 | type ContactBuilder struct { 35 | spec *Extendable[Contact] 36 | } 37 | 38 | func NewContactBuilder() *ContactBuilder { 39 | return &ContactBuilder{ 40 | spec: NewExtendable(&Contact{}), 41 | } 42 | } 43 | 44 | func (b *ContactBuilder) Build() *Extendable[Contact] { 45 | return b.spec 46 | } 47 | 48 | func (b *ContactBuilder) Extensions(v map[string]any) *ContactBuilder { 49 | b.spec.Extensions = v 50 | return b 51 | } 52 | 53 | func (b *ContactBuilder) AddExt(name string, value any) *ContactBuilder { 54 | b.spec.AddExt(name, value) 55 | return b 56 | } 57 | 58 | func (b *ContactBuilder) Name(v string) *ContactBuilder { 59 | b.spec.Spec.Name = v 60 | return b 61 | } 62 | 63 | func (b *ContactBuilder) URL(v string) *ContactBuilder { 64 | b.spec.Spec.URL = v 65 | return b 66 | } 67 | 68 | func (b *ContactBuilder) Email(v string) *ContactBuilder { 69 | b.spec.Spec.Email = v 70 | return b 71 | } 72 | -------------------------------------------------------------------------------- /discriminator.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Discriminator is used when request bodies or response payloads may be one of a number of different schemas, 4 | // a discriminator object can be used to aid in serialization, deserialization, and validation. 5 | // The discriminator is a specific object in a schema which is used to inform the consumer of the document of 6 | // an alternative schema based on the value associated with it. 7 | // When using the discriminator, inline schemas will not be considered. 8 | // 9 | // https://spec.openapis.org/oas/v3.1.1#discriminator-object 10 | // 11 | // Example: 12 | // 13 | // MyResponseType: 14 | // oneOf: 15 | // - $ref: '#/components/schemas/Cat' 16 | // - $ref: '#/components/schemas/Dog' 17 | // - $ref: '#/components/schemas/Lizard' 18 | // - $ref: 'https://gigantic-server.com/schemas/Monster/schema.json' 19 | // discriminator: 20 | // propertyName: petType 21 | // mapping: 22 | // dog: '#/components/schemas/Dog' 23 | // monster: 'https://gigantic-server.com/schemas/Monster/schema.json' 24 | type Discriminator struct { 25 | // An object to hold mappings between payload values and schema names or references. 26 | Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` 27 | // REQUIRED. 28 | // The name of the property in the payload that will hold the discriminator value. 29 | PropertyName string `json:"propertyName" yaml:"propertyName"` 30 | } 31 | 32 | func (o *Discriminator) validateSpec(location string, validator *Validator) []*validationError { 33 | var errs []*validationError 34 | if o.PropertyName == "" { 35 | errs = append(errs, newValidationError(joinLoc(location, "propertyName"), ErrRequired)) 36 | } 37 | for k, v := range o.Mapping { 38 | ref := NewRefOrSpec[Schema](v) 39 | errs = append(errs, ref.validateSpec(joinLoc(location, "mapping", k), validator)...) 40 | } 41 | return errs 42 | } 43 | 44 | type DiscriminatorBuilder struct { 45 | spec *Discriminator 46 | } 47 | 48 | func NewDiscriminatorBuilder() *DiscriminatorBuilder { 49 | return &DiscriminatorBuilder{ 50 | spec: &Discriminator{}, 51 | } 52 | } 53 | 54 | func (b *DiscriminatorBuilder) Build() *Discriminator { 55 | return b.spec 56 | } 57 | 58 | func (b *DiscriminatorBuilder) Mapping(v map[string]string) *DiscriminatorBuilder { 59 | b.spec.Mapping = v 60 | return b 61 | } 62 | 63 | func (b *DiscriminatorBuilder) AddMapping(name, value string) *DiscriminatorBuilder { 64 | if b.spec.Mapping == nil { 65 | b.spec.Mapping = make(map[string]string, 1) 66 | } 67 | b.spec.Mapping[name] = value 68 | return b 69 | } 70 | 71 | func (b *DiscriminatorBuilder) PropertyName(v string) *DiscriminatorBuilder { 72 | b.spec.PropertyName = v 73 | return b 74 | } 75 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Encoding is definition that applied to a single schema property. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#encoding-object 6 | // 7 | // Example: 8 | // 9 | // requestBody: 10 | // content: 11 | // multipart/form-data: 12 | // schema: 13 | // type: object 14 | // properties: 15 | // id: 16 | // # default is text/plain 17 | // type: string 18 | // format: uuid 19 | // address: 20 | // # default is application/json 21 | // type: object 22 | // properties: {} 23 | // historyMetadata: 24 | // # need to declare XML format! 25 | // description: metadata in XML format 26 | // type: object 27 | // properties: {} 28 | // profileImage: {} 29 | // encoding: 30 | // historyMetadata: 31 | // # require XML Content-Type in utf-8 encoding 32 | // contentType: application/xml; charset=utf-8 33 | // profileImage: 34 | // # only accept png/jpeg 35 | // contentType: image/png, image/jpeg 36 | // headers: 37 | // X-Rate-Limit-Limit: 38 | // description: The number of allowed requests in the current period 39 | // schema: 40 | // type: integer 41 | type Encoding struct { 42 | // The Content-Type for encoding a specific property. 43 | // Default value depends on the property type: 44 | // for object - application/json; 45 | // for array – the default is defined based on the inner type; 46 | // for all other cases the default is application/octet-stream. 47 | // The value can be a specific media type (e.g. application/json), a wildcard media type (e.g. image/*), 48 | // or a comma-separated list of the two types. 49 | ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` 50 | // A map allowing additional information to be provided as headers, for example Content-Disposition. 51 | // Content-Type is described separately and SHALL be ignored in this section. 52 | // This property SHALL be ignored if the request body media type is not a multipart. 53 | Headers map[string]*RefOrSpec[Extendable[Header]] `json:"headers,omitempty" yaml:"headers,omitempty"` 54 | // Describes how a specific property value will be serialized depending on its type. 55 | // See Parameter Object for details on the style property. 56 | // The behavior follows the same values as query parameters, including default values. 57 | // This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. 58 | // If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 59 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 60 | // When this is true, property values of type array or object generate separate parameters for each value of the array, 61 | // or key-value-pair of the map. 62 | // For other types of properties this property has no effect. 63 | // When style is form, the default value is true. 64 | // For all other styles, the default value is false. 65 | // This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. 66 | // If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 67 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 68 | // Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986] 69 | // :/?#[]@!$&'()*+,;= 70 | // to be included without percent-encoding. 71 | // The default value is false. 72 | // This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. 73 | // If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored. 74 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 75 | } 76 | 77 | func (o *Encoding) validateSpec(location string, validator *Validator) []*validationError { 78 | var errs []*validationError 79 | if len(o.Headers) > 0 { 80 | for k, v := range o.Headers { 81 | errs = append(errs, v.validateSpec(joinLoc(location, "headers", k), validator)...) 82 | } 83 | } 84 | 85 | switch o.Style { 86 | case "", StyleForm, StyleSpaceDelimited, StylePipeDelimited, StyleDeepObject: 87 | default: 88 | errs = append(errs, newValidationError(joinLoc(location, "style"), "invalid value, expected one of [%s, %s, %s, %s], but got '%s'", StyleForm, StyleSpaceDelimited, StylePipeDelimited, StyleDeepObject, o.Style)) 89 | } 90 | return errs 91 | } 92 | 93 | type EncodingBuilder struct { 94 | spec *Extendable[Encoding] 95 | } 96 | 97 | func NewEncodingBuilder() *EncodingBuilder { 98 | return &EncodingBuilder{ 99 | spec: NewExtendable[Encoding](&Encoding{}), 100 | } 101 | } 102 | 103 | func (b *EncodingBuilder) Build() *Extendable[Encoding] { 104 | return b.spec 105 | } 106 | 107 | func (b *EncodingBuilder) Extensions(v map[string]any) *EncodingBuilder { 108 | b.spec.Extensions = v 109 | return b 110 | } 111 | 112 | func (b *EncodingBuilder) AddExt(name string, value any) *EncodingBuilder { 113 | b.spec.AddExt(name, value) 114 | return b 115 | } 116 | 117 | func (b *EncodingBuilder) ContentType(v string) *EncodingBuilder { 118 | b.spec.Spec.ContentType = v 119 | return b 120 | } 121 | 122 | func (b *EncodingBuilder) Headers(v map[string]*RefOrSpec[Extendable[Header]]) *EncodingBuilder { 123 | b.spec.Spec.Headers = v 124 | return b 125 | } 126 | 127 | func (b *EncodingBuilder) Header(name string, value *RefOrSpec[Extendable[Header]]) *EncodingBuilder { 128 | if b.spec.Spec.Headers == nil { 129 | b.spec.Spec.Headers = make(map[string]*RefOrSpec[Extendable[Header]], 1) 130 | } 131 | b.spec.Spec.Headers[name] = value 132 | return b 133 | } 134 | 135 | func (b *EncodingBuilder) Style(v string) *EncodingBuilder { 136 | b.spec.Spec.Style = v 137 | return b 138 | } 139 | 140 | func (b *EncodingBuilder) Explode(v bool) *EncodingBuilder { 141 | b.spec.Spec.Explode = v 142 | return b 143 | } 144 | 145 | func (b *EncodingBuilder) AllowReserved(v bool) *EncodingBuilder { 146 | b.spec.Spec.AllowReserved = v 147 | return b 148 | } 149 | -------------------------------------------------------------------------------- /example.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Example is expected to be compatible with the type schema of its associated value. 4 | // Tooling implementations MAY choose to validate compatibility automatically, and reject the example value(s) if incompatible. 5 | // 6 | // https://spec.openapis.org/oas/v3.1.1#example-object 7 | // 8 | // Example: 9 | // 10 | // requestBody: 11 | // content: 12 | // 'application/json': 13 | // schema: 14 | // $ref: '#/components/schemas/Address' 15 | // examples: 16 | // foo: 17 | // summary: A foo example 18 | // value: {"foo": "bar"} 19 | // bar: 20 | // summary: A bar example 21 | // value: {"bar": "baz"} 22 | // 'application/xml': 23 | // examples: 24 | // xmlExample: 25 | // summary: This is an example in XML 26 | // externalValue: 'https://example.org/examples/address-example.xml' 27 | // 'text/plain': 28 | // examples: 29 | // textExample: 30 | // summary: This is a text example 31 | // externalValue: 'https://foo.bar/examples/address-example.txt' 32 | type Example struct { 33 | // Short description for the example. 34 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 35 | // Long description for the example. 36 | // CommonMark syntax MAY be used for rich text representation. 37 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 38 | // Embedded literal example. 39 | // The value field and externalValue field are mutually exclusive. 40 | // To represent examples of media types that cannot naturally represented in JSON or YAML, 41 | // use a string value to contain the example, escaping where necessary. 42 | Value any `json:"value,omitempty" yaml:"value,omitempty"` 43 | // A URI that points to the literal example. 44 | // This provides the capability to reference examples that cannot easily be included in JSON or YAML documents. 45 | // The value field and externalValue field are mutually exclusive. 46 | // See the rules for resolving Relative References. 47 | ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` 48 | } 49 | 50 | func (o *Example) validateSpec(location string, _ *Validator) []*validationError { 51 | var errs []*validationError 52 | if o.Value != nil && o.ExternalValue != "" { 53 | errs = append(errs, newValidationError(joinLoc(location, "value&externalValue"), ErrMutuallyExclusive)) 54 | } 55 | if err := checkURL(o.ExternalValue); err != nil { 56 | errs = append(errs, newValidationError(joinLoc(location, "externalValue"), err)) 57 | } 58 | // no validation of Value field, because it needs a schema and 59 | // should be validated in the object that defines the example and a schema 60 | return errs 61 | } 62 | 63 | type ExampleBuilder struct { 64 | spec *RefOrSpec[Extendable[Example]] 65 | } 66 | 67 | func NewExampleBuilder() *ExampleBuilder { 68 | return &ExampleBuilder{ 69 | spec: NewRefOrExtSpec[Example](&Example{}), 70 | } 71 | } 72 | 73 | func (b *ExampleBuilder) Build() *RefOrSpec[Extendable[Example]] { 74 | return b.spec 75 | } 76 | 77 | func (b *ExampleBuilder) Extensions(v map[string]any) *ExampleBuilder { 78 | b.spec.Spec.Extensions = v 79 | return b 80 | } 81 | 82 | func (b *ExampleBuilder) AddExt(name string, value any) *ExampleBuilder { 83 | b.spec.Spec.AddExt(name, value) 84 | return b 85 | } 86 | 87 | func (b *ExampleBuilder) Summary(v string) *ExampleBuilder { 88 | b.spec.Spec.Spec.Summary = v 89 | return b 90 | } 91 | 92 | func (b *ExampleBuilder) Description(v string) *ExampleBuilder { 93 | b.spec.Spec.Spec.Description = v 94 | return b 95 | } 96 | 97 | func (b *ExampleBuilder) Value(v any) *ExampleBuilder { 98 | b.spec.Spec.Spec.Value = v 99 | return b 100 | } 101 | 102 | func (b *ExampleBuilder) ExternalValue(v string) *ExampleBuilder { 103 | b.spec.Spec.Spec.ExternalValue = v 104 | return b 105 | } 106 | -------------------------------------------------------------------------------- /extensions.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | const ExtensionPrefix = "x-" 13 | 14 | // Extendable allows extensions to the OpenAPI Schema. 15 | // The field name MUST begin with `x-`, for example, `x-internal-id`. 16 | // Field names beginning `x-oai-` and `x-oas-` are reserved for uses defined by the OpenAPI Initiative. 17 | // The value can be null, a primitive, an array or an object. 18 | // 19 | // https://spec.openapis.org/oas/v3.1.1#specification-extensions 20 | // 21 | // Example: 22 | // 23 | // openapi: 3.1.1 24 | // info: 25 | // title: Sample Pet Store App 26 | // summary: A pet store manager. 27 | // description: This is a sample server for a pet store. 28 | // version: 1.0.1 29 | // x-build-data: 2006-01-02T15:04:05Z07:00 30 | // x-build-commit-id: dac33af14d0d4a5f1c226141042ca7cefc6aeb75 31 | type Extendable[T any] struct { 32 | Spec *T `json:"-" yaml:"-"` 33 | Extensions map[string]any `json:"-" yaml:"-"` 34 | } 35 | 36 | // NewExtendable creates new Extendable object for given spec 37 | func NewExtendable[T any](spec *T) *Extendable[T] { 38 | ext := Extendable[T]{ 39 | Spec: spec, 40 | Extensions: make(map[string]any), 41 | } 42 | return &ext 43 | } 44 | 45 | // AddExt sets the extension and returns the current object. 46 | // The `x-` prefix will be added automatically to given name. 47 | func (o *Extendable[T]) AddExt(name string, value any) *Extendable[T] { 48 | if o.Extensions == nil { 49 | o.Extensions = make(map[string]any, 1) 50 | } 51 | if !strings.HasPrefix(name, ExtensionPrefix) { 52 | name = ExtensionPrefix + name 53 | } 54 | o.Extensions[name] = value 55 | return o 56 | } 57 | 58 | // GetExt returns the extension value by name. 59 | // The `x-` prefix will be added automatically to given name. 60 | func (o *Extendable[T]) GetExt(name string) any { 61 | if o.Extensions == nil { 62 | return nil 63 | } 64 | if !strings.HasPrefix(name, ExtensionPrefix) { 65 | name = ExtensionPrefix + name 66 | } 67 | return o.Extensions[name] 68 | } 69 | 70 | // MarshalJSON implements json.Marshaler interface. 71 | func (o *Extendable[T]) MarshalJSON() ([]byte, error) { 72 | var raw map[string]json.RawMessage 73 | exts, err := json.Marshal(&o.Extensions) 74 | if err != nil { 75 | return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err) 76 | } 77 | if err := json.Unmarshal(exts, &raw); err != nil { 78 | return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err) 79 | } 80 | fields, err := json.Marshal(&o.Spec) 81 | if err != nil { 82 | return nil, fmt.Errorf("%T: %w", o.Spec, err) 83 | } 84 | if err := json.Unmarshal(fields, &raw); err != nil { 85 | return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err) 86 | } 87 | data, err := json.Marshal(&raw) 88 | if err != nil { 89 | return nil, fmt.Errorf("%T(raw): %w", o.Spec, err) 90 | } 91 | return data, nil 92 | } 93 | 94 | // UnmarshalJSON implements json.Unmarshaler interface. 95 | func (o *Extendable[T]) UnmarshalJSON(data []byte) error { 96 | var raw map[string]json.RawMessage 97 | if err := json.Unmarshal(data, &raw); err != nil { 98 | return fmt.Errorf("%T: %w", o.Spec, err) 99 | } 100 | o.Extensions = make(map[string]any) 101 | for name, value := range raw { 102 | if strings.HasPrefix(name, ExtensionPrefix) { 103 | var v any 104 | if err := json.Unmarshal(value, &v); err != nil { 105 | return fmt.Errorf("%T.Extensions.%s: %w", o.Spec, name, err) 106 | } 107 | o.Extensions[name] = v 108 | delete(raw, name) 109 | } 110 | } 111 | fields, err := json.Marshal(&raw) 112 | if err != nil { 113 | return fmt.Errorf("%T(raw): %w", o.Spec, err) 114 | } 115 | if err := json.Unmarshal(fields, &o.Spec); err != nil { 116 | return fmt.Errorf("%T: %w", o.Spec, err) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // MarshalYAML implements yaml.Marshaler interface. 123 | func (o *Extendable[T]) MarshalYAML() (any, error) { 124 | var raw map[string]any 125 | exts, err := yaml.Marshal(&o.Extensions) 126 | if err != nil { 127 | return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err) 128 | } 129 | if err := yaml.Unmarshal(exts, &raw); err != nil { 130 | return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err) 131 | } 132 | fields, err := yaml.Marshal(&o.Spec) 133 | if err != nil { 134 | return nil, fmt.Errorf("%T: %w", o.Spec, err) 135 | } 136 | if err := yaml.Unmarshal(fields, &raw); err != nil { 137 | return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err) 138 | } 139 | return raw, nil 140 | } 141 | 142 | // UnmarshalYAML implements yaml.Unmarshaler interface. 143 | func (o *Extendable[T]) UnmarshalYAML(node *yaml.Node) error { 144 | var raw map[string]any 145 | if err := node.Decode(&raw); err != nil { 146 | return fmt.Errorf("%T: %w", o.Spec, err) 147 | } 148 | o.Extensions = make(map[string]any) 149 | for name, value := range raw { 150 | if strings.HasPrefix(name, ExtensionPrefix) { 151 | o.Extensions[name] = value 152 | delete(raw, name) 153 | } 154 | } 155 | fields, err := yaml.Marshal(&raw) 156 | if err != nil { 157 | return fmt.Errorf("%T(raw): %w", o.Spec, err) 158 | } 159 | if err := yaml.Unmarshal(fields, &o.Spec); err != nil { 160 | return fmt.Errorf("%T: %w", o.Spec, err) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | var ErrExtensionNameMustStartWithPrefix = errors.New("extension name must start with `" + ExtensionPrefix + "`") 167 | 168 | const unsupportedSpecTypePrefix = "unsupported spec type: " 169 | 170 | type UnsupportedSpecTypeError string 171 | 172 | func (e UnsupportedSpecTypeError) Error() string { 173 | return unsupportedSpecTypePrefix + string(e) 174 | } 175 | 176 | func (e UnsupportedSpecTypeError) Is(target error) bool { 177 | return strings.HasPrefix(target.Error(), unsupportedSpecTypePrefix) 178 | } 179 | 180 | func NewUnsupportedSpecTypeError(spec any) error { 181 | return UnsupportedSpecTypeError(fmt.Sprintf("%T", spec)) 182 | } 183 | 184 | func (o *Extendable[T]) validateSpec(location string, validator *Validator) []*validationError { 185 | var errs []*validationError 186 | if o.Spec != nil { 187 | if spec, ok := any(o.Spec).(validatable); ok { 188 | errs = append(errs, spec.validateSpec(location, validator)...) 189 | } else { 190 | errs = append(errs, newValidationError(location, NewUnsupportedSpecTypeError(o.Spec))) 191 | } 192 | } 193 | if validator.opts.allowExtensionNameWithoutPrefix { 194 | return errs 195 | } 196 | 197 | for name := range o.Extensions { 198 | if !strings.HasPrefix(name, ExtensionPrefix) { 199 | errs = append(errs, newValidationError(joinLoc(location, name), ErrExtensionNameMustStartWithPrefix)) 200 | } 201 | } 202 | return errs 203 | } 204 | -------------------------------------------------------------------------------- /extensions_test.go: -------------------------------------------------------------------------------- 1 | package openapi_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/sv-tools/openapi" 11 | ) 12 | 13 | type testExtendable struct { 14 | A string `json:"a,omitempty" yaml:"a,omitempty"` 15 | } 16 | 17 | func TestExtendable_Marshal_Unmarshal(t *testing.T) { 18 | for _, tt := range []struct { 19 | name string 20 | data string 21 | expected string 22 | emptyExtensions bool 23 | }{ 24 | { 25 | name: "spec only", 26 | data: `{"a": "foo"}`, 27 | emptyExtensions: true, 28 | }, 29 | { 30 | name: "spec with extra non extension field", 31 | data: `{"a": "foo", "b": "bar"}`, 32 | expected: `{"a": "foo"}`, 33 | emptyExtensions: true, 34 | }, 35 | { 36 | name: "spec with extension field", 37 | data: `{"a": "foo", "x-b": "bar"}`, 38 | emptyExtensions: false, 39 | }, 40 | } { 41 | t.Run(tt.name, func(t *testing.T) { 42 | t.Run("json", func(t *testing.T) { 43 | var v *openapi.Extendable[testExtendable] 44 | require.NoError(t, json.Unmarshal([]byte(tt.data), &v)) 45 | if tt.emptyExtensions { 46 | require.Empty(t, v.Extensions) 47 | } else { 48 | require.NotEmpty(t, v.Extensions) 49 | } 50 | data, err := json.Marshal(&v) 51 | require.NoError(t, err) 52 | if tt.expected == "" { 53 | tt.expected = tt.data 54 | } 55 | require.JSONEq(t, tt.expected, string(data)) 56 | }) 57 | t.Run("yaml", func(t *testing.T) { 58 | var v *openapi.Extendable[testExtendable] 59 | require.NoError(t, yaml.Unmarshal([]byte(tt.data), &v)) 60 | if tt.emptyExtensions { 61 | require.Empty(t, v.Extensions) 62 | } else { 63 | require.NotEmpty(t, v.Extensions) 64 | } 65 | data, err := yaml.Marshal(&v) 66 | require.NoError(t, err) 67 | if tt.expected == "" { 68 | tt.expected = tt.data 69 | } 70 | require.YAMLEq(t, tt.expected, string(data)) 71 | }) 72 | }) 73 | } 74 | } 75 | 76 | func TestExtendable_WithExt(t *testing.T) { 77 | for _, tt := range []struct { 78 | name string 79 | key string 80 | value any 81 | expected map[string]any 82 | }{ 83 | { 84 | name: "without prefix", 85 | key: "foo", 86 | value: 42, 87 | expected: map[string]any{ 88 | "x-foo": 42, 89 | }, 90 | }, 91 | { 92 | name: "with prefix", 93 | key: "x-foo", 94 | value: 43, 95 | expected: map[string]any{ 96 | "x-foo": 43, 97 | }, 98 | }, 99 | } { 100 | t.Run(tt.name, func(t *testing.T) { 101 | ext := openapi.NewExtendable(&testExtendable{}) 102 | ext.AddExt(tt.key, tt.value) 103 | require.Equal(t, tt.expected, ext.Extensions) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /external-docs.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // ExternalDocs allows referencing an external resource for extended documentation. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#external-documentation-object 6 | // 7 | // Example: 8 | // 9 | // description: Find more info here 10 | // url: https://example.com 11 | type ExternalDocs struct { 12 | // A description of the target documentation. 13 | // CommonMark syntax MAY be used for rich text representation. 14 | Description string `json:"description" yaml:"description"` 15 | // REQUIRED. 16 | // The URL for the target documentation. 17 | // This MUST be in the form of a URL. 18 | URL string `json:"url" yaml:"url"` 19 | } 20 | 21 | func (o *ExternalDocs) validateSpec(location string, _ *Validator) []*validationError { 22 | var errs []*validationError 23 | if o.URL == "" { 24 | errs = append(errs, newValidationError(joinLoc(location, "url"), ErrRequired)) 25 | } 26 | if err := checkURL(o.URL); err != nil { 27 | errs = append(errs, newValidationError(joinLoc(location, "url"), err)) 28 | } 29 | return errs 30 | } 31 | 32 | type ExternalDocsBuilder struct { 33 | spec *Extendable[ExternalDocs] 34 | } 35 | 36 | func NewExternalDocsBuilder() *ExternalDocsBuilder { 37 | return &ExternalDocsBuilder{ 38 | spec: NewExtendable[ExternalDocs](&ExternalDocs{}), 39 | } 40 | } 41 | 42 | func (b *ExternalDocsBuilder) Build() *Extendable[ExternalDocs] { 43 | return b.spec 44 | } 45 | 46 | func (b *ExternalDocsBuilder) Extensions(v map[string]any) *ExternalDocsBuilder { 47 | b.spec.Extensions = v 48 | return b 49 | } 50 | 51 | func (b *ExternalDocsBuilder) AddExt(name string, value any) *ExternalDocsBuilder { 52 | b.spec.AddExt(name, value) 53 | return b 54 | } 55 | 56 | func (b *ExternalDocsBuilder) Description(v string) *ExternalDocsBuilder { 57 | b.spec.Spec.Description = v 58 | return b 59 | } 60 | 61 | func (b *ExternalDocsBuilder) URL(v string) *ExternalDocsBuilder { 62 | b.spec.Spec.URL = v 63 | return b 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sv-tools/openapi 2 | 3 | go 1.22.0 4 | 5 | retract v0.3.0 // due to a mistake, there is no real v0.3.0 release, it was pointed to v0.2.2 tag 6 | 7 | require ( 8 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 9 | github.com/stretchr/testify v1.10.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/text v0.14.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 4 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= 8 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= 9 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 10 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 12 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Header Object follows the structure of the Parameter Object with the some changes. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#header-object 6 | // 7 | // Example: 8 | // 9 | // description: The number of allowed requests in the current period 10 | // schema: 11 | // type: integer 12 | // 13 | // All fields are copied from Parameter Object as is, except name and in fields. 14 | type Header struct { 15 | // The schema defining the type used for the header. 16 | Schema *RefOrSpec[Schema] `json:"schema,omitempty" yaml:"schema,omitempty"` 17 | // A map containing the representations for the header. 18 | // The key is the media type and the value describes it. 19 | // The map MUST only contain one entry. 20 | Content map[string]*Extendable[MediaType] `json:"content,omitempty" yaml:"content,omitempty"` 21 | // A brief description of the header. 22 | // This could contain examples of use. 23 | // CommonMark syntax MAY be used for rich text representation. 24 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 25 | // Describes how the header value will be serialized. 26 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 27 | // When this is true, header values of type array or object generate separate headers 28 | // for each value of the array or key-value pair of the map. 29 | // For other types of parameters this property has no effect. 30 | // When style is form, the default value is true. 31 | // For all other styles, the default value is false. 32 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 33 | // Determines whether this header is mandatory. 34 | // The property MAY be included and its default value is false. 35 | Required bool `json:"required,omitempty" yaml:"required,omitempty"` 36 | // Specifies that a header is deprecated and SHOULD be transitioned out of usage. 37 | // Default value is false. 38 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 39 | } 40 | 41 | func (o *Header) validateSpec(location string, validator *Validator) []*validationError { 42 | var errs []*validationError 43 | if o.Schema != nil && o.Content != nil { 44 | errs = append(errs, newValidationError(joinLoc(location, "schema&content"), ErrMutuallyExclusive)) 45 | } 46 | 47 | if l := len(o.Content); l > 0 { 48 | if l != 1 { 49 | errs = append(errs, newValidationError(joinLoc(location, "content"), "must be only one item, but got '%d'", l)) 50 | } 51 | for k, v := range o.Content { 52 | errs = append(errs, v.validateSpec(joinLoc(location, "content", k), validator)...) 53 | } 54 | } 55 | if o.Schema != nil { 56 | errs = append(errs, o.Schema.validateSpec(joinLoc(location, "schema"), validator)...) 57 | } 58 | 59 | switch o.Style { 60 | case "", StyleSimple: 61 | default: 62 | errs = append(errs, newValidationError(joinLoc(location, "style"), "invalid value, expected one of [%s], but got '%s'", StyleSimple, o.Style)) 63 | } 64 | 65 | return errs 66 | } 67 | 68 | type HeaderBuilder struct { 69 | spec *RefOrSpec[Extendable[Header]] 70 | } 71 | 72 | func NewHeaderBuilder() *HeaderBuilder { 73 | return &HeaderBuilder{ 74 | spec: NewRefOrExtSpec[Header](&Header{}), 75 | } 76 | } 77 | 78 | func (b *HeaderBuilder) Build() *RefOrSpec[Extendable[Header]] { 79 | return b.spec 80 | } 81 | 82 | func (b *HeaderBuilder) Extensions(v map[string]any) *HeaderBuilder { 83 | b.spec.Spec.Extensions = v 84 | return b 85 | } 86 | 87 | func (b *HeaderBuilder) AddExt(name string, value any) *HeaderBuilder { 88 | b.spec.Spec.AddExt(name, value) 89 | return b 90 | } 91 | 92 | func (b *HeaderBuilder) Schema(v *RefOrSpec[Schema]) *HeaderBuilder { 93 | b.spec.Spec.Spec.Schema = v 94 | return b 95 | } 96 | 97 | func (b *HeaderBuilder) Content(v map[string]*Extendable[MediaType]) *HeaderBuilder { 98 | b.spec.Spec.Spec.Content = v 99 | return b 100 | } 101 | 102 | func (b *HeaderBuilder) AddContent(name string, value *Extendable[MediaType]) *HeaderBuilder { 103 | if b.spec.Spec.Spec.Content == nil { 104 | b.spec.Spec.Spec.Content = make(map[string]*Extendable[MediaType], 1) 105 | } 106 | b.spec.Spec.Spec.Content[name] = value 107 | return b 108 | } 109 | 110 | func (b *HeaderBuilder) Description(v string) *HeaderBuilder { 111 | b.spec.Spec.Spec.Description = v 112 | return b 113 | } 114 | 115 | func (b *HeaderBuilder) Style(v string) *HeaderBuilder { 116 | b.spec.Spec.Spec.Style = v 117 | return b 118 | } 119 | 120 | func (b *HeaderBuilder) Explode(v bool) *HeaderBuilder { 121 | b.spec.Spec.Spec.Explode = v 122 | return b 123 | } 124 | 125 | func (b *HeaderBuilder) Required(v bool) *HeaderBuilder { 126 | b.spec.Spec.Spec.Required = v 127 | return b 128 | } 129 | 130 | func (b *HeaderBuilder) Deprecated(v bool) *HeaderBuilder { 131 | b.spec.Spec.Spec.Deprecated = v 132 | return b 133 | } 134 | -------------------------------------------------------------------------------- /info.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Info provides metadata about the API. 4 | // The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience. 5 | // 6 | // https://spec.openapis.org/oas/v3.1.1#info-object 7 | // 8 | // Example: 9 | // 10 | // title: Sample Pet Store App 11 | // summary: A pet store manager. 12 | // description: This is a sample server for a pet store. 13 | // termsOfService: https://example.com/terms/ 14 | // contact: 15 | // name: API Support 16 | // url: https://www.example.com/support 17 | // email: support@example.com 18 | // license: 19 | // name: Apache 2.0 20 | // url: https://www.apache.org/licenses/LICENSE-2.0.html 21 | // version: 1.0.1 22 | type Info struct { 23 | // REQUIRED. 24 | // The title of the API. 25 | Title string `json:"title" yaml:"title"` 26 | // A short summary of the API. 27 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 28 | // A description of the API. 29 | // CommonMark syntax MAY be used for rich text representation. 30 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 31 | // A URL to the Terms of Service for the API. 32 | // This MUST be in the form of a URL. 33 | TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` 34 | // The contact information for the exposed API. 35 | Contact *Extendable[Contact] `json:"contact,omitempty" yaml:"contact,omitempty"` 36 | // The license information for the exposed API. 37 | License *Extendable[License] `json:"license,omitempty" yaml:"license,omitempty"` 38 | // REQUIRED. 39 | // The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version). 40 | Version string `json:"version" yaml:"version"` 41 | } 42 | 43 | func (o *Info) validateSpec(location string, validator *Validator) []*validationError { 44 | var errs []*validationError 45 | if o.Title == "" { 46 | errs = append(errs, newValidationError(joinLoc(location, "title"), ErrRequired)) 47 | } 48 | if o.Version == "" { 49 | errs = append(errs, newValidationError(joinLoc(location, "version"), ErrRequired)) 50 | } 51 | if o.Contact != nil { 52 | errs = append(errs, o.Contact.validateSpec(joinLoc(location, "contact"), validator)...) 53 | } 54 | if o.License != nil { 55 | errs = append(errs, o.License.validateSpec(joinLoc(location, "license"), validator)...) 56 | } 57 | if err := checkURL(o.TermsOfService); err != nil { 58 | errs = append(errs, newValidationError(joinLoc(location, "termsOfService"), err)) 59 | } 60 | return errs 61 | } 62 | 63 | type InfoBuilder struct { 64 | spec *Extendable[Info] 65 | } 66 | 67 | func NewInfoBuilder() *InfoBuilder { 68 | return &InfoBuilder{ 69 | spec: NewExtendable[Info](&Info{}), 70 | } 71 | } 72 | 73 | func (b *InfoBuilder) Build() *Extendable[Info] { 74 | return b.spec 75 | } 76 | 77 | func (b *InfoBuilder) Extensions(v map[string]any) *InfoBuilder { 78 | b.spec.Extensions = v 79 | return b 80 | } 81 | 82 | func (b *InfoBuilder) AddExt(name string, value any) *InfoBuilder { 83 | b.spec.AddExt(name, value) 84 | return b 85 | } 86 | 87 | func (b *InfoBuilder) Title(v string) *InfoBuilder { 88 | b.spec.Spec.Title = v 89 | return b 90 | } 91 | 92 | func (b *InfoBuilder) Summary(v string) *InfoBuilder { 93 | b.spec.Spec.Summary = v 94 | return b 95 | } 96 | 97 | func (b *InfoBuilder) Description(v string) *InfoBuilder { 98 | b.spec.Spec.Description = v 99 | return b 100 | } 101 | 102 | func (b *InfoBuilder) TermsOfService(v string) *InfoBuilder { 103 | b.spec.Spec.TermsOfService = v 104 | return b 105 | } 106 | 107 | func (b *InfoBuilder) Contact(v *Extendable[Contact]) *InfoBuilder { 108 | b.spec.Spec.Contact = v 109 | return b 110 | } 111 | 112 | func (b *InfoBuilder) License(v *Extendable[License]) *InfoBuilder { 113 | b.spec.Spec.License = v 114 | return b 115 | } 116 | 117 | func (b *InfoBuilder) Version(v string) *InfoBuilder { 118 | b.spec.Spec.Version = v 119 | return b 120 | } 121 | -------------------------------------------------------------------------------- /license.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // License information for the exposed API. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#license-object 6 | // 7 | // Example: 8 | // 9 | // name: Apache 2.0 10 | // identifier: Apache-2.0 11 | type License struct { 12 | // REQUIRED. 13 | // The license name used for the API. 14 | Name string `json:"name" yaml:"name"` 15 | // An SPDX license expression for the API. 16 | // The identifier field is mutually exclusive of the url field. 17 | Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` 18 | // A URL to the license used for the API. 19 | // This MUST be in the form of a URL. 20 | // The url field is mutually exclusive of the identifier field. 21 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 22 | } 23 | 24 | func (o *License) validateSpec(location string, _ *Validator) []*validationError { 25 | var errs []*validationError 26 | if o.Name == "" { 27 | errs = append(errs, newValidationError(joinLoc(location, "name"), ErrRequired)) 28 | } 29 | if o.Identifier != "" && o.URL != "" { 30 | errs = append(errs, newValidationError(joinLoc(location, "identifier&url"), ErrMutuallyExclusive)) 31 | } 32 | if err := checkURL(o.URL); err != nil { 33 | errs = append(errs, newValidationError(joinLoc(location, "url"), err)) 34 | } 35 | return errs 36 | } 37 | 38 | type LicenseBuilder struct { 39 | spec *Extendable[License] 40 | } 41 | 42 | func NewLicenseBuilder() *LicenseBuilder { 43 | return &LicenseBuilder{ 44 | spec: NewExtendable[License](&License{}), 45 | } 46 | } 47 | 48 | func (b *LicenseBuilder) Build() *Extendable[License] { 49 | return b.spec 50 | } 51 | 52 | func (b *LicenseBuilder) Extensions(v map[string]any) *LicenseBuilder { 53 | b.spec.Extensions = v 54 | return b 55 | } 56 | 57 | func (b *LicenseBuilder) AddExt(name string, value any) *LicenseBuilder { 58 | b.spec.AddExt(name, value) 59 | return b 60 | } 61 | 62 | func (b *LicenseBuilder) Name(v string) *LicenseBuilder { 63 | b.spec.Spec.Name = v 64 | return b 65 | } 66 | 67 | func (b *LicenseBuilder) Identifier(v string) *LicenseBuilder { 68 | b.spec.Spec.Identifier = v 69 | return b 70 | } 71 | 72 | func (b *LicenseBuilder) URL(v string) *LicenseBuilder { 73 | b.spec.Spec.URL = v 74 | return b 75 | } 76 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Link represents a possible design-time link for a response. 4 | // The presence of a link does not guarantee the caller’s ability to successfully invoke it, 5 | // rather it provides a known relationship and traversal mechanism between responses and other operations. 6 | // Unlike dynamic links (i.e. links provided in the response payload), 7 | // the OAS linking mechanism does not require link information in the runtime response. 8 | // For computing links, and providing instructions to execute them, 9 | // a runtime expression is used for accessing values in an operation 10 | // and using them as parameters while invoking the linked operation. 11 | // 12 | // https://spec.openapis.org/oas/v3.1.1#link-object 13 | // 14 | // Example: 15 | // 16 | // paths: 17 | // /users/{id}: 18 | // parameters: 19 | // - name: id 20 | // in: path 21 | // required: true 22 | // description: the user identifier, as userId 23 | // schema: 24 | // type: string 25 | // get: 26 | // responses: 27 | // '200': 28 | // description: the user being returned 29 | // content: 30 | // application/json: 31 | // schema: 32 | // type: object 33 | // properties: 34 | // uuid: # the unique user id 35 | // type: string 36 | // format: uuid 37 | // links: 38 | // address: 39 | // # the target link operationId 40 | // operationId: getUserAddress 41 | // parameters: 42 | // # get the `id` field from the request path parameter named `id` 43 | // userId: $request.path.id 44 | // # the path item of the linked operation 45 | // /users/{userid}/address: 46 | // parameters: 47 | // - name: userid 48 | // in: path 49 | // required: true 50 | // description: the user identifier, as userId 51 | // schema: 52 | // type: string 53 | // # linked operation 54 | // get: 55 | // operationId: getUserAddress 56 | // responses: 57 | // '200': 58 | // description: the user's address 59 | type Link struct { 60 | // A literal value or {expression} to use as a request body when calling the target operation. 61 | RequestBody any `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` 62 | // A map representing parameters to pass to an operation as specified with operationId or identified via operationRef. 63 | // The key is the parameter name to be used, whereas the value can be a constant or an expression to be evaluated and 64 | // passed to the linked operation. 65 | // The parameter name can be qualified using the parameter location [{in}.]{name} for operations that use 66 | // the same parameter name in different locations (e.g. path.id). 67 | Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` 68 | // A server object to be used by the target operation. 69 | Server *Extendable[Server] `json:"server,omitempty" yaml:"server,omitempty"` 70 | // A relative or absolute URI reference to an OAS operation. 71 | // This field is mutually exclusive of the operationId field, and MUST point to an Operation Object. 72 | // Relative operationRef values MAY be used to locate an existing Operation Object in the OpenAPI definition. 73 | // See the rules for resolving Relative References. 74 | OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` 75 | // The name of an existing, resolvable OAS operation, as defined with a unique operationId. 76 | // This field is mutually exclusive of the operationRef field. 77 | OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` 78 | // A description of the link. 79 | // CommonMark syntax MAY be used for rich text representation. 80 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 81 | } 82 | 83 | func (o *Link) validateSpec(location string, validator *Validator) []*validationError { 84 | var errs []*validationError 85 | if o.OperationRef != "" && o.OperationID != "" { 86 | errs = append(errs, newValidationError(joinLoc(location, "operationRef&operationId"), ErrMutuallyExclusive)) 87 | } 88 | if o.OperationID != "" { 89 | id := joinLoc("operations", o.OperationID) 90 | if !validator.visited[id] { 91 | validator.linkToOperationID[joinLoc(location, "operationId")] = o.OperationID 92 | } 93 | } 94 | // uncomment when JSONLookup is implemented 95 | // if o.OperationRef != "" { 96 | // ref := NewRefOrExtSpec[Operation](o.OperationRef) 97 | // errs = append(errs, ref.validateSpec(joinLoc(location, "operationRef"), validator)...) 98 | //} 99 | if o.Server != nil { 100 | errs = append(errs, o.Server.validateSpec(joinLoc(location, "server"), validator)...) 101 | } 102 | return errs 103 | } 104 | 105 | type LinkBuilder struct { 106 | spec *RefOrSpec[Extendable[Link]] 107 | } 108 | 109 | func NewLinkBuilder() *LinkBuilder { 110 | return &LinkBuilder{ 111 | spec: NewRefOrExtSpec[Link](&Link{}), 112 | } 113 | } 114 | 115 | func (b *LinkBuilder) Build() *RefOrSpec[Extendable[Link]] { 116 | return b.spec 117 | } 118 | 119 | func (b *LinkBuilder) Extensions(v map[string]any) *LinkBuilder { 120 | b.spec.Spec.Extensions = v 121 | return b 122 | } 123 | 124 | func (b *LinkBuilder) AddExt(name string, value any) *LinkBuilder { 125 | b.spec.Spec.AddExt(name, value) 126 | return b 127 | } 128 | 129 | func (b *LinkBuilder) RequestBody(v any) *LinkBuilder { 130 | b.spec.Spec.Spec.RequestBody = v 131 | return b 132 | } 133 | 134 | func (b *LinkBuilder) Parameters(v map[string]any) *LinkBuilder { 135 | b.spec.Spec.Spec.Parameters = v 136 | return b 137 | } 138 | 139 | func (b *LinkBuilder) AddParameter(name string, value any) *LinkBuilder { 140 | if b.spec.Spec.Spec.Parameters == nil { 141 | b.spec.Spec.Spec.Parameters = make(map[string]any, 1) 142 | } 143 | b.spec.Spec.Spec.Parameters[name] = value 144 | return b 145 | } 146 | 147 | func (b *LinkBuilder) Server(v *Extendable[Server]) *LinkBuilder { 148 | b.spec.Spec.Spec.Server = v 149 | return b 150 | } 151 | 152 | func (b *LinkBuilder) OperationRef(v string) *LinkBuilder { 153 | b.spec.Spec.Spec.OperationRef = v 154 | return b 155 | } 156 | 157 | func (b *LinkBuilder) OperationID(v string) *LinkBuilder { 158 | b.spec.Spec.Spec.OperationID = v 159 | return b 160 | } 161 | 162 | func (b *LinkBuilder) Description(v string) *LinkBuilder { 163 | b.spec.Spec.Spec.Description = v 164 | return b 165 | } 166 | -------------------------------------------------------------------------------- /media_type.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // MediaType provides schema and examples for the media type identified by its key. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#media-type-object 6 | // 7 | // Example: 8 | // 9 | // application/json: 10 | // schema: 11 | // $ref: "#/components/schemas/Pet" 12 | // examples: 13 | // cat: 14 | // summary: An example of a cat 15 | // value: 16 | // name: Fluffy 17 | // petType: Cat 18 | // color: White 19 | // gender: male 20 | // breed: Persian 21 | // dog: 22 | // summary: An example of a dog with a cat's name 23 | // value: 24 | // name: Puma 25 | // petType: Dog 26 | // color: Black 27 | // gender: Female 28 | // breed: Mixed 29 | // frog: 30 | // $ref: "#/components/examples/frog-example" 31 | type MediaType struct { 32 | // The schema defining the content of the request, response, or parameter. 33 | Schema *RefOrSpec[Schema] `json:"schema,omitempty" yaml:"schema,omitempty"` 34 | // Example of the media type. The example object SHOULD be in the correct format as specified by the media type. 35 | // The example field is mutually exclusive of the examples field. 36 | // Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. 37 | Example any `json:"example,omitempty" yaml:"example,omitempty"` 38 | // Examples of the parameter’s potential value. 39 | // Each example SHOULD contain a value in the correct format as specified in the parameter encoding. 40 | // The examples field is mutually exclusive of the example field. 41 | // Furthermore, if referencing a schema that contains an example, the examples value SHALL override the example provided by the schema. 42 | Examples map[string]*RefOrSpec[Extendable[Example]] `json:"examples,omitempty" yaml:"examples,omitempty"` 43 | // A map between a property name and its encoding information. 44 | // The key, being the property name, MUST exist in the schema as a property. 45 | // The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded. 46 | Encoding map[string]*Extendable[Encoding] `json:"encoding,omitempty" yaml:"encoding,omitempty"` 47 | } 48 | 49 | func (o *MediaType) validateSpec(location string, validator *Validator) []*validationError { 50 | var errs []*validationError 51 | if o.Schema != nil { 52 | errs = append(errs, o.Schema.validateSpec(joinLoc(location, "schema"), validator)...) 53 | } 54 | if len(o.Encoding) > 0 { 55 | for k, v := range o.Encoding { 56 | errs = append(errs, v.validateSpec(joinLoc(location, "encoding", k), validator)...) 57 | } 58 | } 59 | if o.Example != nil && len(o.Examples) > 0 { 60 | errs = append(errs, newValidationError(joinLoc(location, "example&examples"), ErrMutuallyExclusive)) 61 | } 62 | if len(o.Examples) > 0 { 63 | for k, v := range o.Examples { 64 | errs = append(errs, v.validateSpec(joinLoc(location, "examples", k), validator)...) 65 | } 66 | } 67 | 68 | if validator.opts.doNotValidateExamples { 69 | return errs 70 | } 71 | if o.Schema == nil { 72 | return append(errs, newValidationError(location, "unable to validate examples without schema")) 73 | } 74 | schemaRef := o.Schema.getLocationOrRef(joinLoc(location, "schema")) 75 | if o.Example != nil { 76 | if e := validator.ValidateData(schemaRef, o.Example); e != nil { 77 | errs = append(errs, newValidationError(joinLoc(location, "example"), e)) 78 | } 79 | } 80 | if len(o.Examples) > 0 { 81 | for k, v := range o.Examples { 82 | example, err := v.GetSpec(validator.spec.Spec.Components) 83 | if err != nil { 84 | // do not add the error, because it is already validated earlier 85 | continue 86 | } 87 | if value := example.Spec.Value; value != nil { 88 | if e := validator.ValidateData(schemaRef, value); e != nil { 89 | errs = append(errs, newValidationError(joinLoc(location, "examples", k), e)) 90 | } 91 | } 92 | } 93 | } 94 | 95 | return errs 96 | } 97 | 98 | type MediaTypeBuilder struct { 99 | spec *Extendable[MediaType] 100 | } 101 | 102 | func NewMediaTypeBuilder() *MediaTypeBuilder { 103 | return &MediaTypeBuilder{ 104 | spec: NewExtendable[MediaType](&MediaType{}), 105 | } 106 | } 107 | 108 | func (b *MediaTypeBuilder) Build() *Extendable[MediaType] { 109 | return b.spec 110 | } 111 | 112 | func (b *MediaTypeBuilder) Extensions(v map[string]any) *MediaTypeBuilder { 113 | b.spec.Extensions = v 114 | return b 115 | } 116 | 117 | func (b *MediaTypeBuilder) AddExt(name string, value any) *MediaTypeBuilder { 118 | b.spec.AddExt(name, value) 119 | return b 120 | } 121 | 122 | func (b *MediaTypeBuilder) Schema(v *RefOrSpec[Schema]) *MediaTypeBuilder { 123 | b.spec.Spec.Schema = v 124 | return b 125 | } 126 | 127 | func (b *MediaTypeBuilder) Example(v any) *MediaTypeBuilder { 128 | b.spec.Spec.Example = v 129 | return b 130 | } 131 | 132 | func (b *MediaTypeBuilder) Examples(v map[string]*RefOrSpec[Extendable[Example]]) *MediaTypeBuilder { 133 | b.spec.Spec.Examples = v 134 | return b 135 | } 136 | 137 | func (b *MediaTypeBuilder) AddExample(name string, value *RefOrSpec[Extendable[Example]]) *MediaTypeBuilder { 138 | if b.spec.Spec.Examples == nil { 139 | b.spec.Spec.Examples = make(map[string]*RefOrSpec[Extendable[Example]], 1) 140 | } 141 | b.spec.Spec.Examples[name] = value 142 | return b 143 | } 144 | 145 | func (b *MediaTypeBuilder) Encoding(v map[string]*Extendable[Encoding]) *MediaTypeBuilder { 146 | b.spec.Spec.Encoding = v 147 | return b 148 | } 149 | 150 | func (b *MediaTypeBuilder) AddEncoding(name string, value *Extendable[Encoding]) *MediaTypeBuilder { 151 | if b.spec.Spec.Encoding == nil { 152 | b.spec.Spec.Encoding = make(map[string]*Extendable[Encoding], 1) 153 | } 154 | b.spec.Spec.Encoding[name] = value 155 | return b 156 | } 157 | -------------------------------------------------------------------------------- /oauth-flow.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // OAuthFlow configuration details for a supported OAuth Flow 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#oauth-flow-object 6 | // 7 | // Example: 8 | // 9 | // implicit: 10 | // authorizationUrl: https://example.com/api/oauth/dialog 11 | // scopes: 12 | // write:pets: modify pets in your account 13 | // read:pets: read your pets 14 | // authorizationCode 15 | // authorizationUrl: https://example.com/api/oauth/dialog 16 | // scopes: 17 | // write:pets: modify pets in your account 18 | // read:pets: read your pets 19 | type OAuthFlow struct { 20 | // REQUIRED. 21 | // The available scopes for the OAuth2 security scheme. 22 | // A map between the scope name and a short description for it. 23 | // The map MAY be empty. 24 | // 25 | // Applies To: oauth2 26 | Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` 27 | // REQUIRED. 28 | // The authorization URL to be used for this flow. 29 | // This MUST be in the form of a URL. 30 | // The OAuth2 standard requires the use of TLS. 31 | // 32 | // Applies To:oauth2 ("implicit", "authorizationCode") 33 | AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` 34 | // REQUIRED. 35 | // The token URL to be used for this flow. 36 | // This MUST be in the form of a URL. 37 | // The OAuth2 standard requires the use of TLS. 38 | // 39 | // Applies To: oauth2 ("password", "clientCredentials", "authorizationCode") 40 | TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` 41 | // The URL to be used for obtaining refresh tokens. 42 | // This MUST be in the form of a URL. 43 | // The OAuth2 standard requires the use of TLS. 44 | // 45 | // Applies To: oauth2 46 | RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` 47 | } 48 | 49 | func (o *OAuthFlow) validateSpec(path string, validator *Validator) []*validationError { 50 | // all the validations are done in the parent object 51 | return nil 52 | } 53 | 54 | type OAuthFlowBuilder struct { 55 | spec *Extendable[OAuthFlow] 56 | } 57 | 58 | func NewOAuthFlowBuilder() *OAuthFlowBuilder { 59 | return &OAuthFlowBuilder{ 60 | spec: NewExtendable[OAuthFlow](&OAuthFlow{}), 61 | } 62 | } 63 | 64 | func (b *OAuthFlowBuilder) Build() *Extendable[OAuthFlow] { 65 | return b.spec 66 | } 67 | 68 | func (b *OAuthFlowBuilder) Extensions(v map[string]any) *OAuthFlowBuilder { 69 | b.spec.Extensions = v 70 | return b 71 | } 72 | 73 | func (b *OAuthFlowBuilder) AddExt(name string, value any) *OAuthFlowBuilder { 74 | b.spec.AddExt(name, value) 75 | return b 76 | } 77 | 78 | func (b *OAuthFlowBuilder) Scopes(v map[string]string) *OAuthFlowBuilder { 79 | b.spec.Spec.Scopes = v 80 | return b 81 | } 82 | 83 | func (b *OAuthFlowBuilder) AddScope(name, value string) *OAuthFlowBuilder { 84 | if b.spec.Spec.Scopes == nil { 85 | b.spec.Spec.Scopes = make(map[string]string, 1) 86 | } 87 | b.spec.Spec.Scopes[name] = value 88 | return b 89 | } 90 | 91 | func (b *OAuthFlowBuilder) AuthorizationURL(v string) *OAuthFlowBuilder { 92 | b.spec.Spec.AuthorizationURL = v 93 | return b 94 | } 95 | 96 | func (b *OAuthFlowBuilder) TokenURL(v string) *OAuthFlowBuilder { 97 | b.spec.Spec.TokenURL = v 98 | return b 99 | } 100 | 101 | func (b *OAuthFlowBuilder) RefreshURL(v string) *OAuthFlowBuilder { 102 | b.spec.Spec.RefreshURL = v 103 | return b 104 | } 105 | -------------------------------------------------------------------------------- /oauth-flows.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // OAuthFlows allows configuration of the supported OAuth Flows. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#oauth-flows-object 6 | // 7 | // Example: 8 | // 9 | // type: oauth2 10 | // flows: 11 | // implicit: 12 | // authorizationUrl: https://example.com/api/oauth/dialog 13 | // scopes: 14 | // write:pets: modify pets in your account 15 | // read:pets: read your pets 16 | // authorizationCode: 17 | // authorizationUrl: https://example.com/api/oauth/dialog 18 | // tokenUrl: https://example.com/api/oauth/token 19 | // scopes: 20 | // write:pets: modify pets in your account 21 | // read:pets: read your pets 22 | type OAuthFlows struct { 23 | // Configuration for the OAuth Implicit flow. 24 | Implicit *Extendable[OAuthFlow] `json:"implicit,omitempty" yaml:"implicit,omitempty"` 25 | // Configuration for the OAuth Resource Owner Password flow. 26 | Password *Extendable[OAuthFlow] `json:"password,omitempty" yaml:"password,omitempty"` 27 | // Configuration for the OAuth Client Credentials flow. 28 | // Previously called application in OpenAPI 2.0. 29 | ClientCredentials *Extendable[OAuthFlow] `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` 30 | // Configuration for the OAuth Authorization Code flow. 31 | // Previously called accessCode in OpenAPI 2.0. 32 | AuthorizationCode *Extendable[OAuthFlow] `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` 33 | } 34 | 35 | func (o *OAuthFlows) validateSpec(location string, validator *Validator) []*validationError { 36 | var errs []*validationError 37 | if o.Implicit != nil { 38 | errs = append(errs, o.Implicit.validateSpec(joinLoc(location, "implicit"), validator)...) 39 | if o.Implicit.Spec.AuthorizationURL == "" { 40 | errs = append(errs, newValidationError(joinLoc(location, "implicit", "authorizationUrl"), ErrRequired)) 41 | } 42 | } 43 | if o.Password != nil { 44 | errs = append(errs, o.Password.validateSpec(joinLoc(location, "password"), validator)...) 45 | if o.Password.Spec.TokenURL == "" { 46 | errs = append(errs, newValidationError(joinLoc(location, "password", "tokenUrl"), ErrRequired)) 47 | } 48 | } 49 | if o.ClientCredentials != nil { 50 | errs = append(errs, o.ClientCredentials.validateSpec(joinLoc(location, "clientCredentials"), validator)...) 51 | if o.ClientCredentials.Spec.TokenURL == "" { 52 | errs = append(errs, newValidationError(joinLoc(location, "clientCredentials", "tokenUrl"), ErrRequired)) 53 | } 54 | } 55 | if o.AuthorizationCode != nil { 56 | errs = append(errs, o.AuthorizationCode.validateSpec(joinLoc(location, "authorizationCode"), validator)...) 57 | if o.AuthorizationCode.Spec.AuthorizationURL == "" { 58 | errs = append(errs, newValidationError(joinLoc(location, "authorizationCode", "authorizationUrl"), ErrRequired)) 59 | } 60 | if o.AuthorizationCode.Spec.TokenURL == "" { 61 | errs = append(errs, newValidationError(joinLoc(location, "authorizationCode", "tokenUrl"), ErrRequired)) 62 | } 63 | } 64 | 65 | return errs 66 | } 67 | 68 | type OAuthFlowsBuilder struct { 69 | spec *Extendable[OAuthFlows] 70 | } 71 | 72 | func NewOAuthFlowsBuilder() *OAuthFlowsBuilder { 73 | return &OAuthFlowsBuilder{ 74 | spec: NewExtendable[OAuthFlows](&OAuthFlows{}), 75 | } 76 | } 77 | 78 | func (b *OAuthFlowsBuilder) Build() *Extendable[OAuthFlows] { 79 | return b.spec 80 | } 81 | 82 | func (b *OAuthFlowsBuilder) Extensions(v map[string]any) *OAuthFlowsBuilder { 83 | b.spec.Extensions = v 84 | return b 85 | } 86 | 87 | func (b *OAuthFlowsBuilder) AddExt(name string, value any) *OAuthFlowsBuilder { 88 | b.spec.AddExt(name, value) 89 | return b 90 | } 91 | 92 | func (b *OAuthFlowsBuilder) Implicit(v *Extendable[OAuthFlow]) *OAuthFlowsBuilder { 93 | b.spec.Spec.Implicit = v 94 | return b 95 | } 96 | 97 | func (b *OAuthFlowsBuilder) Password(v *Extendable[OAuthFlow]) *OAuthFlowsBuilder { 98 | b.spec.Spec.Password = v 99 | return b 100 | } 101 | 102 | func (b *OAuthFlowsBuilder) ClientCredentials(v *Extendable[OAuthFlow]) *OAuthFlowsBuilder { 103 | b.spec.Spec.ClientCredentials = v 104 | return b 105 | } 106 | 107 | func (b *OAuthFlowsBuilder) AuthorizationCode(v *Extendable[OAuthFlow]) *OAuthFlowsBuilder { 108 | b.spec.Spec.AuthorizationCode = v 109 | return b 110 | } 111 | -------------------------------------------------------------------------------- /path_item.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // PathItem describes the operations available on a single path. 4 | // A Path Item MAY be empty, due to ACL constraints. 5 | // The path itself is still exposed to the documentation viewer but they will not know which operations and parameters are available. 6 | // 7 | // https://spec.openapis.org/oas/v3.1.1#path-item-object 8 | // 9 | // Example: 10 | // 11 | // get: 12 | // description: Returns pets based on ID 13 | // summary: Find pets by ID 14 | // operationId: getPetsById 15 | // responses: 16 | // '200': 17 | // description: pet response 18 | // content: 19 | // '*/*' : 20 | // schema: 21 | // type: array 22 | // items: 23 | // $ref: '#/components/schemas/Pet' 24 | // default: 25 | // description: error payload 26 | // content: 27 | // 'text/html': 28 | // schema: 29 | // $ref: '#/components/schemas/ErrorModel' 30 | // parameters: 31 | // - name: id 32 | // in: path 33 | // description: ID of pet to use 34 | // required: true 35 | // schema: 36 | // type: array 37 | // items: 38 | // type: string 39 | // style: simple 40 | type PathItem struct { 41 | // An optional, string summary, intended to apply to all operations in this path. 42 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 43 | // An optional, string description, intended to apply to all operations in this path. 44 | // CommonMark syntax MAY be used for rich text representation. 45 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 46 | // A definition of a GET operation on this path. 47 | Get *Extendable[Operation] `json:"get,omitempty" yaml:"get,omitempty"` 48 | // A definition of a PUT operation on this path. 49 | Put *Extendable[Operation] `json:"put,omitempty" yaml:"put,omitempty"` 50 | // A definition of a POST operation on this path. 51 | Post *Extendable[Operation] `json:"post,omitempty" yaml:"post,omitempty"` 52 | // A definition of a DELETE operation on this path. 53 | Delete *Extendable[Operation] `json:"delete,omitempty" yaml:"delete,omitempty"` 54 | // A definition of a OPTIONS operation on this path. 55 | Options *Extendable[Operation] `json:"options,omitempty" yaml:"options,omitempty"` 56 | // A definition of a HEAD operation on this path. 57 | Head *Extendable[Operation] `json:"head,omitempty" yaml:"head,omitempty"` 58 | // A definition of a PATCH operation on this path. 59 | Patch *Extendable[Operation] `json:"patch,omitempty" yaml:"patch,omitempty"` 60 | // A definition of a TRACE operation on this path. 61 | Trace *Extendable[Operation] `json:"trace,omitempty" yaml:"trace,omitempty"` 62 | // An alternative server array to service all operations in this path. 63 | Servers []*Extendable[Server] `json:"servers,omitempty" yaml:"servers,omitempty"` 64 | // A list of parameters that are applicable for all the operations described under this path. 65 | // These parameters can be overridden at the operation level, but cannot be removed there. 66 | // The list MUST NOT include duplicated parameters. 67 | // A unique parameter is defined by a combination of a name and location. 68 | // The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object’s components/parameters. 69 | Parameters []*RefOrSpec[Extendable[Parameter]] `json:"parameters,omitempty" yaml:"parameters,omitempty"` 70 | } 71 | 72 | func (o *PathItem) validateSpec(location string, validator *Validator) []*validationError { 73 | var errs []*validationError 74 | if len(o.Parameters) > 0 { 75 | for i, v := range o.Parameters { 76 | errs = append(errs, v.validateSpec(joinLoc(location, "parameters", i), validator)...) 77 | } 78 | } 79 | if len(o.Servers) > 0 { 80 | for i, v := range o.Servers { 81 | errs = append(errs, v.validateSpec(joinLoc(location, "servers", i), validator)...) 82 | } 83 | } 84 | if o.Get != nil { 85 | errs = append(errs, o.Get.validateSpec(joinLoc(location, "get"), validator)...) 86 | } 87 | if o.Put != nil { 88 | errs = append(errs, o.Put.validateSpec(joinLoc(location, "put"), validator)...) 89 | } 90 | if o.Post != nil { 91 | errs = append(errs, o.Post.validateSpec(joinLoc(location, "post"), validator)...) 92 | } 93 | if o.Delete != nil { 94 | errs = append(errs, o.Delete.validateSpec(joinLoc(location, "delete"), validator)...) 95 | } 96 | if o.Options != nil { 97 | errs = append(errs, o.Options.validateSpec(joinLoc(location, "options"), validator)...) 98 | } 99 | if o.Head != nil { 100 | errs = append(errs, o.Head.validateSpec(joinLoc(location, "head"), validator)...) 101 | } 102 | if o.Patch != nil { 103 | errs = append(errs, o.Patch.validateSpec(joinLoc(location, "patch"), validator)...) 104 | } 105 | if o.Trace != nil { 106 | errs = append(errs, o.Trace.validateSpec(joinLoc(location, "trace"), validator)...) 107 | } 108 | return errs 109 | } 110 | 111 | type PathItemBuilder struct { 112 | spec *RefOrSpec[Extendable[PathItem]] 113 | } 114 | 115 | func NewPathItemBuilder() *PathItemBuilder { 116 | return &PathItemBuilder{ 117 | spec: NewRefOrExtSpec[PathItem](&PathItem{}), 118 | } 119 | } 120 | 121 | func (b *PathItemBuilder) Build() *RefOrSpec[Extendable[PathItem]] { 122 | return b.spec 123 | } 124 | 125 | func (b *PathItemBuilder) Extensions(v map[string]any) *PathItemBuilder { 126 | b.spec.Spec.Extensions = v 127 | return b 128 | } 129 | 130 | func (b *PathItemBuilder) AddExt(name string, value any) *PathItemBuilder { 131 | b.spec.Spec.AddExt(name, value) 132 | return b 133 | } 134 | 135 | func (b *PathItemBuilder) Summary(v string) *PathItemBuilder { 136 | b.spec.Spec.Spec.Summary = v 137 | return b 138 | } 139 | 140 | func (b *PathItemBuilder) Description(v string) *PathItemBuilder { 141 | b.spec.Spec.Spec.Description = v 142 | return b 143 | } 144 | 145 | func (b *PathItemBuilder) Get(v *Extendable[Operation]) *PathItemBuilder { 146 | b.spec.Spec.Spec.Get = v 147 | return b 148 | } 149 | 150 | func (b *PathItemBuilder) Put(v *Extendable[Operation]) *PathItemBuilder { 151 | b.spec.Spec.Spec.Put = v 152 | return b 153 | } 154 | 155 | func (b *PathItemBuilder) Post(v *Extendable[Operation]) *PathItemBuilder { 156 | b.spec.Spec.Spec.Post = v 157 | return b 158 | } 159 | 160 | func (b *PathItemBuilder) Delete(v *Extendable[Operation]) *PathItemBuilder { 161 | b.spec.Spec.Spec.Delete = v 162 | return b 163 | } 164 | 165 | func (b *PathItemBuilder) Options(v *Extendable[Operation]) *PathItemBuilder { 166 | b.spec.Spec.Spec.Options = v 167 | return b 168 | } 169 | 170 | func (b *PathItemBuilder) Head(v *Extendable[Operation]) *PathItemBuilder { 171 | b.spec.Spec.Spec.Head = v 172 | return b 173 | } 174 | 175 | func (b *PathItemBuilder) Patch(v *Extendable[Operation]) *PathItemBuilder { 176 | b.spec.Spec.Spec.Patch = v 177 | return b 178 | } 179 | 180 | func (b *PathItemBuilder) Trace(v *Extendable[Operation]) *PathItemBuilder { 181 | b.spec.Spec.Spec.Trace = v 182 | return b 183 | } 184 | 185 | func (b *PathItemBuilder) Servers(v ...*Extendable[Server]) *PathItemBuilder { 186 | b.spec.Spec.Spec.Servers = v 187 | return b 188 | } 189 | 190 | func (b *PathItemBuilder) AddServers(v ...*Extendable[Server]) *PathItemBuilder { 191 | b.spec.Spec.Spec.Servers = append(b.spec.Spec.Spec.Servers, v...) 192 | return b 193 | } 194 | 195 | func (b *PathItemBuilder) Parameters(v ...*RefOrSpec[Extendable[Parameter]]) *PathItemBuilder { 196 | b.spec.Spec.Spec.Parameters = v 197 | return b 198 | } 199 | 200 | func (b *PathItemBuilder) AddParameters(v ...*RefOrSpec[Extendable[Parameter]]) *PathItemBuilder { 201 | b.spec.Spec.Spec.Parameters = append(b.spec.Spec.Spec.Parameters, v...) 202 | return b 203 | } 204 | -------------------------------------------------------------------------------- /paths.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // Paths holds the relative paths to the individual endpoints and their operations. 11 | // The path is appended to the URL from the Server Object in order to construct the full URL. 12 | // The Paths MAY be empty, due to Access Control List (ACL) constraints. 13 | // 14 | // https://spec.openapis.org/oas/v3.1.1#paths-object 15 | // 16 | // Example: 17 | // 18 | // /pets: 19 | // get: 20 | // description: Returns all pets from the system that the user has access to 21 | // responses: 22 | // '200': 23 | // description: A list of pets. 24 | // content: 25 | // application/json: 26 | // schema: 27 | // type: array 28 | // items: 29 | // $ref: '#/components/schemas/pet' 30 | type Paths struct { 31 | // A relative path to an individual endpoint. 32 | // The field name MUST begin with a forward slash (/). 33 | // The path is appended (no relative URL resolution) to the expanded URL 34 | // from the Server Object’s url field in order to construct the full URL. 35 | // Path templating is allowed. 36 | // When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. 37 | // Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical. 38 | // In case of ambiguous matching, it’s up to the tooling to decide which one to use. 39 | Paths map[string]*RefOrSpec[Extendable[PathItem]] `json:"-" yaml:"-"` 40 | } 41 | 42 | // MarshalJSON implements json.Marshaler interface. 43 | func (o *Paths) MarshalJSON() ([]byte, error) { 44 | return json.Marshal(&o.Paths) 45 | } 46 | 47 | // UnmarshalYAML implements yaml.Unmarshaler interface. 48 | func (o *Paths) UnmarshalYAML(node *yaml.Node) error { 49 | return node.Decode(&o.Paths) 50 | } 51 | 52 | // MarshalYAML implements yaml.Marshaler interface. 53 | func (o *Paths) MarshalYAML() (any, error) { 54 | return o.Paths, nil 55 | } 56 | 57 | // UnmarshalJSON implements json.Unmarshaler interface. 58 | func (o *Paths) UnmarshalJSON(data []byte) error { 59 | return json.Unmarshal(data, &o.Paths) 60 | } 61 | 62 | func (o *Paths) validateSpec(location string, validator *Validator) []*validationError { 63 | var errs []*validationError 64 | for k, v := range o.Paths { 65 | if !strings.HasPrefix(k, "/") { 66 | errs = append(errs, newValidationError(joinLoc(location, k), "path must start with a forward slash (`/`)")) 67 | } 68 | if v == nil { 69 | errs = append(errs, newValidationError(joinLoc(location, k), "path item cannot be empty")) 70 | } else { 71 | errs = append(errs, v.validateSpec(joinLoc(location, k), validator)...) 72 | } 73 | } 74 | return errs 75 | } 76 | 77 | func (o *Paths) Add(path string, item *RefOrSpec[Extendable[PathItem]]) *Paths { 78 | if item == nil { 79 | return o 80 | } 81 | if o.Paths == nil { 82 | o.Paths = make(map[string]*RefOrSpec[Extendable[PathItem]]) 83 | } 84 | o.Paths[path] = item 85 | return o 86 | } 87 | 88 | func NewPaths() *Extendable[Paths] { 89 | return NewExtendable[Paths](&Paths{}) 90 | } 91 | -------------------------------------------------------------------------------- /ref.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Ref is a simple object to allow referencing other components in the OpenAPI document, internally and externally. 12 | // The $ref string value contains a URI [RFC3986], which identifies the location of the value being referenced. 13 | // See the rules for resolving Relative References. 14 | // 15 | // https://spec.openapis.org/oas/v3.1.1#reference-object 16 | // 17 | // Example: 18 | // 19 | // $ref: '#/components/schemas/Pet' 20 | type Ref struct { 21 | // REQUIRED. 22 | // The reference identifier. 23 | // This MUST be in the form of a URI. 24 | Ref string `json:"$ref" yaml:"$ref"` 25 | // A short summary which by default SHOULD override that of the referenced component. 26 | // If the referenced object-type does not allow a summary field, then this field has no effect. 27 | Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` 28 | // A description which by default SHOULD override that of the referenced component. 29 | // CommonMark syntax MAY be used for rich text representation. 30 | // If the referenced object-type does not allow a description field, then this field has no effect. 31 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 32 | } 33 | 34 | // RefOrSpec holds either Ref or any OpenAPI spec type. 35 | // 36 | // NOTE: The Ref object takes precedent over Spec if using json or yaml Marshal and Unmarshal functions. 37 | type RefOrSpec[T any] struct { 38 | Ref *Ref `json:"-" yaml:"-"` 39 | Spec *T `json:"-" yaml:"-"` 40 | } 41 | 42 | // NewRefOrSpec creates an object of RefOrSpec type from given Ref or string or any form of Spec. 43 | func NewRefOrSpec[T any](v any) *RefOrSpec[T] { 44 | o := RefOrSpec[T]{} 45 | switch t := v.(type) { 46 | case *Ref: 47 | o.Ref = t 48 | case Ref: 49 | o.Ref = &t 50 | case string: 51 | o.Ref = &Ref{Ref: t} 52 | case nil: 53 | case *T: 54 | o.Spec = t 55 | case T: 56 | o.Spec = &t 57 | } 58 | return &o 59 | } 60 | 61 | // NewRefOrExtSpec creates an object of RefOrSpec[Extendable[any]] type from given Ref or string or any form of Spec. 62 | func NewRefOrExtSpec[T any](v any) *RefOrSpec[Extendable[T]] { 63 | o := RefOrSpec[Extendable[T]]{} 64 | switch t := v.(type) { 65 | case *Ref: 66 | o.Ref = t 67 | case Ref: 68 | o.Ref = &t 69 | case string: 70 | o.Ref = &Ref{Ref: t} 71 | case nil: 72 | case *T: 73 | o.Spec = NewExtendable[T](t) 74 | case T: 75 | o.Spec = NewExtendable[T](&t) 76 | } 77 | return &o 78 | } 79 | 80 | func (o *RefOrSpec[T]) getLocationOrRef(location string) string { 81 | if o.Ref != nil { 82 | return o.Ref.Ref 83 | } 84 | return location 85 | } 86 | 87 | // GetSpec return a Spec if it is set or loads it from Components in case of Ref or an error 88 | func (o *RefOrSpec[T]) GetSpec(c *Extendable[Components]) (*T, error) { 89 | return o.getSpec(c, make(visitedObjects)) 90 | } 91 | 92 | const specNotFoundPrefix = "spec not found: " 93 | 94 | type SpecNotFoundError struct { 95 | message string 96 | visitedObjects string 97 | } 98 | 99 | func (e *SpecNotFoundError) Error() string { 100 | return specNotFoundPrefix + e.message + "; visited refs: " + e.visitedObjects 101 | } 102 | 103 | func (e *SpecNotFoundError) Is(target error) bool { 104 | return strings.HasPrefix(target.Error(), specNotFoundPrefix) 105 | } 106 | 107 | func NewSpecNotFoundError(message string, visitedObjects visitedObjects) error { 108 | return &SpecNotFoundError{ 109 | message: message, 110 | visitedObjects: visitedObjects.String(), 111 | } 112 | } 113 | 114 | func (o *RefOrSpec[T]) getSpec(c *Extendable[Components], visited visitedObjects) (*T, error) { 115 | // some guards 116 | switch { 117 | case o.Spec != nil: 118 | return o.Spec, nil 119 | case o.Ref == nil: 120 | return nil, NewSpecNotFoundError("nil Ref", visited) 121 | case visited[o.Ref.Ref]: 122 | return nil, NewSpecNotFoundError(fmt.Sprintf("cycle ref %q detected", o.Ref.Ref), visited) 123 | case !strings.HasPrefix(o.Ref.Ref, "#/components/"): 124 | // TODO: support loading by url 125 | return nil, NewSpecNotFoundError(fmt.Sprintf("loading outside of components is not implemented for the ref %q", o.Ref.Ref), visited) 126 | case c == nil: 127 | return nil, NewSpecNotFoundError("components is required, but got nil", visited) 128 | } 129 | visited[o.Ref.Ref] = true 130 | 131 | parts := strings.SplitN(o.Ref.Ref[13:], "/", 2) 132 | if len(parts) != 2 { 133 | return nil, NewSpecNotFoundError(fmt.Sprintf("incorrect ref %q", o.Ref.Ref), visited) 134 | } 135 | objName := parts[1] 136 | var ref any 137 | switch parts[0] { 138 | case "schemas": 139 | ref = c.Spec.Schemas[objName] 140 | case "responses": 141 | ref = c.Spec.Responses[objName] 142 | case "parameters": 143 | ref = c.Spec.Parameters[objName] 144 | case "examples": 145 | ref = c.Spec.Examples[objName] 146 | case "requestBodies": 147 | ref = c.Spec.RequestBodies[objName] 148 | case "headers": 149 | ref = c.Spec.Headers[objName] 150 | case "links": 151 | ref = c.Spec.Links[objName] 152 | case "callbacks": 153 | ref = c.Spec.Callbacks[objName] 154 | case "paths": 155 | ref = c.Spec.Paths[objName] 156 | default: 157 | return nil, NewSpecNotFoundError(fmt.Sprintf("unexpected component %q", ref), visited) 158 | } 159 | obj, ok := ref.(*RefOrSpec[T]) 160 | if !ok { 161 | return nil, NewSpecNotFoundError(fmt.Sprintf("expected spec of type %T, but got %T", RefOrSpec[T]{}, ref), visited) 162 | } 163 | if obj.Spec != nil { 164 | return obj.Spec, nil 165 | } 166 | return obj.getSpec(c, visited) 167 | } 168 | 169 | // MarshalJSON implements json.Marshaler interface. 170 | func (o *RefOrSpec[T]) MarshalJSON() ([]byte, error) { 171 | var v any 172 | if o.Ref != nil { 173 | v = o.Ref 174 | } else { 175 | v = o.Spec 176 | } 177 | data, err := json.Marshal(&v) 178 | if err != nil { 179 | return nil, fmt.Errorf("%T: %w", o.Spec, err) 180 | } 181 | return data, nil 182 | } 183 | 184 | // UnmarshalJSON implements json.Unmarshaler interface. 185 | func (o *RefOrSpec[T]) UnmarshalJSON(data []byte) error { 186 | if json.Unmarshal(data, &o.Ref) == nil && o.Ref.Ref != "" { 187 | o.Spec = nil 188 | return nil 189 | } 190 | 191 | o.Ref = nil 192 | if err := json.Unmarshal(data, &o.Spec); err != nil { 193 | return fmt.Errorf("%T: %w", o.Spec, err) 194 | } 195 | return nil 196 | } 197 | 198 | // MarshalYAML implements yaml.Marshaler interface. 199 | func (o *RefOrSpec[T]) MarshalYAML() (any, error) { 200 | var v any 201 | if o.Ref != nil { 202 | v = o.Ref 203 | } else { 204 | v = o.Spec 205 | } 206 | return v, nil 207 | } 208 | 209 | // UnmarshalYAML implements yaml.Unmarshaler interface. 210 | func (o *RefOrSpec[T]) UnmarshalYAML(node *yaml.Node) error { 211 | if node.Decode(&o.Ref) == nil && o.Ref.Ref != "" { 212 | return nil 213 | } 214 | 215 | o.Ref = nil 216 | if err := node.Decode(&o.Spec); err != nil { 217 | return fmt.Errorf("%T: %w", o.Spec, err) 218 | } 219 | return nil 220 | } 221 | 222 | func (o *RefOrSpec[T]) validateSpec(location string, validator *Validator) []*validationError { 223 | var errs []*validationError 224 | if o.Spec != nil { 225 | if spec, ok := any(o.Spec).(validatable); ok { 226 | errs = append(errs, spec.validateSpec(location, validator)...) 227 | } else { 228 | errs = append(errs, newValidationError(location, NewUnsupportedSpecTypeError(o.Spec))) 229 | } 230 | } else { 231 | // do not validate already visited refs 232 | if validator.visited[o.Ref.Ref] { 233 | return errs 234 | } 235 | validator.visited[o.Ref.Ref] = true 236 | spec, err := o.GetSpec(validator.spec.Spec.Components) 237 | if err != nil { 238 | errs = append(errs, newValidationError(location, err)) 239 | } else if spec != nil { 240 | errs = append(errs, o.validateSpec(location, validator)...) 241 | } 242 | } 243 | return errs 244 | } 245 | -------------------------------------------------------------------------------- /request_body.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // RequestBody describes a single request body. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#request-body-object 6 | // 7 | // Example: 8 | // 9 | // description: user to add to the system 10 | // content: 11 | // 'application/json': 12 | // schema: 13 | // $ref: '#/components/schemas/User' 14 | // examples: 15 | // user: 16 | // summary: User Example 17 | // externalValue: 'https://foo.bar/examples/user-example.json' 18 | // 'application/xml': 19 | // schema: 20 | // $ref: '#/components/schemas/User' 21 | // examples: 22 | // user: 23 | // summary: User example in XML 24 | // externalValue: 'https://foo.bar/examples/user-example.xml' 25 | // 'text/plain': 26 | // examples: 27 | // user: 28 | // summary: User example in Plain text 29 | // externalValue: 'https://foo.bar/examples/user-example.txt' 30 | // '*/*': 31 | // examples: 32 | // user: 33 | // summary: User example in other format 34 | // externalValue: 'https://foo.bar/examples/user-example.whatever' 35 | type RequestBody struct { 36 | // REQUIRED. 37 | // The content of the request body. 38 | // The key is a media type or [media type range](appendix-D) and the value describes it. 39 | // For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* 40 | Content map[string]*Extendable[MediaType] `json:"content,omitempty" yaml:"content,omitempty"` 41 | // A brief description of the request body. 42 | // This could contain examples of use. 43 | // CommonMark syntax MAY be used for rich text representation. 44 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 45 | // Determines if the request body is required in the request. 46 | // Defaults to false. 47 | Required bool `json:"required,omitempty" yaml:"required,omitempty"` 48 | } 49 | 50 | func (o *RequestBody) validateSpec(location string, validator *Validator) []*validationError { 51 | var errs []*validationError 52 | if len(o.Content) == 0 { 53 | errs = append(errs, newValidationError(joinLoc(location, "content"), ErrRequired)) 54 | } else { 55 | for k, v := range o.Content { 56 | errs = append(errs, v.validateSpec(joinLoc(location, "content", k), validator)...) 57 | } 58 | } 59 | return errs 60 | } 61 | 62 | type RequestBodyBuilder struct { 63 | spec *RefOrSpec[Extendable[RequestBody]] 64 | } 65 | 66 | func NewRequestBodyBuilder() *RequestBodyBuilder { 67 | return &RequestBodyBuilder{ 68 | spec: NewRefOrExtSpec[RequestBody](&RequestBody{}), 69 | } 70 | } 71 | 72 | func (b *RequestBodyBuilder) Build() *RefOrSpec[Extendable[RequestBody]] { 73 | return b.spec 74 | } 75 | 76 | func (b *RequestBodyBuilder) Extensions(v map[string]any) *RequestBodyBuilder { 77 | b.spec.Spec.Extensions = v 78 | return b 79 | } 80 | 81 | func (b *RequestBodyBuilder) AddExt(name string, value any) *RequestBodyBuilder { 82 | b.spec.Spec.AddExt(name, value) 83 | return b 84 | } 85 | 86 | func (b *RequestBodyBuilder) Content(v map[string]*Extendable[MediaType]) *RequestBodyBuilder { 87 | b.spec.Spec.Spec.Content = v 88 | return b 89 | } 90 | 91 | func (b *RequestBodyBuilder) AddContent(key string, value *Extendable[MediaType]) *RequestBodyBuilder { 92 | if b.spec.Spec.Spec.Content == nil { 93 | b.spec.Spec.Spec.Content = make(map[string]*Extendable[MediaType], 1) 94 | } 95 | b.spec.Spec.Spec.Content[key] = value 96 | return b 97 | } 98 | 99 | func (b *RequestBodyBuilder) Description(v string) *RequestBodyBuilder { 100 | b.spec.Spec.Spec.Description = v 101 | return b 102 | } 103 | 104 | func (b *RequestBodyBuilder) Required(v bool) *RequestBodyBuilder { 105 | b.spec.Spec.Spec.Required = v 106 | return b 107 | } 108 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Response describes a single response from an API Operation, including design-time, static links to operations based on the response. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#response-object 6 | // 7 | // Example: 8 | // 9 | // description: A complex object array response 10 | // content: 11 | // application/json: 12 | // schema: 13 | // type: array 14 | // items: 15 | // $ref: '#/components/schemas/VeryComplexType' 16 | type Response struct { 17 | // Maps a header name to its definition. 18 | // [RFC7230] states header names are case insensitive. 19 | // If a response header is defined with the name "Content-Type", it SHALL be ignored. 20 | Headers map[string]*RefOrSpec[Extendable[Header]] `json:"headers,omitempty" yaml:"headers,omitempty"` 21 | // A map containing descriptions of potential response payloads. 22 | // The key is a media type or [media type range](appendix-D) and the value describes it. 23 | // For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* 24 | Content map[string]*Extendable[MediaType] `json:"content,omitempty" yaml:"content,omitempty"` 25 | // A map of operations links that can be followed from the response. 26 | // The key of the map is a short name for the link, following the naming constraints of the names for Component Objects. 27 | Links map[string]*RefOrSpec[Extendable[Link]] `json:"links,omitempty" yaml:"links,omitempty"` 28 | // REQUIRED. 29 | // A description of the response. 30 | // CommonMark syntax MAY be used for rich text representation. 31 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 32 | } 33 | 34 | func (o *Response) validateSpec(location string, validator *Validator) []*validationError { 35 | errs := make([]*validationError, 0) 36 | if o.Description == "" { 37 | errs = append(errs, newValidationError(joinLoc(location, "description"), ErrRequired)) 38 | } 39 | if o.Content != nil { 40 | for k, v := range o.Content { 41 | errs = append(errs, v.validateSpec(joinLoc(location, "content", k), validator)...) 42 | } 43 | } 44 | if o.Links != nil { 45 | for k, v := range o.Links { 46 | errs = append(errs, v.validateSpec(joinLoc(location, "links", k), validator)...) 47 | } 48 | } 49 | if o.Headers != nil { 50 | for k, v := range o.Headers { 51 | errs = append(errs, v.validateSpec(joinLoc(location, "headers", k), validator)...) 52 | } 53 | } 54 | return errs 55 | } 56 | 57 | type ResponseBuilder struct { 58 | spec *RefOrSpec[Extendable[Response]] 59 | } 60 | 61 | func NewResponseBuilder() *ResponseBuilder { 62 | return &ResponseBuilder{ 63 | spec: NewRefOrExtSpec[Response](&Response{}), 64 | } 65 | } 66 | 67 | func (b *ResponseBuilder) Build() *RefOrSpec[Extendable[Response]] { 68 | return b.spec 69 | } 70 | 71 | func (b *ResponseBuilder) Extensions(v map[string]any) *ResponseBuilder { 72 | b.spec.Spec.Extensions = v 73 | return b 74 | } 75 | 76 | func (b *ResponseBuilder) AddExt(name string, value any) *ResponseBuilder { 77 | b.spec.Spec.AddExt(name, value) 78 | return b 79 | } 80 | 81 | func (b *ResponseBuilder) Headers(v map[string]*RefOrSpec[Extendable[Header]]) *ResponseBuilder { 82 | b.spec.Spec.Spec.Headers = v 83 | return b 84 | } 85 | 86 | func (b *ResponseBuilder) AddHeader(key string, value *RefOrSpec[Extendable[Header]]) *ResponseBuilder { 87 | if b.spec.Spec.Spec.Headers == nil { 88 | b.spec.Spec.Spec.Headers = make(map[string]*RefOrSpec[Extendable[Header]], 1) 89 | } 90 | b.spec.Spec.Spec.Headers[key] = value 91 | return b 92 | } 93 | 94 | func (b *ResponseBuilder) Content(v map[string]*Extendable[MediaType]) *ResponseBuilder { 95 | b.spec.Spec.Spec.Content = v 96 | return b 97 | } 98 | 99 | func (b *ResponseBuilder) AddContent(key string, value *Extendable[MediaType]) *ResponseBuilder { 100 | if b.spec.Spec.Spec.Content == nil { 101 | b.spec.Spec.Spec.Content = make(map[string]*Extendable[MediaType], 1) 102 | } 103 | b.spec.Spec.Spec.Content[key] = value 104 | return b 105 | } 106 | 107 | func (b *ResponseBuilder) Links(v map[string]*RefOrSpec[Extendable[Link]]) *ResponseBuilder { 108 | b.spec.Spec.Spec.Links = v 109 | return b 110 | } 111 | 112 | func (b *ResponseBuilder) AddLink(key string, value *RefOrSpec[Extendable[Link]]) *ResponseBuilder { 113 | if b.spec.Spec.Spec.Links == nil { 114 | b.spec.Spec.Spec.Links = make(map[string]*RefOrSpec[Extendable[Link]], 1) 115 | } 116 | b.spec.Spec.Spec.Links[key] = value 117 | return b 118 | } 119 | 120 | func (b *ResponseBuilder) Description(v string) *ResponseBuilder { 121 | b.spec.Spec.Spec.Description = v 122 | return b 123 | } 124 | -------------------------------------------------------------------------------- /responses.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | var ResponseCodePattern = regexp.MustCompile(`^[1-5](?:\d{2}|XX)$`) 11 | 12 | // Responses is a container for the expected responses of an operation. 13 | // The container maps a HTTP response code to the expected response. 14 | // The documentation is not necessarily expected to cover all possible HTTP response codes because they may not be known in advance. 15 | // However, documentation is expected to cover a successful operation response and any known errors. 16 | // The default MAY be used as a default response object for all HTTP codes that are not covered individually by the Responses Object. 17 | // The Responses Object MUST contain at least one response code, and if only one response code is provided 18 | // it SHOULD be the response for a successful operation call. 19 | // 20 | // https://spec.openapis.org/oas/v3.1.1#responses-object 21 | // 22 | // Example: 23 | // 24 | // '200': 25 | // description: a pet to be returned 26 | // content: 27 | // application/json: 28 | // schema: 29 | // $ref: '#/components/schemas/Pet' 30 | // default: 31 | // description: Unexpected error 32 | // content: 33 | // application/json: 34 | // schema: 35 | // $ref: '#/components/schemas/ErrorModel' 36 | type Responses struct { 37 | // The documentation of responses other than the ones declared for specific HTTP response codes. 38 | // Use this field to cover undeclared responses. 39 | Default *RefOrSpec[Extendable[Response]] `json:"default,omitempty" yaml:"default,omitempty"` 40 | // Any HTTP status code can be used as the property name, but only one property per code, 41 | // to describe the expected response for that HTTP status code. 42 | // This field MUST be enclosed in quotation marks (for example, “200”) for compatibility between JSON and YAML. 43 | // To define a range of response codes, this field MAY contain the uppercase wildcard character X. 44 | // For example, 2XX represents all response codes between [200-299]. 45 | // Only the following range definitions are allowed: 1XX, 2XX, 3XX, 4XX, and 5XX. 46 | // If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code. 47 | Response map[string]*RefOrSpec[Extendable[Response]] `json:"-" yaml:"-"` 48 | } 49 | 50 | // MarshalJSON implements json.Marshaler interface. 51 | func (o *Responses) MarshalJSON() ([]byte, error) { 52 | var raw map[string]json.RawMessage 53 | data, err := json.Marshal(&o.Response) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if err := json.Unmarshal(data, &raw); err != nil { 58 | return nil, err 59 | } 60 | 61 | if o.Default != nil { 62 | data, err = json.Marshal(&o.Default) 63 | if err != nil { 64 | return nil, err 65 | } 66 | if raw == nil { 67 | raw = make(map[string]json.RawMessage, 1) 68 | } 69 | raw["default"] = data 70 | } 71 | return json.Marshal(&raw) 72 | } 73 | 74 | // UnmarshalJSON implements json.Unmarshaler interface. 75 | func (o *Responses) UnmarshalJSON(data []byte) error { 76 | var raw map[string]json.RawMessage 77 | if err := json.Unmarshal(data, &raw); err != nil { 78 | return err 79 | } 80 | if v, ok := raw["default"]; ok { 81 | if err := json.Unmarshal(v, &o.Default); err != nil { 82 | return err 83 | } 84 | delete(raw, "default") 85 | } 86 | data, err := json.Marshal(&raw) 87 | if err != nil { 88 | return err 89 | } 90 | return json.Unmarshal(data, &o.Response) 91 | } 92 | 93 | // MarshalYAML implements yaml.Marshaler interface. 94 | func (o *Responses) MarshalYAML() (any, error) { 95 | var raw map[string]any 96 | data, err := yaml.Marshal(&o.Response) 97 | if err != nil { 98 | return nil, err 99 | } 100 | if err := yaml.Unmarshal(data, &raw); err != nil { 101 | return nil, err 102 | } 103 | 104 | if o.Default != nil { 105 | if raw == nil { 106 | raw = make(map[string]any, 1) 107 | } 108 | raw["default"] = o.Default 109 | } 110 | return raw, nil 111 | } 112 | 113 | // UnmarshalYAML implements yaml.Unmarshaler interface. 114 | func (o *Responses) UnmarshalYAML(node *yaml.Node) error { 115 | var raw map[string]any 116 | if err := node.Decode(&raw); err != nil { 117 | return err 118 | } 119 | if v, ok := raw["default"]; ok { 120 | data, err := yaml.Marshal(&v) 121 | if err != nil { 122 | return err 123 | } 124 | if err := yaml.Unmarshal(data, &o.Default); err != nil { 125 | return err 126 | } 127 | delete(raw, "default") 128 | } 129 | data, err := yaml.Marshal(&raw) 130 | if err != nil { 131 | return err 132 | } 133 | return yaml.Unmarshal(data, &o.Response) 134 | } 135 | 136 | func (o *Responses) validateSpec(location string, validator *Validator) []*validationError { 137 | var errs []*validationError 138 | if o.Default != nil { 139 | errs = append(errs, o.Default.validateSpec(joinLoc(location, "default"), validator)...) 140 | } 141 | for k, v := range o.Response { 142 | if !ResponseCodePattern.MatchString(k) { 143 | errs = append(errs, newValidationError(joinLoc(location, k), "must match pattern '%s', but got '%s'", ResponseCodePattern, k)) 144 | } 145 | errs = append(errs, v.validateSpec(joinLoc(location, k), validator)...) 146 | } 147 | return errs 148 | } 149 | 150 | type ResponsesBuilder struct { 151 | spec *RefOrSpec[Extendable[Responses]] 152 | } 153 | 154 | func NewResponsesBuilder() *ResponsesBuilder { 155 | return &ResponsesBuilder{ 156 | spec: NewRefOrExtSpec[Responses](&Responses{}), 157 | } 158 | } 159 | 160 | func (b *ResponsesBuilder) Build() *RefOrSpec[Extendable[Responses]] { 161 | return b.spec 162 | } 163 | 164 | func (b *ResponsesBuilder) Extensions(v map[string]any) *ResponsesBuilder { 165 | b.spec.Spec.Extensions = v 166 | return b 167 | } 168 | 169 | func (b *ResponsesBuilder) AddExt(name string, value any) *ResponsesBuilder { 170 | b.spec.Spec.AddExt(name, value) 171 | return b 172 | } 173 | 174 | func (b *ResponsesBuilder) Default(v *RefOrSpec[Extendable[Response]]) *ResponsesBuilder { 175 | b.spec.Spec.Spec.Default = v 176 | return b 177 | } 178 | 179 | func (b *ResponsesBuilder) Response(v map[string]*RefOrSpec[Extendable[Response]]) *ResponsesBuilder { 180 | b.spec.Spec.Spec.Response = v 181 | return b 182 | } 183 | 184 | func (b *ResponsesBuilder) AddResponse(key string, value *RefOrSpec[Extendable[Response]]) *ResponsesBuilder { 185 | if b.spec.Spec.Spec.Response == nil { 186 | b.spec.Spec.Spec.Response = make(map[string]*RefOrSpec[Extendable[Response]], 1) 187 | } 188 | b.spec.Spec.Spec.Response[key] = value 189 | return b 190 | } 191 | -------------------------------------------------------------------------------- /schema_test.go: -------------------------------------------------------------------------------- 1 | package openapi_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/sv-tools/openapi" 11 | ) 12 | 13 | func TestSchema_Marshal_Unmarshal(t *testing.T) { 14 | for _, tt := range []struct { 15 | name string 16 | data string 17 | expected string 18 | emptyExtensions bool 19 | }{ 20 | { 21 | name: "spec only", 22 | data: `{"title": "foo"}`, 23 | emptyExtensions: true, 24 | }, 25 | { 26 | name: "spec with extension field", 27 | data: `{"title": "foo", "b": "bar"}`, 28 | emptyExtensions: false, 29 | }, 30 | } { 31 | t.Run(tt.name, func(t *testing.T) { 32 | t.Run("json", func(t *testing.T) { 33 | var v *openapi.Schema 34 | require.NoError(t, json.Unmarshal([]byte(tt.data), &v)) 35 | if tt.emptyExtensions { 36 | require.Empty(t, v.Extensions) 37 | } else { 38 | require.NotEmpty(t, v.Extensions) 39 | } 40 | data, err := json.Marshal(&v) 41 | require.NoError(t, err) 42 | if tt.expected == "" { 43 | tt.expected = tt.data 44 | } 45 | require.JSONEq(t, tt.expected, string(data)) 46 | }) 47 | t.Run("yaml", func(t *testing.T) { 48 | var v *openapi.Schema 49 | require.NoError(t, yaml.Unmarshal([]byte(tt.data), &v)) 50 | if tt.emptyExtensions { 51 | require.Empty(t, v.Extensions) 52 | } else { 53 | require.NotEmpty(t, v.Extensions) 54 | } 55 | data, err := yaml.Marshal(&v) 56 | require.NoError(t, err) 57 | if tt.expected == "" { 58 | tt.expected = tt.data 59 | } 60 | require.YAMLEq(t, tt.expected, string(data)) 61 | }) 62 | }) 63 | } 64 | } 65 | 66 | func TestSchema_AddExt(t *testing.T) { 67 | for _, tt := range []struct { 68 | name string 69 | key string 70 | value any 71 | expected map[string]any 72 | }{ 73 | { 74 | name: "without prefix", 75 | key: "foo", 76 | value: 42, 77 | expected: map[string]any{ 78 | "foo": 42, 79 | }, 80 | }, 81 | { 82 | name: "with prefix", 83 | key: "x-foo", 84 | value: 43, 85 | expected: map[string]any{ 86 | "x-foo": 43, 87 | }, 88 | }, 89 | } { 90 | t.Run(tt.name, func(t *testing.T) { 91 | ext := openapi.Schema{} 92 | ext.AddExt(tt.key, tt.value) 93 | require.Equal(t, tt.expected, ext.Extensions) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /security-requirement.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // SecurityRequirement is the lists of the required security schemes to execute this operation. 4 | // The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. 5 | // Security Requirement Objects that contain multiple schemes require that all schemes MUST be satisfied for a request to be authorized. 6 | // This enables support for scenarios where multiple query parameters or HTTP headers are required to convey security information. 7 | // When a list of Security Requirement Objects is defined on the OpenAPI Object or Operation Object, 8 | // only one of the Security Requirement Objects in the list needs to be satisfied to authorize the request. 9 | // 10 | // https://spec.openapis.org/oas/v3.1.1#security-requirement-object 11 | // 12 | // Example: 13 | // 14 | // api_key: [] 15 | type SecurityRequirement map[string][]string 16 | 17 | func (o *SecurityRequirement) validateSpec(_ string, validator *Validator) []*validationError { //nolint: unparam // by design 18 | for k := range *o { 19 | validator.visited[joinLoc("#", "components", "securitySchemes", k)] = true 20 | } 21 | return nil // nothing to validate 22 | } 23 | 24 | type SecurityRequirementBuilder struct { 25 | spec SecurityRequirement 26 | } 27 | 28 | func NewSecurityRequirementBuilder() *SecurityRequirementBuilder { 29 | return &SecurityRequirementBuilder{ 30 | spec: make(SecurityRequirement), 31 | } 32 | } 33 | 34 | func (b *SecurityRequirementBuilder) Build() *SecurityRequirement { 35 | return &b.spec 36 | } 37 | 38 | func (b *SecurityRequirementBuilder) Add(name string, scopes ...string) *SecurityRequirementBuilder { 39 | b.spec[name] = append(b.spec[name], scopes...) 40 | return b 41 | } 42 | -------------------------------------------------------------------------------- /security-scheme.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | const ( 4 | TypeApiKey = "apiKey" 5 | TypeHTTP = "http" 6 | TypeMutualTLS = "mutualTLS" 7 | TypeOAuth2 = "oauth2" 8 | TypeOpenIDConnect = "openIdConnect" 9 | ) 10 | 11 | // SecurityScheme defines a security scheme that can be used by the operations. 12 | // Supported schemes are HTTP authentication, an API key (either as a header, a cookie parameter or as a query parameter), 13 | // mutual TLS (use of a client certificate), OAuth2’s common flows (implicit, password, client credentials and authorization code) 14 | // as defined in [RFC6749], and OpenID Connect Discovery. 15 | // Please note that as of 2020, the implicit flow is about to be deprecated by OAuth 2.0 Security Best Current Practice. 16 | // Recommended for most use case is Authorization Code Grant flow with PKCE. 17 | // 18 | // https://spec.openapis.org/oas/v3.1.1#security-scheme-object 19 | // 20 | // Example: 21 | // 22 | // type: oauth2 23 | // flows: 24 | // implicit: 25 | // authorizationUrl: https://example.com/api/oauth/dialog 26 | // scopes: 27 | // write:pets: modify pets in your account 28 | // read:pets: read your pets 29 | type SecurityScheme struct { 30 | // REQUIRED. 31 | // The type of the security scheme. 32 | // Valid values are "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect". 33 | // 34 | // Applies To: any 35 | Type string `json:"type" yaml:"type"` 36 | // A description for security scheme. 37 | // CommonMark syntax MAY be used for rich text representation. 38 | // 39 | // Applies To: any 40 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 41 | // REQUIRED. 42 | // The name of the header, query or cookie parameter to be used. 43 | // 44 | // Applies To: apiKey 45 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 46 | // REQUIRED. 47 | // The location of the API key. 48 | // Valid values are "query", "header" or "cookie". 49 | // 50 | // Applies To: apiKey 51 | In string `json:"in,omitempty" yaml:"in,omitempty"` 52 | // REQUIRED. 53 | // The name of the HTTP Authorization scheme to be used in the Authorization header as defined in [RFC7235]. 54 | // The values used SHOULD be registered in the IANA Authentication Scheme registry. 55 | // 56 | // Applies To: http 57 | Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` 58 | // A hint to the client to identify how the bearer token is formatted. 59 | // Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes. 60 | // 61 | // Applies To: http ("bearer") 62 | BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` 63 | // REQUIRED. 64 | // An object containing configuration information for the flow types supported. 65 | // 66 | // Applies To: oauth2 67 | Flows *Extendable[OAuthFlows] `json:"flows,omitempty" yaml:"flows,omitempty"` 68 | // REQUIRED. 69 | // OpenId Connect URL to discover OAuth2 configuration values. 70 | // This MUST be in the form of a URL. 71 | // The OpenID Connect standard requires the use of TLS. 72 | // 73 | // Applies To: openIdConnect 74 | OpenIDConnectURL string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` 75 | } 76 | 77 | func (o *SecurityScheme) validateSpec(location string, validator *Validator) []*validationError { 78 | var errs []*validationError 79 | if o.Type == "" { 80 | errs = append(errs, newValidationError(joinLoc(location, "type"), ErrRequired)) 81 | } else { 82 | switch o.Type { 83 | case TypeApiKey: 84 | if o.Name == "" { 85 | errs = append(errs, newValidationError(joinLoc(location, "name"), ErrRequired)) 86 | } 87 | if o.In == "" { 88 | errs = append(errs, newValidationError(joinLoc(location, "in"), ErrRequired)) 89 | } else { 90 | switch o.In { 91 | case InQuery, InHeader, InCookie: 92 | default: 93 | errs = append(errs, newValidationError(joinLoc(location, "in"), "invalid value, expected one of [%s, %s, %s], but got '%s'", InQuery, InHeader, InCookie, o.In)) 94 | } 95 | } 96 | case TypeHTTP: 97 | if o.Scheme == "" { 98 | errs = append(errs, newValidationError(joinLoc(location, "scheme"), ErrRequired)) 99 | } 100 | case TypeOAuth2: 101 | if o.Flows == nil { 102 | errs = append(errs, newValidationError(joinLoc(location, "flows"), ErrRequired)) 103 | } else { 104 | errs = o.Flows.validateSpec(joinLoc(location, "flows"), validator) 105 | } 106 | case TypeOpenIDConnect: 107 | if o.OpenIDConnectURL == "" { 108 | errs = append(errs, newValidationError(joinLoc(location, "openIdConnectUrl"), ErrRequired)) 109 | } 110 | case TypeMutualTLS: 111 | default: 112 | errs = append(errs, newValidationError(joinLoc(location, "type"), "invalid value, expected one of [%s, %s, %s, %s, %s], but got '%s'", TypeApiKey, TypeHTTP, TypeMutualTLS, TypeOAuth2, TypeOpenIDConnect, o.Type)) 113 | } 114 | } 115 | return errs 116 | } 117 | 118 | type SecuritySchemeBuilder struct { 119 | spec *RefOrSpec[Extendable[SecurityScheme]] 120 | } 121 | 122 | func NewSecuritySchemeBuilder() *SecuritySchemeBuilder { 123 | return &SecuritySchemeBuilder{ 124 | spec: NewRefOrExtSpec[SecurityScheme](&SecurityScheme{}), 125 | } 126 | } 127 | 128 | func (b *SecuritySchemeBuilder) Build() *RefOrSpec[Extendable[SecurityScheme]] { 129 | return b.spec 130 | } 131 | 132 | func (b *SecuritySchemeBuilder) Extensions(v map[string]any) *SecuritySchemeBuilder { 133 | b.spec.Spec.Extensions = v 134 | return b 135 | } 136 | 137 | func (b *SecuritySchemeBuilder) AddExt(name string, value any) *SecuritySchemeBuilder { 138 | b.spec.Spec.AddExt(name, value) 139 | return b 140 | } 141 | 142 | func (b *SecuritySchemeBuilder) Type(v string) *SecuritySchemeBuilder { 143 | b.spec.Spec.Spec.Type = v 144 | return b 145 | } 146 | 147 | func (b *SecuritySchemeBuilder) Description(v string) *SecuritySchemeBuilder { 148 | b.spec.Spec.Spec.Description = v 149 | return b 150 | } 151 | 152 | func (b *SecuritySchemeBuilder) Name(v string) *SecuritySchemeBuilder { 153 | b.spec.Spec.Spec.Name = v 154 | return b 155 | } 156 | 157 | func (b *SecuritySchemeBuilder) In(v string) *SecuritySchemeBuilder { 158 | b.spec.Spec.Spec.In = v 159 | return b 160 | } 161 | 162 | func (b *SecuritySchemeBuilder) Scheme(v string) *SecuritySchemeBuilder { 163 | b.spec.Spec.Spec.Scheme = v 164 | return b 165 | } 166 | 167 | func (b *SecuritySchemeBuilder) BearerFormat(v string) *SecuritySchemeBuilder { 168 | b.spec.Spec.Spec.BearerFormat = v 169 | return b 170 | } 171 | 172 | func (b *SecuritySchemeBuilder) Flows(v *Extendable[OAuthFlows]) *SecuritySchemeBuilder { 173 | b.spec.Spec.Spec.Flows = v 174 | return b 175 | } 176 | 177 | func (b *SecuritySchemeBuilder) OpenIDConnectURL(v string) *SecuritySchemeBuilder { 178 | b.spec.Spec.Spec.OpenIDConnectURL = v 179 | return b 180 | } 181 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import "strings" 4 | 5 | // Server is an object representing a Server. 6 | // 7 | // https://spec.openapis.org/oas/v3.1.1#server-object 8 | // 9 | // Example: 10 | // 11 | // servers: 12 | // - url: https://development.gigantic-server.com/v1 13 | // description: Development server 14 | // - url: https://staging.gigantic-server.com/v1 15 | // description: Staging server 16 | // - url: https://api.gigantic-server.com/v1 17 | // description: Production server 18 | type Server struct { 19 | // A map between a variable name and its value. 20 | // The value is used for substitution in the server’s URL template. 21 | Variables map[string]*Extendable[ServerVariable] `json:"variables,omitempty" yaml:"variables,omitempty"` 22 | // REQUIRED. 23 | // A URL to the target host. 24 | // This URL supports Server Variables and MAY be relative, to indicate that the host location is relative 25 | // to the location where the OpenAPI document is being served. 26 | // Variable substitutions will be made when a variable is named in {brackets}. 27 | URL string `json:"url" yaml:"url"` 28 | // An optional string describing the host designated by the URL. 29 | // CommonMark syntax MAY be used for rich text representation. 30 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 31 | } 32 | 33 | func (o *Server) validateSpec(location string, validator *Validator) []*validationError { 34 | var errs []*validationError 35 | if o.URL == "" { 36 | errs = append(errs, newValidationError(joinLoc(location, "url"), ErrRequired)) 37 | } 38 | if l := len(o.Variables); l == 0 { 39 | if err := checkURL(o.URL); err != nil { 40 | errs = append(errs, newValidationError(joinLoc(location, "url"), err)) 41 | } 42 | } else { 43 | oldnew := make([]string, 0, l*2) 44 | for k, v := range o.Variables { 45 | errs = append(errs, v.validateSpec(joinLoc(location, "variables", k), validator)...) 46 | oldnew = append(oldnew, "{"+k+"}", v.Spec.Default) 47 | } 48 | u := strings.NewReplacer(oldnew...).Replace(o.URL) 49 | if err := checkURL(u); err != nil { 50 | errs = append(errs, newValidationError(joinLoc(location, "url"), err)) 51 | } 52 | } 53 | return errs 54 | } 55 | 56 | type ServerBuilder struct { 57 | spec *Extendable[Server] 58 | } 59 | 60 | func NewServerBuilder() *ServerBuilder { 61 | return &ServerBuilder{ 62 | spec: NewExtendable[Server](&Server{}), 63 | } 64 | } 65 | 66 | func (b *ServerBuilder) Build() *Extendable[Server] { 67 | return b.spec 68 | } 69 | 70 | func (b *ServerBuilder) Extensions(v map[string]any) *ServerBuilder { 71 | b.spec.Extensions = v 72 | return b 73 | } 74 | 75 | func (b *ServerBuilder) AddExt(name string, value any) *ServerBuilder { 76 | b.spec.AddExt(name, value) 77 | return b 78 | } 79 | 80 | func (b *ServerBuilder) Variables(v map[string]*Extendable[ServerVariable]) *ServerBuilder { 81 | b.spec.Spec.Variables = v 82 | return b 83 | } 84 | 85 | func (b *ServerBuilder) AddVariable(name string, value *Extendable[ServerVariable]) *ServerBuilder { 86 | if b.spec.Spec.Variables == nil { 87 | b.spec.Spec.Variables = make(map[string]*Extendable[ServerVariable], 1) 88 | } 89 | b.spec.Spec.Variables[name] = value 90 | return b 91 | } 92 | 93 | func (b *ServerBuilder) URL(v string) *ServerBuilder { 94 | b.spec.Spec.URL = v 95 | return b 96 | } 97 | 98 | func (b *ServerBuilder) Description(v string) *ServerBuilder { 99 | b.spec.Spec.Description = v 100 | return b 101 | } 102 | -------------------------------------------------------------------------------- /server_variable.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // ServerVariable is an object representing a Server Variable for server URL template substitution. 4 | // 5 | // https://spec.openapis.org/oas/v3.1.1#server-variable-object 6 | type ServerVariable struct { 7 | // REQUIRED. 8 | // The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. 9 | // Note this behavior is different than the Schema Object’s treatment of default values, 10 | // because in those cases parameter values are optional. 11 | // If the enum is defined, the value MUST exist in the enum’s values. 12 | Default string `json:"default" yaml:"default"` 13 | // An optional description for the server variable. 14 | // CommonMark syntax MAY be used for rich text representation. 15 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 16 | // An enumeration of string values to be used if the substitution options are from a limited set. 17 | // The array MUST NOT be empty. 18 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` 19 | } 20 | 21 | func (o *ServerVariable) validateSpec(location string, _ *Validator) []*validationError { 22 | var errs []*validationError 23 | if o.Default == "" { 24 | errs = append(errs, newValidationError(joinLoc(location, "default"), ErrRequired)) 25 | } 26 | return errs 27 | } 28 | 29 | type ServerVariableBuilder struct { 30 | spec *Extendable[ServerVariable] 31 | } 32 | 33 | func NewServerVariableBuilder() *ServerVariableBuilder { 34 | return &ServerVariableBuilder{ 35 | spec: NewExtendable[ServerVariable](&ServerVariable{}), 36 | } 37 | } 38 | 39 | func (b *ServerVariableBuilder) Build() *Extendable[ServerVariable] { 40 | return b.spec 41 | } 42 | 43 | func (b *ServerVariableBuilder) Extensions(v map[string]any) *ServerVariableBuilder { 44 | b.spec.Extensions = v 45 | return b 46 | } 47 | 48 | func (b *ServerVariableBuilder) AddExt(name string, value any) *ServerVariableBuilder { 49 | b.spec.AddExt(name, value) 50 | return b 51 | } 52 | 53 | func (b *ServerVariableBuilder) Default(v string) *ServerVariableBuilder { 54 | b.spec.Spec.Default = v 55 | return b 56 | } 57 | 58 | func (b *ServerVariableBuilder) Description(v string) *ServerVariableBuilder { 59 | b.spec.Spec.Description = v 60 | return b 61 | } 62 | 63 | func (b *ServerVariableBuilder) Enum(v ...string) *ServerVariableBuilder { 64 | b.spec.Spec.Enum = v 65 | return b 66 | } 67 | 68 | func (b *ServerVariableBuilder) AddEnum(v ...string) *ServerVariableBuilder { 69 | b.spec.Spec.Enum = append(b.spec.Spec.Enum, v...) 70 | return b 71 | } 72 | -------------------------------------------------------------------------------- /single_or_array.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // SingleOrArray holds list or single value 10 | type SingleOrArray[T any] []T 11 | 12 | // NewSingleOrArray creates SingleOrArray object. 13 | func NewSingleOrArray[T any](v ...T) *SingleOrArray[T] { 14 | vv := SingleOrArray[T](v) 15 | return &vv 16 | } 17 | 18 | // UnmarshalJSON implements json.Unmarshaler interface. 19 | func (o *SingleOrArray[T]) UnmarshalJSON(data []byte) error { 20 | var ret []T 21 | if json.Unmarshal(data, &ret) != nil { 22 | var s T 23 | if err := json.Unmarshal(data, &s); err != nil { 24 | return err 25 | } 26 | ret = []T{s} 27 | } 28 | *o = ret 29 | return nil 30 | } 31 | 32 | // MarshalJSON implements json.Marshaler interface. 33 | func (o *SingleOrArray[T]) MarshalJSON() ([]byte, error) { 34 | var v any = []T(*o) 35 | if len(*o) == 1 { 36 | v = (*o)[0] 37 | } 38 | return json.Marshal(&v) 39 | } 40 | 41 | // UnmarshalYAML implements yaml.Unmarshaler interface. 42 | func (o *SingleOrArray[T]) UnmarshalYAML(node *yaml.Node) error { 43 | var ret []T 44 | if node.Decode(&ret) != nil { 45 | var s T 46 | if err := node.Decode(&s); err != nil { 47 | return err 48 | } 49 | ret = []T{s} 50 | } 51 | *o = ret 52 | return nil 53 | } 54 | 55 | // MarshalYAML implements yaml.Marshaler interface. 56 | func (o *SingleOrArray[T]) MarshalYAML() (any, error) { 57 | var v any = []T(*o) 58 | if len(*o) == 1 { 59 | v = (*o)[0] 60 | } 61 | return v, nil 62 | } 63 | 64 | func (o *SingleOrArray[T]) Add(v ...T) *SingleOrArray[T] { 65 | *o = append(*o, v...) 66 | return o 67 | } 68 | -------------------------------------------------------------------------------- /single_or_array_test.go: -------------------------------------------------------------------------------- 1 | package openapi_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "gopkg.in/yaml.v3" 10 | 11 | "github.com/sv-tools/openapi" 12 | ) 13 | 14 | type singleOrArrayCase[T any] struct { 15 | name string 16 | data []byte 17 | expected *openapi.SingleOrArray[T] 18 | wantErr bool 19 | } 20 | 21 | func testSingleOrArrayJSON[T any](t *testing.T, tests []singleOrArrayCase[T]) { 22 | t.Helper() 23 | 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | t.Parallel() 27 | 28 | var o *openapi.SingleOrArray[T] 29 | err := json.Unmarshal(tt.data, &o) 30 | if tt.wantErr { 31 | require.Error(t, err) 32 | return 33 | } else { 34 | require.NoError(t, err) 35 | require.Equal(t, tt.expected, o) 36 | } 37 | newData, err := json.Marshal(&o) 38 | require.NoError(t, err) 39 | t.Log("orig: ", string(tt.data)) 40 | t.Log(" new: ", string(newData)) 41 | require.JSONEq(t, string(tt.data), string(newData)) 42 | }) 43 | } 44 | } 45 | 46 | func TestSingleOrArrayJSON(t *testing.T) { 47 | t.Run("string", func(t *testing.T) { 48 | testSingleOrArrayJSON(t, []singleOrArrayCase[string]{ 49 | { 50 | name: "single", 51 | data: []byte(`"single"`), 52 | expected: openapi.NewSingleOrArray("single"), 53 | }, 54 | { 55 | name: "multi", 56 | data: []byte(`["first","second"]`), 57 | expected: openapi.NewSingleOrArray("first", "second"), 58 | }, 59 | { 60 | name: "null", 61 | data: []byte(`null`), 62 | }, 63 | { 64 | name: "int for string", 65 | data: []byte(`42`), 66 | wantErr: true, 67 | }, 68 | { 69 | name: "array of int for string", 70 | data: []byte(`[42, 103]`), 71 | wantErr: true, 72 | }, 73 | { 74 | name: "empty for string", 75 | data: []byte(``), 76 | wantErr: true, 77 | }, 78 | }) 79 | }) 80 | 81 | t.Run("int", func(t *testing.T) { 82 | testSingleOrArrayJSON(t, []singleOrArrayCase[int]{ 83 | { 84 | name: "single", 85 | data: []byte(`1`), 86 | expected: openapi.NewSingleOrArray(1), 87 | }, 88 | { 89 | name: "multi", 90 | data: []byte(`[1,2]`), 91 | expected: openapi.NewSingleOrArray(1, 2), 92 | }, 93 | { 94 | name: "null", 95 | data: []byte(`null`), 96 | }, 97 | { 98 | name: "string for int", 99 | data: []byte(`"single"`), 100 | wantErr: true, 101 | }, 102 | { 103 | name: "array of string for int", 104 | data: []byte(`["first","second"]`), 105 | wantErr: true, 106 | }, 107 | { 108 | name: "empty for int", 109 | data: []byte(``), 110 | wantErr: true, 111 | }, 112 | }) 113 | }) 114 | 115 | type Foo struct { 116 | A string 117 | B int 118 | } 119 | t.Run("struct", func(t *testing.T) { 120 | testSingleOrArrayJSON(t, []singleOrArrayCase[Foo]{ 121 | { 122 | name: "single", 123 | data: []byte(`{"A":"single","B":42}`), 124 | expected: openapi.NewSingleOrArray(Foo{A: "single", B: 42}), 125 | }, 126 | { 127 | name: "multi", 128 | data: []byte(`[{"A":"first","B":1},{"A":"second","B":2}]`), 129 | expected: openapi.NewSingleOrArray(Foo{A: "first", B: 1}, Foo{A: "second", B: 2}), 130 | }, 131 | { 132 | name: "null", 133 | data: []byte(`null`), 134 | }, 135 | { 136 | name: "string for struct", 137 | data: []byte(`"single"`), 138 | wantErr: true, 139 | }, 140 | { 141 | name: "array of string for struct", 142 | data: []byte(`["first","second"]`), 143 | wantErr: true, 144 | }, 145 | { 146 | name: "empty for struct", 147 | data: []byte(``), 148 | wantErr: true, 149 | }, 150 | }) 151 | }) 152 | } 153 | 154 | func testSingleOrArrayYAML[T any](t *testing.T, tests []singleOrArrayCase[T]) { 155 | t.Helper() 156 | 157 | for _, tt := range tests { 158 | t.Run(tt.name, func(t *testing.T) { 159 | t.Parallel() 160 | 161 | var o *openapi.SingleOrArray[T] 162 | err := yaml.Unmarshal(tt.data, &o) 163 | if tt.wantErr { 164 | require.Error(t, err) 165 | return 166 | } else { 167 | require.NoError(t, err) 168 | require.Equal(t, tt.expected, o) 169 | } 170 | newData, err := yaml.Marshal(&o) 171 | newData = bytes.TrimSpace(newData) 172 | require.NoError(t, err) 173 | t.Log("orig: ", string(tt.data)) 174 | t.Log(" new: ", string(newData)) 175 | require.Equal(t, tt.data, newData) 176 | }) 177 | } 178 | } 179 | 180 | func TestSingleOrArrayYAML(t *testing.T) { 181 | t.Run("string", func(t *testing.T) { 182 | testSingleOrArrayYAML(t, []singleOrArrayCase[string]{ 183 | { 184 | name: "single", 185 | data: []byte(`single`), 186 | expected: openapi.NewSingleOrArray("single"), 187 | }, 188 | { 189 | name: "multi", 190 | data: []byte(`- first 191 | - second`), 192 | expected: openapi.NewSingleOrArray("first", "second"), 193 | }, 194 | }) 195 | }) 196 | 197 | t.Run("int", func(t *testing.T) { 198 | testSingleOrArrayYAML(t, []singleOrArrayCase[int]{ 199 | { 200 | name: "single", 201 | data: []byte(`1`), 202 | expected: openapi.NewSingleOrArray(1), 203 | }, 204 | { 205 | name: "multi", 206 | data: []byte(`- 1 207 | - 2`), 208 | expected: openapi.NewSingleOrArray(1, 2), 209 | }, 210 | { 211 | name: "string for int", 212 | data: []byte(`single`), 213 | wantErr: true, 214 | }, 215 | { 216 | name: "array of string for int", 217 | data: []byte(`- first 218 | - second`), 219 | wantErr: true, 220 | }, 221 | }) 222 | }) 223 | 224 | type Foo struct { 225 | A string 226 | B int 227 | } 228 | t.Run("struct", func(t *testing.T) { 229 | testSingleOrArrayYAML(t, []singleOrArrayCase[Foo]{ 230 | { 231 | name: "single", 232 | data: []byte(`a: single 233 | b: 42`), 234 | expected: openapi.NewSingleOrArray(Foo{A: "single", B: 42}), 235 | }, 236 | { 237 | name: "multi", 238 | data: []byte(`- a: first 239 | b: 1 240 | - a: second 241 | b: 2`), 242 | expected: openapi.NewSingleOrArray(Foo{A: "first", B: 1}, Foo{A: "second", B: 2}), 243 | }, 244 | { 245 | name: "string for struct", 246 | data: []byte(`single`), 247 | wantErr: true, 248 | }, 249 | { 250 | name: "array of string for struct", 251 | data: []byte(`- first 252 | - second`), 253 | wantErr: true, 254 | }, 255 | }) 256 | }) 257 | } 258 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // Tag adds metadata to a single tag that is used by the Operation Object. 4 | // It is not mandatory to have a Tag Object per tag defined in the Operation Object instances. 5 | // 6 | // https://spec.openapis.org/oas/v3.1.1#tag-object 7 | // 8 | // Example: 9 | // 10 | // name: pet 11 | // description: Pets operations 12 | type Tag struct { 13 | // Additional external documentation for this tag. 14 | ExternalDocs *Extendable[ExternalDocs] `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 15 | // REQUIRED. 16 | // The name of the tag. 17 | Name string `json:"name" yaml:"name"` 18 | // A description for the tag. 19 | // CommonMark syntax MAY be used for rich text representation. 20 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 21 | } 22 | 23 | func (o *Tag) validateSpec(location string, validator *Validator) []*validationError { 24 | var errs []*validationError 25 | if o.Name == "" { 26 | errs = append(errs, newValidationError(joinLoc(location, "name"), ErrRequired)) 27 | } 28 | if o.ExternalDocs != nil { 29 | errs = append(errs, o.ExternalDocs.validateSpec(joinLoc(location, "externalDocs"), validator)...) 30 | } 31 | validator.visited[joinLoc("tags", o.Name)] = true 32 | return errs 33 | } 34 | 35 | type TagBuilder struct { 36 | spec *Extendable[Tag] 37 | } 38 | 39 | func NewTagBuilder() *TagBuilder { 40 | return &TagBuilder{ 41 | spec: NewExtendable[Tag](&Tag{}), 42 | } 43 | } 44 | 45 | func (b *TagBuilder) Build() *Extendable[Tag] { 46 | return b.spec 47 | } 48 | 49 | func (b *TagBuilder) Extensions(v map[string]any) *TagBuilder { 50 | b.spec.Extensions = v 51 | return b 52 | } 53 | 54 | func (b *TagBuilder) AddExt(name string, value any) *TagBuilder { 55 | b.spec.AddExt(name, value) 56 | return b 57 | } 58 | 59 | func (b *TagBuilder) ExternalDocs(v *Extendable[ExternalDocs]) *TagBuilder { 60 | b.spec.Spec.ExternalDocs = v 61 | return b 62 | } 63 | 64 | func (b *TagBuilder) Name(v string) *TagBuilder { 65 | b.spec.Spec.Name = v 66 | return b 67 | } 68 | 69 | func (b *TagBuilder) Description(v string) *TagBuilder { 70 | b.spec.Spec.Description = v 71 | return b 72 | } 73 | -------------------------------------------------------------------------------- /testdata/api-with-examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Simple API overview", 5 | "version": "2.0.0" 6 | }, 7 | "paths": { 8 | "/": { 9 | "get": { 10 | "operationId": "listVersionsv2", 11 | "summary": "List API versions", 12 | "responses": { 13 | "200": { 14 | "description": "200 response", 15 | "content": { 16 | "application/json": { 17 | "schema": { 18 | "$ref": "#/components/schemas/Versions" 19 | }, 20 | "examples": { 21 | "foo": { 22 | "value": "{\n \"versions\": [\n {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v2/\",\n \"rel\": \"self\"\n }\n ]\n },\n {\n \"status\": \"EXPERIMENTAL\",\n \"updated\": \"2013-07-23T11:33:21Z\",\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n ]\n}\n" 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | "300": { 29 | "description": "300 response", 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "$ref": "#/components/schemas/Versions" 34 | }, 35 | "examples": { 36 | "foo": { 37 | "value": "{\n \"versions\": [\n {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v2/\",\n \"rel\": \"self\"\n }\n ]\n },\n {\n \"status\": \"EXPERIMENTAL\",\n \"updated\": \"2013-07-23T11:33:21Z\",\n \"id\": \"v3.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n ]\n}\n" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "/v2": { 47 | "get": { 48 | "operationId": "getVersionDetailsv2", 49 | "summary": "Show API version details", 50 | "responses": { 51 | "200": { 52 | "description": "200 response", 53 | "content": { 54 | "application/json": { 55 | "schema": { 56 | "$ref": "#/components/schemas/VersionDetails" 57 | }, 58 | "examples": { 59 | "foo": { 60 | "value": "{\n \"version\": {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"media-types\": [\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.compute+xml;version=2\"\n },\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.compute+json;version=2\"\n }\n ],\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://127.0.0.1:8774/v2/\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf\",\n \"type\": \"application/pdf\",\n \"rel\": \"describedby\"\n },\n {\n \"href\": \"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl\",\n \"type\": \"application/vnd.sun.wadl+xml\",\n \"rel\": \"describedby\"\n },\n {\n \"href\": \"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl\",\n \"type\": \"application/vnd.sun.wadl+xml\",\n \"rel\": \"describedby\"\n }\n ]\n }\n}\n" 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "203": { 67 | "description": "203 response", 68 | "content": { 69 | "application/json": { 70 | "schema": { 71 | "$ref": "#/components/schemas/VersionDetails" 72 | }, 73 | "examples": { 74 | "foo": { 75 | "value": "{\n \"version\": {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"media-types\": [\n {\n \"base\": \"application/xml\",\n \"type\": \"application/vnd.openstack.compute+xml;version=2\"\n },\n {\n \"base\": \"application/json\",\n \"type\": \"application/vnd.openstack.compute+json;version=2\"\n }\n ],\n \"id\": \"v2.0\",\n \"links\": [\n {\n \"href\": \"http://23.253.228.211:8774/v2/\",\n \"rel\": \"self\"\n },\n {\n \"href\": \"http://docs.openstack.org/api/openstack-compute/2/os-compute-devguide-2.pdf\",\n \"type\": \"application/pdf\",\n \"rel\": \"describedby\"\n },\n {\n \"href\": \"http://docs.openstack.org/api/openstack-compute/2/wadl/os-compute-2.wadl\",\n \"type\": \"application/vnd.sun.wadl+xml\",\n \"rel\": \"describedby\"\n }\n ]\n }\n}\n" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }, 85 | "components": { 86 | "schemas": { 87 | "Version": { 88 | "type": "object", 89 | "properties": { 90 | "status": { 91 | "type": "string" 92 | }, 93 | "updated": { 94 | "type": "string" 95 | }, 96 | "media-types": { 97 | "type": "array", 98 | "items": { 99 | "type": "object", 100 | "properties": { 101 | "base": { 102 | "type": "string" 103 | }, 104 | "type": { 105 | "type": "string" 106 | } 107 | }, 108 | "required": [ 109 | "base", 110 | "type" 111 | ] 112 | } 113 | }, 114 | "id": { 115 | "type": "string" 116 | }, 117 | "links": { 118 | "type": "array", 119 | "items": { 120 | "type": "object", 121 | "properties": { 122 | "href": { 123 | "type": "string" 124 | }, 125 | "rel": { 126 | "type": "string" 127 | }, 128 | "type": { 129 | "type": "string" 130 | } 131 | }, 132 | "required": [ 133 | "href", 134 | "rel" 135 | ] 136 | } 137 | } 138 | }, 139 | "required": [ 140 | "status", 141 | "updated", 142 | "id", 143 | "links" 144 | ] 145 | }, 146 | "Versions": { 147 | "type": "object", 148 | "properties": { 149 | "versions": { 150 | "type": "array", 151 | "items": { 152 | "$ref": "#/components/schemas/Version" 153 | } 154 | } 155 | }, 156 | "required": [ 157 | "versions" 158 | ] 159 | }, 160 | "VersionDetails": { 161 | "type": "object", 162 | "properties": { 163 | "version": { 164 | "$ref": "#/components/schemas/Version" 165 | } 166 | }, 167 | "required": [ 168 | "version" 169 | ] 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /testdata/callback-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Callback Example", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/streams": { 9 | "post": { 10 | "description": "subscribes a client to receive out-of-band data", 11 | "parameters": [ 12 | { 13 | "name": "callbackUrl", 14 | "in": "query", 15 | "required": true, 16 | "description": "the location where data will be sent. Must be network accessible\nby the source server\n", 17 | "schema": { 18 | "type": "string", 19 | "format": "uri", 20 | "example": "https://tonys-server.com" 21 | } 22 | } 23 | ], 24 | "responses": { 25 | "201": { 26 | "description": "subscription successfully created", 27 | "content": { 28 | "application/json": { 29 | "schema": { 30 | "description": "subscription information", 31 | "required": [ 32 | "subscriptionId" 33 | ], 34 | "properties": { 35 | "subscriptionId": { 36 | "description": "this unique identifier allows management of the subscription", 37 | "type": "string", 38 | "example": "2531329f-fb09-4ef7-887e-84e648214436" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "callbacks": { 47 | "onData": { 48 | "{$request.query.callbackUrl}/data": { 49 | "post": { 50 | "requestBody": { 51 | "description": "subscription payload", 52 | "content": { 53 | "application/json": { 54 | "schema": { 55 | "type": "object", 56 | "properties": { 57 | "timestamp": { 58 | "type": "string", 59 | "format": "date-time" 60 | }, 61 | "userData": { 62 | "type": "string" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | "responses": { 70 | "202": { 71 | "description": "Your server implementation should return this HTTP status code\nif the data was received successfully\n" 72 | }, 73 | "204": { 74 | "description": "Your server should return this HTTP status code if no longer interested\nin further updates\n" 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /testdata/callback-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Callback Example 4 | version: 1.0.0 5 | paths: 6 | /streams: 7 | post: 8 | description: subscribes a client to receive out-of-band data 9 | parameters: 10 | - name: callbackUrl 11 | in: query 12 | required: true 13 | description: | 14 | the location where data will be sent. Must be network accessible 15 | by the source server 16 | schema: 17 | type: string 18 | format: uri 19 | example: https://tonys-server.com 20 | responses: 21 | '201': 22 | description: subscription successfully created 23 | content: 24 | application/json: 25 | schema: 26 | description: subscription information 27 | required: 28 | - subscriptionId 29 | properties: 30 | subscriptionId: 31 | description: this unique identifier allows management of the subscription 32 | type: string 33 | example: 2531329f-fb09-4ef7-887e-84e648214436 34 | callbacks: 35 | # the name `onData` is a convenience locator 36 | onData: 37 | # when data is sent, it will be sent to the `callbackUrl` provided 38 | # when making the subscription PLUS the suffix `/data` 39 | '{$request.query.callbackUrl}/data': 40 | post: 41 | requestBody: 42 | description: subscription payload 43 | content: 44 | application/json: 45 | schema: 46 | type: object 47 | properties: 48 | timestamp: 49 | type: string 50 | format: date-time 51 | userData: 52 | type: string 53 | responses: 54 | '202': 55 | description: | 56 | Your server implementation should return this HTTP status code 57 | if the data was received successfully 58 | '204': 59 | description: | 60 | Your server should return this HTTP status code if no longer interested 61 | in further updates 62 | -------------------------------------------------------------------------------- /testdata/link-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Link Example 4 | version: 1.0.0 5 | paths: 6 | /2.0/users/{username}: 7 | get: 8 | operationId: getUserByName 9 | parameters: 10 | - name: username 11 | in: path 12 | required: true 13 | schema: 14 | type: string 15 | responses: 16 | '200': 17 | description: The User 18 | content: 19 | application/json: 20 | schema: 21 | $ref: '#/components/schemas/user' 22 | links: 23 | userRepositories: 24 | $ref: '#/components/links/UserRepositories' 25 | /2.0/repositories/{username}: 26 | get: 27 | operationId: getRepositoriesByOwner 28 | parameters: 29 | - name: username 30 | in: path 31 | required: true 32 | schema: 33 | type: string 34 | responses: 35 | '200': 36 | description: repositories owned by the supplied user 37 | content: 38 | application/json: 39 | schema: 40 | type: array 41 | items: 42 | $ref: '#/components/schemas/repository' 43 | links: 44 | userRepository: 45 | $ref: '#/components/links/UserRepository' 46 | /2.0/repositories/{username}/{slug}: 47 | get: 48 | operationId: getRepository 49 | parameters: 50 | - name: username 51 | in: path 52 | required: true 53 | schema: 54 | type: string 55 | - name: slug 56 | in: path 57 | required: true 58 | schema: 59 | type: string 60 | responses: 61 | '200': 62 | description: The repository 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/repository' 67 | links: 68 | repositoryPullRequests: 69 | $ref: '#/components/links/RepositoryPullRequests' 70 | /2.0/repositories/{username}/{slug}/pullrequests: 71 | get: 72 | operationId: getPullRequestsByRepository 73 | parameters: 74 | - name: username 75 | in: path 76 | required: true 77 | schema: 78 | type: string 79 | - name: slug 80 | in: path 81 | required: true 82 | schema: 83 | type: string 84 | - name: state 85 | in: query 86 | schema: 87 | type: string 88 | enum: 89 | - open 90 | - merged 91 | - declined 92 | responses: 93 | '200': 94 | description: an array of pull request objects 95 | content: 96 | application/json: 97 | schema: 98 | type: array 99 | items: 100 | $ref: '#/components/schemas/pullrequest' 101 | /2.0/repositories/{username}/{slug}/pullrequests/{pid}: 102 | get: 103 | operationId: getPullRequestsById 104 | parameters: 105 | - name: username 106 | in: path 107 | required: true 108 | schema: 109 | type: string 110 | - name: slug 111 | in: path 112 | required: true 113 | schema: 114 | type: string 115 | - name: pid 116 | in: path 117 | required: true 118 | schema: 119 | type: string 120 | responses: 121 | '200': 122 | description: a pull request object 123 | content: 124 | application/json: 125 | schema: 126 | $ref: '#/components/schemas/pullrequest' 127 | links: 128 | pullRequestMerge: 129 | $ref: '#/components/links/PullRequestMerge' 130 | /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge: 131 | post: 132 | operationId: mergePullRequest 133 | parameters: 134 | - name: username 135 | in: path 136 | required: true 137 | schema: 138 | type: string 139 | - name: slug 140 | in: path 141 | required: true 142 | schema: 143 | type: string 144 | - name: pid 145 | in: path 146 | required: true 147 | schema: 148 | type: string 149 | responses: 150 | '204': 151 | description: the PR was successfully merged 152 | components: 153 | links: 154 | UserRepositories: 155 | # returns array of '#/components/schemas/repository' 156 | operationId: getRepositoriesByOwner 157 | parameters: 158 | username: $response.body#/username 159 | UserRepository: 160 | # returns '#/components/schemas/repository' 161 | operationId: getRepository 162 | parameters: 163 | username: $response.body#/owner/username 164 | slug: $response.body#/slug 165 | RepositoryPullRequests: 166 | # returns '#/components/schemas/pullrequest' 167 | operationId: getPullRequestsByRepository 168 | parameters: 169 | username: $response.body#/owner/username 170 | slug: $response.body#/slug 171 | PullRequestMerge: 172 | # executes /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge 173 | operationId: mergePullRequest 174 | parameters: 175 | username: $response.body#/author/username 176 | slug: $response.body#/repository/slug 177 | pid: $response.body#/id 178 | schemas: 179 | user: 180 | type: object 181 | properties: 182 | username: 183 | type: string 184 | uuid: 185 | type: string 186 | repository: 187 | type: object 188 | properties: 189 | slug: 190 | type: string 191 | owner: 192 | $ref: '#/components/schemas/user' 193 | pullrequest: 194 | type: object 195 | properties: 196 | id: 197 | type: integer 198 | title: 199 | type: string 200 | repository: 201 | $ref: '#/components/schemas/repository' 202 | author: 203 | $ref: '#/components/schemas/user' 204 | -------------------------------------------------------------------------------- /testdata/non-oauth-scopes.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Non-oAuth Scopes example", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/users": { 9 | "get": { 10 | "security": [ 11 | { 12 | "bearerAuth": [ 13 | "read:users", 14 | "public" 15 | ] 16 | } 17 | ] 18 | } 19 | } 20 | }, 21 | "components": { 22 | "securitySchemes": { 23 | "bearerAuth": { 24 | "type": "http", 25 | "scheme": "bearer", 26 | "bearerFormat": "jwt", 27 | "description": "note: non-oauth scopes are not defined at the securityScheme level" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /testdata/non-oauth-scopes.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Non-oAuth Scopes example 4 | version: 1.0.0 5 | paths: 6 | /users: 7 | get: 8 | security: 9 | - bearerAuth: 10 | - 'read:users' 11 | - 'public' 12 | components: 13 | securitySchemes: 14 | bearerAuth: 15 | type: http 16 | scheme: bearer 17 | bearerFormat: jwt 18 | description: 'note: non-oauth scopes are not defined at the securityScheme level' 19 | -------------------------------------------------------------------------------- /testdata/petstore-expanded.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "description": "A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "name": "Swagger API Team", 10 | "email": "apiteam@swagger.io", 11 | "url": "http://swagger.io" 12 | }, 13 | "license": { 14 | "name": "Apache 2.0", 15 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html" 16 | } 17 | }, 18 | "servers": [ 19 | { 20 | "url": "https://petstore.swagger.io/v2" 21 | } 22 | ], 23 | "paths": { 24 | "/pets": { 25 | "get": { 26 | "description": "Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.\n\nSed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.\n", 27 | "operationId": "findPets", 28 | "parameters": [ 29 | { 30 | "name": "tags", 31 | "in": "query", 32 | "description": "tags to filter by", 33 | "style": "form", 34 | "schema": { 35 | "type": "array", 36 | "items": { 37 | "type": "string" 38 | } 39 | } 40 | }, 41 | { 42 | "name": "limit", 43 | "in": "query", 44 | "description": "maximum number of results to return", 45 | "schema": { 46 | "type": "integer", 47 | "format": "int32" 48 | } 49 | } 50 | ], 51 | "responses": { 52 | "200": { 53 | "description": "pet response", 54 | "content": { 55 | "application/json": { 56 | "schema": { 57 | "type": "array", 58 | "items": { 59 | "$ref": "#/components/schemas/Pet" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "default": { 66 | "description": "unexpected error", 67 | "content": { 68 | "application/json": { 69 | "schema": { 70 | "$ref": "#/components/schemas/Error" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "post": { 78 | "description": "Creates a new pet in the store. Duplicates are allowed", 79 | "operationId": "addPet", 80 | "requestBody": { 81 | "description": "Pet to add to the store", 82 | "content": { 83 | "application/json": { 84 | "schema": { 85 | "$ref": "#/components/schemas/NewPet" 86 | } 87 | } 88 | } 89 | }, 90 | "responses": { 91 | "200": { 92 | "description": "pet response", 93 | "content": { 94 | "application/json": { 95 | "schema": { 96 | "$ref": "#/components/schemas/Pet" 97 | } 98 | } 99 | } 100 | }, 101 | "default": { 102 | "description": "unexpected error", 103 | "content": { 104 | "application/json": { 105 | "schema": { 106 | "$ref": "#/components/schemas/Error" 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "/pets/{id}": { 115 | "get": { 116 | "description": "Returns a user based on a single ID, if the user does not have access to the pet", 117 | "operationId": "find pet by id", 118 | "parameters": [ 119 | { 120 | "name": "id", 121 | "in": "path", 122 | "description": "ID of pet to fetch", 123 | "required": true, 124 | "schema": { 125 | "type": "integer", 126 | "format": "int64" 127 | } 128 | } 129 | ], 130 | "responses": { 131 | "200": { 132 | "description": "pet response", 133 | "content": { 134 | "application/json": { 135 | "schema": { 136 | "$ref": "#/components/schemas/Pet" 137 | } 138 | } 139 | } 140 | }, 141 | "default": { 142 | "description": "unexpected error", 143 | "content": { 144 | "application/json": { 145 | "schema": { 146 | "$ref": "#/components/schemas/Error" 147 | } 148 | } 149 | } 150 | } 151 | } 152 | }, 153 | "delete": { 154 | "description": "deletes a single pet based on the ID supplied", 155 | "operationId": "deletePet", 156 | "parameters": [ 157 | { 158 | "name": "id", 159 | "in": "path", 160 | "description": "ID of pet to delete", 161 | "required": true, 162 | "schema": { 163 | "type": "integer", 164 | "format": "int64" 165 | } 166 | } 167 | ], 168 | "responses": { 169 | "204": { 170 | "description": "pet deleted" 171 | }, 172 | "default": { 173 | "description": "unexpected error", 174 | "content": { 175 | "application/json": { 176 | "schema": { 177 | "$ref": "#/components/schemas/Error" 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | } 185 | }, 186 | "components": { 187 | "schemas": { 188 | "Pet": { 189 | "allOf": [ 190 | { 191 | "$ref": "#/components/schemas/NewPet" 192 | }, 193 | { 194 | "type": "object", 195 | "required": [ 196 | "id" 197 | ], 198 | "properties": { 199 | "id": { 200 | "type": "integer", 201 | "format": "int64" 202 | } 203 | } 204 | } 205 | ] 206 | }, 207 | "NewPet": { 208 | "type": "object", 209 | "required": [ 210 | "name" 211 | ], 212 | "properties": { 213 | "name": { 214 | "type": "string" 215 | }, 216 | "tag": { 217 | "type": "string" 218 | } 219 | } 220 | }, 221 | "Error": { 222 | "type": "object", 223 | "required": [ 224 | "code", 225 | "message" 226 | ], 227 | "properties": { 228 | "code": { 229 | "type": "integer", 230 | "format": "int32" 231 | }, 232 | "message": { 233 | "type": "string" 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /testdata/petstore-expanded.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: https://petstore.swagger.io/v2 16 | paths: 17 | /pets: 18 | get: 19 | description: | 20 | Returns all pets from the system that the user has access to 21 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 22 | 23 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 24 | operationId: findPets 25 | parameters: 26 | - name: tags 27 | in: query 28 | description: tags to filter by 29 | style: form 30 | schema: 31 | type: array 32 | items: 33 | type: string 34 | - name: limit 35 | in: query 36 | description: maximum number of results to return 37 | schema: 38 | type: integer 39 | format: int32 40 | responses: 41 | '200': 42 | description: pet response 43 | content: 44 | application/json: 45 | schema: 46 | type: array 47 | items: 48 | $ref: '#/components/schemas/Pet' 49 | default: 50 | description: unexpected error 51 | content: 52 | application/json: 53 | schema: 54 | $ref: '#/components/schemas/Error' 55 | post: 56 | description: Creates a new pet in the store. Duplicates are allowed 57 | operationId: addPet 58 | requestBody: 59 | description: Pet to add to the store 60 | required: true 61 | content: 62 | application/json: 63 | schema: 64 | $ref: '#/components/schemas/NewPet' 65 | responses: 66 | '200': 67 | description: pet response 68 | content: 69 | application/json: 70 | schema: 71 | $ref: '#/components/schemas/Pet' 72 | default: 73 | description: unexpected error 74 | content: 75 | application/json: 76 | schema: 77 | $ref: '#/components/schemas/Error' 78 | /pets/{id}: 79 | get: 80 | description: Returns a user based on a single ID, if the user does not have access to the pet 81 | operationId: find pet by id 82 | parameters: 83 | - name: id 84 | in: path 85 | description: ID of pet to fetch 86 | required: true 87 | schema: 88 | type: integer 89 | format: int64 90 | responses: 91 | '200': 92 | description: pet response 93 | content: 94 | application/json: 95 | schema: 96 | $ref: '#/components/schemas/Pet' 97 | default: 98 | description: unexpected error 99 | content: 100 | application/json: 101 | schema: 102 | $ref: '#/components/schemas/Error' 103 | delete: 104 | description: deletes a single pet based on the ID supplied 105 | operationId: deletePet 106 | parameters: 107 | - name: id 108 | in: path 109 | description: ID of pet to delete 110 | required: true 111 | schema: 112 | type: integer 113 | format: int64 114 | responses: 115 | '204': 116 | description: pet deleted 117 | default: 118 | description: unexpected error 119 | content: 120 | application/json: 121 | schema: 122 | $ref: '#/components/schemas/Error' 123 | components: 124 | schemas: 125 | Pet: 126 | allOf: 127 | - $ref: '#/components/schemas/NewPet' 128 | - type: object 129 | required: 130 | - id 131 | properties: 132 | id: 133 | type: integer 134 | format: int64 135 | 136 | NewPet: 137 | type: object 138 | required: 139 | - name 140 | properties: 141 | name: 142 | type: string 143 | tag: 144 | type: string 145 | 146 | Error: 147 | type: object 148 | required: 149 | - code 150 | - message 151 | properties: 152 | code: 153 | type: integer 154 | format: int32 155 | message: 156 | type: string 157 | -------------------------------------------------------------------------------- /testdata/petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "Apache 2.0", 8 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html" 9 | } 10 | }, 11 | "servers": [ 12 | { 13 | "url": "http://petstore.swagger.io/v1" 14 | } 15 | ], 16 | "paths": { 17 | "/pets": { 18 | "get": { 19 | "summary": "List all pets", 20 | "operationId": "listPets", 21 | "tags": [ 22 | "pets" 23 | ], 24 | "parameters": [ 25 | { 26 | "name": "limit", 27 | "in": "query", 28 | "description": "How many items to return at one time (max 100)", 29 | "schema": { 30 | "type": "integer", 31 | "maximum": 100, 32 | "format": "int32" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "200": { 38 | "description": "A paged array of pets", 39 | "headers": { 40 | "x-next": { 41 | "description": "A link to the next page of responses", 42 | "schema": { 43 | "type": "string" 44 | } 45 | } 46 | }, 47 | "content": { 48 | "application/json": { 49 | "schema": { 50 | "$ref": "#/components/schemas/Pets" 51 | } 52 | } 53 | } 54 | }, 55 | "default": { 56 | "description": "unexpected error", 57 | "content": { 58 | "application/json": { 59 | "schema": { 60 | "$ref": "#/components/schemas/Error" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "post": { 68 | "summary": "Create a pet", 69 | "operationId": "createPets", 70 | "tags": [ 71 | "pets" 72 | ], 73 | "requestBody": { 74 | "content": { 75 | "application/json": { 76 | "schema": { 77 | "$ref": "#/components/schemas/Pet" 78 | } 79 | } 80 | }, 81 | "required": true 82 | }, 83 | "responses": { 84 | "201": { 85 | "description": "Null response" 86 | }, 87 | "default": { 88 | "description": "unexpected error", 89 | "content": { 90 | "application/json": { 91 | "schema": { 92 | "$ref": "#/components/schemas/Error" 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "/pets/{petId}": { 101 | "get": { 102 | "summary": "Info for a specific pet", 103 | "operationId": "showPetById", 104 | "tags": [ 105 | "pets" 106 | ], 107 | "parameters": [ 108 | { 109 | "name": "petId", 110 | "in": "path", 111 | "required": true, 112 | "description": "The id of the pet to retrieve", 113 | "schema": { 114 | "type": "string" 115 | } 116 | } 117 | ], 118 | "responses": { 119 | "200": { 120 | "description": "Expected response to a valid request", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Pet" 125 | } 126 | } 127 | } 128 | }, 129 | "default": { 130 | "description": "unexpected error", 131 | "content": { 132 | "application/json": { 133 | "schema": { 134 | "$ref": "#/components/schemas/Error" 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | }, 143 | "components": { 144 | "schemas": { 145 | "Pet": { 146 | "type": "object", 147 | "required": [ 148 | "id", 149 | "name" 150 | ], 151 | "properties": { 152 | "id": { 153 | "type": "integer", 154 | "format": "int64" 155 | }, 156 | "name": { 157 | "type": "string" 158 | }, 159 | "tag": { 160 | "type": "string" 161 | } 162 | } 163 | }, 164 | "Pets": { 165 | "type": "array", 166 | "maxItems": 100, 167 | "items": { 168 | "$ref": "#/components/schemas/Pet" 169 | } 170 | }, 171 | "Error": { 172 | "type": "object", 173 | "required": [ 174 | "code", 175 | "message" 176 | ], 177 | "properties": { 178 | "code": { 179 | "type": "integer", 180 | "format": "int32" 181 | }, 182 | "message": { 183 | "type": "string" 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /testdata/petstore.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: Apache 2.0 7 | url: https://www.apache.org/licenses/LICENSE-2.0.html 8 | servers: 9 | - url: http://petstore.swagger.io/v1 10 | paths: 11 | /pets: 12 | get: 13 | summary: List all pets 14 | operationId: listPets 15 | tags: 16 | - pets 17 | parameters: 18 | - name: limit 19 | in: query 20 | description: How many items to return at one time (max 100) 21 | schema: 22 | type: integer 23 | maximum: 100 24 | format: int32 25 | responses: 26 | '200': 27 | description: A paged array of pets 28 | headers: 29 | x-next: 30 | description: A link to the next page of responses 31 | schema: 32 | type: string 33 | content: 34 | application/json: 35 | schema: 36 | $ref: "#/components/schemas/Pets" 37 | default: 38 | description: unexpected error 39 | content: 40 | application/json: 41 | schema: 42 | $ref: "#/components/schemas/Error" 43 | post: 44 | summary: Create a pet 45 | operationId: createPets 46 | tags: 47 | - pets 48 | requestBody: 49 | content: 50 | application/json: 51 | schema: 52 | $ref: '#/components/schemas/Pet' 53 | required: true 54 | responses: 55 | '201': 56 | description: Null response 57 | default: 58 | description: unexpected error 59 | content: 60 | application/json: 61 | schema: 62 | $ref: "#/components/schemas/Error" 63 | /pets/{petId}: 64 | get: 65 | summary: Info for a specific pet 66 | operationId: showPetById 67 | tags: 68 | - pets 69 | parameters: 70 | - name: petId 71 | in: path 72 | required: true 73 | description: The id of the pet to retrieve 74 | schema: 75 | type: string 76 | responses: 77 | '200': 78 | description: Expected response to a valid request 79 | content: 80 | application/json: 81 | schema: 82 | $ref: "#/components/schemas/Pet" 83 | default: 84 | description: unexpected error 85 | content: 86 | application/json: 87 | schema: 88 | $ref: "#/components/schemas/Error" 89 | components: 90 | schemas: 91 | Pet: 92 | type: object 93 | required: 94 | - id 95 | - name 96 | properties: 97 | id: 98 | type: integer 99 | format: int64 100 | name: 101 | type: string 102 | tag: 103 | type: string 104 | Pets: 105 | type: array 106 | maxItems: 100 107 | items: 108 | $ref: "#/components/schemas/Pet" 109 | Error: 110 | type: object 111 | required: 112 | - code 113 | - message 114 | properties: 115 | code: 116 | type: integer 117 | format: int32 118 | message: 119 | type: string 120 | -------------------------------------------------------------------------------- /testdata/uspto.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | servers: 3 | - url: '{scheme}://developer.uspto.gov/ds-api' 4 | variables: 5 | scheme: 6 | description: 'The Data Set API is accessible via https and http' 7 | enum: 8 | - 'https' 9 | - 'http' 10 | default: 'https' 11 | info: 12 | description: >- 13 | The Data Set API (DSAPI) allows the public users to discover and search 14 | USPTO exported data sets. This is a generic API that allows USPTO users to 15 | make any CSV based data files searchable through API. With the help of GET 16 | call, it returns the list of data fields that are searchable. With the help 17 | of POST call, data can be fetched based on the filters on the field names. 18 | Please note that POST call is used to search the actual data. The reason for 19 | the POST call is that it allows users to specify any complex search criteria 20 | without worry about the GET size limitations as well as encoding of the 21 | input parameters. 22 | version: 1.0.0 23 | title: USPTO Data Set API 24 | contact: 25 | name: Open Data Portal 26 | url: 'https://developer.uspto.gov' 27 | email: developer@uspto.gov 28 | tags: 29 | - name: metadata 30 | description: Find out about the data sets 31 | - name: search 32 | description: Search a data set 33 | paths: 34 | /: 35 | get: 36 | tags: 37 | - metadata 38 | operationId: list-data-sets 39 | summary: List available data sets 40 | responses: 41 | '200': 42 | description: Returns a list of data sets 43 | content: 44 | application/json: 45 | schema: 46 | $ref: '#/components/schemas/dataSetList' 47 | example: 48 | { 49 | "total": 2, 50 | "apis": [ 51 | { 52 | "apiKey": "oa_citations", 53 | "apiVersionNumber": "v1", 54 | "apiUrl": "https://developer.uspto.gov/ds-api/oa_citations/v1/fields", 55 | "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/oa_citations.json" 56 | }, 57 | { 58 | "apiKey": "cancer_moonshot", 59 | "apiVersionNumber": "v1", 60 | "apiUrl": "https://developer.uspto.gov/ds-api/cancer_moonshot/v1/fields", 61 | "apiDocumentationUrl": "https://developer.uspto.gov/ds-api-docs/index.html?url=https://developer.uspto.gov/ds-api/swagger/docs/cancer_moonshot.json" 62 | } 63 | ] 64 | } 65 | /{dataset}/{version}/fields: 66 | get: 67 | tags: 68 | - metadata 69 | summary: >- 70 | Provides the general information about the API and the list of fields 71 | that can be used to query the dataset. 72 | description: >- 73 | This GET API returns the list of all the searchable field names that are 74 | in the oa_citations. Please see the 'fields' attribute which returns an 75 | array of field names. Each field or a combination of fields can be 76 | searched using the syntax options shown below. 77 | operationId: list-searchable-fields 78 | parameters: 79 | - name: dataset 80 | in: path 81 | description: 'Name of the dataset.' 82 | required: true 83 | example: "oa_citations" 84 | schema: 85 | type: string 86 | - name: version 87 | in: path 88 | description: Version of the dataset. 89 | required: true 90 | example: "v1" 91 | schema: 92 | type: string 93 | responses: 94 | '200': 95 | description: >- 96 | The dataset API for the given version is found and it is accessible 97 | to consume. 98 | content: 99 | application/json: 100 | schema: 101 | type: string 102 | '404': 103 | description: >- 104 | The combination of dataset name and version is not found in the 105 | system or it is not published yet to be consumed by public. 106 | content: 107 | application/json: 108 | schema: 109 | type: string 110 | /{dataset}/{version}/records: 111 | post: 112 | tags: 113 | - search 114 | summary: >- 115 | Provides search capability for the data set with the given search 116 | criteria. 117 | description: >- 118 | This API is based on Solr/Lucene Search. The data is indexed using 119 | SOLR. This GET API returns the list of all the searchable field names 120 | that are in the Solr Index. Please see the 'fields' attribute which 121 | returns an array of field names. Each field or a combination of fields 122 | can be searched using the Solr/Lucene Syntax. Please refer 123 | https://lucene.apache.org/core/3_6_2/queryparsersyntax.html#Overview for 124 | the query syntax. List of field names that are searchable can be 125 | determined using above GET api. 126 | operationId: perform-search 127 | parameters: 128 | - name: version 129 | in: path 130 | description: Version of the dataset. 131 | required: true 132 | schema: 133 | type: string 134 | default: v1 135 | - name: dataset 136 | in: path 137 | description: 'Name of the dataset. In this case, the default value is oa_citations' 138 | required: true 139 | schema: 140 | type: string 141 | default: oa_citations 142 | responses: 143 | '200': 144 | description: successful operation 145 | content: 146 | application/json: 147 | schema: 148 | type: array 149 | items: 150 | type: object 151 | additionalProperties: 152 | type: object 153 | '404': 154 | description: No matching record found for the given criteria. 155 | requestBody: 156 | content: 157 | application/x-www-form-urlencoded: 158 | schema: 159 | type: object 160 | properties: 161 | criteria: 162 | description: >- 163 | Uses Lucene Query Syntax in the format of 164 | propertyName:value, propertyName:[num1 TO num2] and date 165 | range format: propertyName:[yyyyMMdd TO yyyyMMdd]. In the 166 | response please see the 'docs' element which has the list of 167 | record objects. Each record structure would consist of all 168 | the fields and their corresponding values. 169 | type: string 170 | default: '*:*' 171 | start: 172 | description: Starting record number. Default value is 0. 173 | type: integer 174 | default: 0 175 | rows: 176 | description: >- 177 | Specify number of rows to be returned. If you run the search 178 | with default values, in the response you will see 'numFound' 179 | attribute which will tell the number of records available in 180 | the dataset. 181 | type: integer 182 | default: 100 183 | required: 184 | - criteria 185 | components: 186 | schemas: 187 | dataSetList: 188 | type: object 189 | properties: 190 | total: 191 | type: integer 192 | apis: 193 | type: array 194 | items: 195 | type: object 196 | properties: 197 | apiKey: 198 | type: string 199 | description: To be used as a dataset parameter value 200 | apiVersionNumber: 201 | type: string 202 | description: To be used as a version parameter value 203 | apiUrl: 204 | type: string 205 | format: uriref 206 | description: "The URL describing the dataset's fields" 207 | apiDocumentationUrl: 208 | type: string 209 | format: uriref 210 | description: A URL to the API console for each API 211 | -------------------------------------------------------------------------------- /testdata/webhook-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "Webhook Example", 5 | "version": "1.0.0" 6 | }, 7 | "webhooks": { 8 | "newPet": { 9 | "post": { 10 | "requestBody": { 11 | "description": "Information about a new pet in the system", 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "$ref": "#/components/schemas/Pet" 16 | } 17 | } 18 | } 19 | }, 20 | "responses": { 21 | "200": { 22 | "description": "Return a 200 status to indicate that the data was received successfully" 23 | } 24 | } 25 | } 26 | } 27 | }, 28 | "components": { 29 | "schemas": { 30 | "Pet": { 31 | "required": [ 32 | "id", 33 | "name" 34 | ], 35 | "properties": { 36 | "id": { 37 | "type": "integer", 38 | "format": "int64" 39 | }, 40 | "name": { 41 | "type": "string" 42 | }, 43 | "tag": { 44 | "type": "string" 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /testdata/webhook-example.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Webhook Example 4 | version: 1.0.0 5 | # Since OAS 3.1.0 the paths element isn't necessary. Now a valid OpenAPI Document can describe only paths, webhooks, or even only reusable components 6 | webhooks: 7 | # Each webhook needs a name 8 | newPet: 9 | # This is a Path Item Object, the only difference is that the request is initiated by the API provider 10 | post: 11 | requestBody: 12 | description: Information about a new pet in the system 13 | content: 14 | application/json: 15 | schema: 16 | $ref: "#/components/schemas/Pet" 17 | responses: 18 | "200": 19 | description: Return a 200 status to indicate that the data was received successfully 20 | 21 | components: 22 | schemas: 23 | Pet: 24 | required: 25 | - id 26 | - name 27 | properties: 28 | id: 29 | type: integer 30 | format: int64 31 | name: 32 | type: string 33 | tag: 34 | type: string 35 | -------------------------------------------------------------------------------- /type_formats.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | const ( 4 | // ******* Built-in OpenAPI formats ******* 5 | // 6 | // https://spec.openapis.org/oas/v3.1.1#data-types 7 | 8 | Int32Format = "int32" 9 | Int64Format = "int64" 10 | FloatFormat = "float" 11 | DoubleFormat = "double" 12 | PasswordFormat = "password" 13 | 14 | // ******* Built-in JSON Schema formats ******* 15 | // 16 | // https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats 17 | 18 | // DateTimeFormat is date and time together, for example, 2018-11-13T20:20:39+00:00. 19 | DateTimeFormat = "date-time" 20 | // TimeFormat is time, for example, 20:20:39+00:00 21 | TimeFormat = "time" 22 | // DateFormat is date, for example, 2018-11-13. 23 | DateFormat = "date" 24 | // DurationFormat is a duration as defined by the ISO 8601 ABNF for “duration”. 25 | // For example, P3D expresses a duration of 3 days. 26 | // 27 | // https://datatracker.ietf.org/doc/html/rfc3339#appendix-A 28 | DurationFormat = "duration" 29 | // EmailFormat is internet email address, see RFC 5321, section 4.1.2. 30 | // 31 | // https://tools.ietf.org/html/rfc5321#section-4.1.2 32 | EmailFormat = "email" 33 | // IDNEmailFormat is the internationalized form of an Internet email address, see RFC 6531. 34 | // 35 | // https://tools.ietf.org/html/rfc6531 36 | IDNEmailFormat = "idn-email" 37 | // HostnameFormat is internet host name, see RFC 1123, section 2.1. 38 | // 39 | // https://datatracker.ietf.org/doc/html/rfc1123#section-2.1 40 | HostnameFormat = "hostname" 41 | // IDNHostnameFormat is an internationalized Internet host name, see RFC5890, section 2.3.2.3. 42 | // 43 | // https://tools.ietf.org/html/rfc6531 44 | IDNHostnameFormat = "idn-hostname" 45 | // IPv4Format is IPv4 address, according to dotted-quad ABNF syntax as defined in RFC 2673, section 3.2. 46 | // 47 | // https://tools.ietf.org/html/rfc2673#section-3.2 48 | IPv4Format = "ipv4" 49 | // IPv6Format is IPv6 address, as defined in RFC 2373, section 2.2. 50 | // 51 | // https://tools.ietf.org/html/rfc2373#section-2.2 52 | IPv6Format = "ipv6" 53 | // UUIDFormat is a Universally Unique Identifier as defined by RFC 4122. 54 | // Example: 3e4666bf-d5e5-4aa7-b8ce-cefe41c7568a 55 | // 56 | // RFC 4122 57 | UUIDFormat = "uuid" 58 | // URIFormat is a universal resource identifier (URI), according to RFC3986. 59 | // 60 | // https://tools.ietf.org/html/rfc3986 61 | URIFormat = "uri" 62 | // URIReferenceFormat is a URI Reference (either a URI or a relative-reference), according to RFC3986, section 4.1. 63 | // 64 | // https://tools.ietf.org/html/rfc3986#section-4.1 65 | URIReferenceFormat = "uri-reference" 66 | // IRIFormat is the internationalized equivalent of a “uri”, according to RFC3987. 67 | // 68 | // https://tools.ietf.org/html/rfc3987 69 | IRIFormat = "iri" 70 | // IRIReferenceFormat is The internationalized equivalent of a “uri-reference”, according to RFC3987 71 | // 72 | // https://tools.ietf.org/html/rfc3987 73 | IRIReferenceFormat = "iri-reference" 74 | // URITemplateFormat is a URI Template (of any level) according to RFC6570. 75 | // If you don’t already know what a URI Template is, you probably don’t need this value. 76 | // 77 | // https://tools.ietf.org/html/rfc6570 78 | URITemplateFormat = "uri-template" 79 | // JsonPointerFormat is a JSON Pointer, according to RFC6901. 80 | // There is more discussion on the use of JSON Pointer within JSON Schema in Structuring a complex schema. 81 | // Note that this should be used only when the entire string contains only JSON Pointer content, e.g. /foo/bar. 82 | // JSON Pointer URI fragments, e.g. #/foo/bar/ should use "uri-reference". 83 | // 84 | // https://tools.ietf.org/html/rfc6901 85 | JsonPointerFormat = "json-pointer" 86 | // RelativeJsonPointerFormat is a relative JSON pointer. 87 | // 88 | // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01 89 | RelativeJsonPointerFormat = "relative-json-pointer" 90 | // RegexFormat is a regular expression, which should be valid according to the ECMA 262 dialect. 91 | // 92 | // https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf 93 | RegexFormat = "regex" 94 | ) 95 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | // ******* Type-specific keywords ******* 11 | // 12 | // https://json-schema.org/understanding-json-schema/reference/type.html 13 | 14 | // StringType is used for strings of text. It may contain Unicode characters. 15 | // 16 | // https://json-schema.org/understanding-json-schema/reference/string.html#string 17 | StringType = "string" 18 | // NumberType is used for any numeric type, either integers or floating point numbers. 19 | // 20 | // https://json-schema.org/understanding-json-schema/reference/numeric.html#number 21 | NumberType = "number" 22 | // IntegerType is used for integral numbers. 23 | // JSON does not have distinct types for integers and floating-point values. 24 | // Therefore, the presence or absence of a decimal point is not enough to distinguish between integers and non-integers. 25 | // For example, 1 and 1.0 are two ways to represent the same value in JSON. 26 | // JSON Schema considers that value an integer no matter which representation was used. 27 | // 28 | // https://json-schema.org/understanding-json-schema/reference/numeric.html#integer 29 | IntegerType = "integer" 30 | // ObjectType is the mapping type in JSON. 31 | // They map “keys” to “values”. 32 | // In JSON, the “keys” must always be strings. 33 | // Each of these pairs is conventionally referred to as a “property”. 34 | // 35 | // https://json-schema.org/understanding-json-schema/reference/object.html#object 36 | ObjectType = "object" 37 | // ArrayType is used for ordered elements. 38 | // In JSON, each element in an array may be of a different type. 39 | // 40 | // https://json-schema.org/understanding-json-schema/reference/array.html#array 41 | ArrayType = "array" 42 | // BooleanType matches only two special values: true and false. 43 | // Note that values that evaluate to true or false, such as 1 and 0, are not accepted by the schema. 44 | // 45 | // https://json-schema.org/understanding-json-schema/reference/boolean.html#boolean 46 | BooleanType = "boolean" 47 | // NullType has only one acceptable value: null. 48 | // 49 | // https://json-schema.org/understanding-json-schema/reference/null.html#null 50 | NullType = "null" 51 | 52 | // ******* Media: string-encoding non-JSON data ******* 53 | // 54 | // https://json-schema.org/understanding-json-schema/reference/non_json_data.html 55 | 56 | SevenBitEncoding = "7bit" 57 | EightBitEncoding = "8bit" 58 | BinaryEncoding = "binary" 59 | QuotedPrintableEncoding = "quoted-printable" 60 | Base16Encoding = "base16" 61 | Base32Encoding = "base32" 62 | Base64Encoding = "base64" 63 | ) 64 | 65 | // GetType returns the JSON Schema type of the given value. 66 | func GetType(v any) (string, error) { 67 | if v == nil { 68 | return NullType, nil 69 | } 70 | return kindToType(getKind(v)) 71 | } 72 | 73 | func getKind(v any) reflect.Kind { 74 | t := reflect.TypeOf(v) 75 | if t == nil { 76 | return reflect.Invalid 77 | } 78 | k := t.Kind() 79 | if k == reflect.Ptr { 80 | k = t.Elem().Kind() 81 | } 82 | return k 83 | } 84 | 85 | const unsupportedTypePrefix = "unsupported type: " 86 | 87 | type UnsupportedTypeError string 88 | 89 | func (e UnsupportedTypeError) Error() string { 90 | return unsupportedTypePrefix + string(e) 91 | } 92 | 93 | func (e UnsupportedTypeError) Is(target error) bool { 94 | return strings.HasPrefix(target.Error(), unsupportedTypePrefix) 95 | } 96 | 97 | func NewUnsupportedTypeError(v reflect.Kind) error { 98 | return UnsupportedTypeError(fmt.Sprintf("%T", v)) 99 | } 100 | 101 | func kindToType(k reflect.Kind) (string, error) { 102 | switch k { 103 | case reflect.String: 104 | return StringType, nil 105 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 106 | return IntegerType, nil 107 | case reflect.Float32, reflect.Float64: 108 | return NumberType, nil 109 | case reflect.Bool: 110 | return BooleanType, nil 111 | case reflect.Map, reflect.Struct: 112 | return ObjectType, nil 113 | case reflect.Slice, reflect.Array: 114 | return ArrayType, nil 115 | default: 116 | return "", NewUnsupportedTypeError(k) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /validation.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/mail" 9 | "net/url" 10 | "reflect" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/santhosh-tekuri/jsonschema/v6" 15 | ) 16 | 17 | // Validatable is an interface for validating the specification. 18 | type validatable interface { 19 | // an unexported method to be used by ValidateSpec function 20 | validateSpec(location string, validator *Validator) []*validationError 21 | } 22 | 23 | type visitedObjects map[string]bool 24 | 25 | func (o visitedObjects) String() string { 26 | keys := make([]string, 0, len(o)) 27 | for k := range o { 28 | keys = append(keys, k) 29 | } 30 | return strings.Join(keys, ", ") 31 | } 32 | 33 | type validationError struct { 34 | location string 35 | err error 36 | } 37 | 38 | func newValidationError(location string, err any, args ...any) *validationError { 39 | switch e := err.(type) { 40 | case error: 41 | return &validationError{location: location, err: e} 42 | case string: 43 | return &validationError{location: location, err: fmt.Errorf(e, args...)} //nolint:err113 // by design 44 | default: 45 | // unreachable 46 | panic(fmt.Sprintf("unsupported error type: %T", e)) 47 | } 48 | } 49 | 50 | var jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1") 51 | 52 | func joinLoc(base string, parts ...any) string { 53 | if len(parts) == 0 { 54 | return base 55 | } 56 | 57 | elems := append(make([]string, 0, len(parts)+1), base) 58 | for _, v := range parts { 59 | elems = append(elems, jsonPointerEscaper.Replace(fmt.Sprintf("%v", v))) 60 | } 61 | 62 | return strings.Join(elems, "/") 63 | } 64 | 65 | func (e *validationError) Error() string { 66 | return fmt.Sprintf("%s: %s", e.location, e.err) 67 | } 68 | 69 | func (e *validationError) Unwrap() error { 70 | return e.err 71 | } 72 | 73 | var ( 74 | ErrRequired = errors.New("required") 75 | ErrMutuallyExclusive = errors.New("mutually exclusive") 76 | ErrUnused = errors.New("unused") 77 | ) 78 | 79 | func checkURL(value string) error { 80 | if value == "" { 81 | return nil 82 | } 83 | if _, err := url.Parse(value); err != nil { 84 | return fmt.Errorf("invalid URL: %w", err) 85 | } 86 | return nil 87 | } 88 | 89 | func checkEmail(value string) error { 90 | if value == "" { 91 | return nil 92 | } 93 | if _, err := mail.ParseAddress(value); err != nil { 94 | return fmt.Errorf("invalid email: %w", err) 95 | } 96 | return nil 97 | } 98 | 99 | // Validator is a struct for validating the OpenAPI specification and a data. 100 | type Validator struct { 101 | spec *Extendable[OpenAPI] 102 | 103 | compiler *jsonschema.Compiler 104 | schemas sync.Map 105 | mu sync.Mutex 106 | 107 | opts *validationOptions 108 | visited visitedObjects 109 | linkToOperationID map[string]string 110 | } 111 | 112 | const specPrefix = "http://spec" 113 | 114 | // NewValidator creates an instance of Validator struct. 115 | // 116 | // The function creates new jsonschema comppiler and adds the given spec to the compiler. 117 | func NewValidator(spec *Extendable[OpenAPI], opts ...ValidationOption) (*Validator, error) { 118 | options := &validationOptions{} 119 | for _, opt := range opts { 120 | opt(options) 121 | } 122 | validator := &Validator{ 123 | spec: spec, 124 | schemas: sync.Map{}, 125 | opts: options, 126 | } 127 | data, err := json.Marshal(spec) 128 | if err != nil { 129 | return nil, fmt.Errorf("marshaling spec failed: %w", err) 130 | } 131 | doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(data)) 132 | if err != nil { 133 | return nil, fmt.Errorf("unmarshaling spec failed: %w", err) 134 | } 135 | compiler := jsonschema.NewCompiler() 136 | compiler.DefaultDraft(jsonschema.Draft2020) 137 | if err := compiler.AddResource(specPrefix, doc); err != nil { 138 | return nil, fmt.Errorf("adding spec to compiler failed: %w", err) 139 | } 140 | for _, f := range validator.opts.updateCompiler { 141 | f(compiler) 142 | } 143 | validator.compiler = compiler 144 | return validator, nil 145 | } 146 | 147 | // ValidateSpec validates the specification. 148 | func (v *Validator) ValidateSpec() error { 149 | // clear visited objects 150 | v.visited = make(visitedObjects) 151 | v.linkToOperationID = make(map[string]string) 152 | 153 | if errs := v.spec.validateSpec("", v); len(errs) > 0 { 154 | joinErrors := make([]error, len(errs)) 155 | for i := range errs { 156 | joinErrors[i] = errs[i] 157 | } 158 | return errors.Join(joinErrors...) 159 | } 160 | 161 | return nil 162 | } 163 | 164 | // ValidateData validates the given value against the schema located at the given location. 165 | // 166 | // The location should be in form of JSON Pointer. 167 | // The value can be a struct, a string containing JSON, or any other types. 168 | // If the value is a struct, it will be marshaled and unmarshaled to JSON. 169 | func (v *Validator) ValidateData(location string, value any) error { 170 | var schema *jsonschema.Schema 171 | if s, ok := v.schemas.Load(location); ok { 172 | schema = s.(*jsonschema.Schema) 173 | } else { 174 | var err error 175 | // use lambda to simplify the mutex unlocking code after the schema is compiled 176 | schema, err = func() (*jsonschema.Schema, error) { 177 | v.mu.Lock() 178 | defer v.mu.Unlock() 179 | if s, ok := v.schemas.Load(location); ok { 180 | return s.(*jsonschema.Schema), nil 181 | } else { 182 | if !strings.HasPrefix(location, "#") { 183 | location = "#" + location 184 | } 185 | schema, err := v.compiler.Compile(specPrefix + location) 186 | if err != nil { 187 | return nil, fmt.Errorf("compiling spec for given location %q failed: %w", location, err) 188 | } 189 | v.schemas.Store(location, schema) 190 | return schema, nil 191 | } 192 | }() 193 | if err != nil { 194 | return err 195 | } 196 | } 197 | 198 | switch getKind(value) { 199 | case reflect.Struct: 200 | // jsonschema does not support struct, so we need to marshal and unmarshal 201 | // the value to JSON representation (map[any]struct). 202 | data, err := json.Marshal(value) 203 | if err != nil { 204 | return fmt.Errorf("marshaling value failed: %w", err) 205 | } 206 | value, err = jsonschema.UnmarshalJSON(bytes.NewReader(data)) 207 | if err != nil { 208 | return fmt.Errorf("unmarshaling value failed: %w", err) 209 | } 210 | case reflect.String: 211 | if v.opts.validateDataAsJSON { 212 | // check if the value is already a JSON, if not keep it as is. 213 | s, err := jsonschema.UnmarshalJSON(strings.NewReader(value.(string))) 214 | if err == nil { 215 | value = s 216 | } 217 | } 218 | } 219 | return schema.Validate(value) 220 | } 221 | 222 | // ValidateDataAsJSON marshal and unmarshals the given value to JSON and 223 | // validates it against the schema located at the given location. 224 | // 225 | // If the value is a string, it will be unmarshaled to JSON first, if failed it will be kept as is. 226 | func (v *Validator) ValidateDataAsJSON(location string, value any) error { 227 | switch getKind(value) { 228 | // marshal and unmarshal the value to JSON representation (map[any]struct). 229 | case reflect.Struct: 230 | var err error 231 | value, err = ConvertToJSON(value) 232 | if err != nil { 233 | return err 234 | } 235 | // check if the value is already a JSON, if not keep it as is. 236 | case reflect.String: 237 | s, err := jsonschema.UnmarshalJSON(strings.NewReader(value.(string))) 238 | if err == nil { 239 | value = s 240 | } 241 | } 242 | return v.ValidateData(location, value) 243 | } 244 | 245 | func ConvertToJSON(value any) (any, error) { 246 | data, err := json.Marshal(value) 247 | if err != nil { 248 | return nil, fmt.Errorf("marshaling value failed: %w", err) 249 | } 250 | value, err = jsonschema.UnmarshalJSON(bytes.NewReader(data)) 251 | if err != nil { 252 | return nil, fmt.Errorf("unmarshaling value failed: %w", err) 253 | } 254 | return value, nil 255 | } 256 | -------------------------------------------------------------------------------- /validation_options.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | import "github.com/santhosh-tekuri/jsonschema/v6" 4 | 5 | type validationOptions struct { 6 | allowExtensionNameWithoutPrefix bool 7 | allowRequestBodyForGet bool 8 | allowRequestBodyForHead bool 9 | allowRequestBodyForDelete bool 10 | allowUndefinedTagsInOperation bool 11 | allowUnusedComponents bool 12 | doNotValidateExamples bool 13 | doNotValidateDefaultValues bool 14 | validateDataAsJSON bool 15 | updateCompiler []func(*jsonschema.Compiler) 16 | } 17 | 18 | // ValidationOption is a type for validation options. 19 | type ValidationOption func(*validationOptions) 20 | 21 | // AllowExtensionNameWithoutPrefix is a validation option to allow extension name without `x-` prefix. 22 | func AllowExtensionNameWithoutPrefix() ValidationOption { 23 | return func(v *validationOptions) { 24 | v.allowExtensionNameWithoutPrefix = true 25 | } 26 | } 27 | 28 | // AllowRequestBodyForGet is a validation option to allow request body for GET operation. 29 | func AllowRequestBodyForGet() ValidationOption { 30 | return func(v *validationOptions) { 31 | v.allowRequestBodyForGet = true 32 | } 33 | } 34 | 35 | // AllowRequestBodyForHead is a validation option to allow request body for HEAD operation. 36 | func AllowRequestBodyForHead() ValidationOption { 37 | return func(v *validationOptions) { 38 | v.allowRequestBodyForHead = true 39 | } 40 | } 41 | 42 | // AllowRequestBodyForDelete is a validation option to allow request body for DELETE operation. 43 | func AllowRequestBodyForDelete() ValidationOption { 44 | return func(v *validationOptions) { 45 | v.allowRequestBodyForDelete = true 46 | } 47 | } 48 | 49 | // AllowUndefinedTagsInOperation is a validation option to allow undefined tags in operation. 50 | func AllowUndefinedTagsInOperation() ValidationOption { 51 | return func(v *validationOptions) { 52 | v.allowUndefinedTagsInOperation = true 53 | } 54 | } 55 | 56 | // AllowUnusedComponents is a validation option to allow unused components. 57 | func AllowUnusedComponents() ValidationOption { 58 | return func(v *validationOptions) { 59 | v.allowUnusedComponents = true 60 | } 61 | } 62 | 63 | // DoNotValidateExamples is a validation option to skip examples validation. 64 | func DoNotValidateExamples() ValidationOption { 65 | return func(v *validationOptions) { 66 | v.doNotValidateExamples = true 67 | } 68 | } 69 | 70 | // DoNotValidateDefaultValues is a validation option to skip default values validation. 71 | func DoNotValidateDefaultValues() ValidationOption { 72 | return func(v *validationOptions) { 73 | v.doNotValidateDefaultValues = true 74 | } 75 | } 76 | 77 | func ValidateStringDataAsJSON() ValidationOption { 78 | return func(v *validationOptions) { 79 | v.validateDataAsJSON = true 80 | } 81 | } 82 | 83 | // UpdateCompiler is a type to modify the jsonschema.Compiler. 84 | func UpdateCompiler(f func(*jsonschema.Compiler)) ValidationOption { 85 | return func(v *validationOptions) { 86 | v.updateCompiler = append(v.updateCompiler, f) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /webhooks.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | type Webhooks = map[string]*RefOrSpec[Extendable[PathItem]] 4 | 5 | func NewWebhooks() Webhooks { 6 | return make(Webhooks) 7 | } 8 | -------------------------------------------------------------------------------- /xml.go: -------------------------------------------------------------------------------- 1 | package openapi 2 | 3 | // XML is a metadata object that allows for more fine-tuned XML model definitions. 4 | // When using arrays, XML element names are not inferred (for singular/plural forms) and the name property SHOULD 5 | // be used to add that information. 6 | // See examples for expected behavior. 7 | // 8 | // https://spec.openapis.org/oas/v3.1.1#xml-object 9 | // 10 | // Example: 11 | // 12 | // Person: 13 | // type: object 14 | // properties: 15 | // id: 16 | // type: integer 17 | // format: int32 18 | // xml: 19 | // attribute: true 20 | // name: 21 | // type: string 22 | // xml: 23 | // namespace: https://example.com/schema/sample 24 | // prefix: sample 25 | // 26 | // 27 | // example 28 | // 29 | type XML struct { 30 | // Replaces the name of the element/attribute used for the described schema property. 31 | // When defined within items, it will affect the name of the individual XML elements within the list. 32 | // When defined alongside type being array (outside the items), it will affect the wrapping element and only if wrapped is true. 33 | // If wrapped is false, it will be ignored. 34 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 35 | // The URI of the namespace definition. 36 | // This MUST be in the form of an absolute URI. 37 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 38 | // The prefix to be used for the name. 39 | Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` 40 | // Declares whether the property definition translates to an attribute instead of an element. Default value is false. 41 | Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` 42 | // MAY be used only for an array definition. 43 | // Signifies whether the array is wrapped (for example, ) or unwrapped (). 44 | // Default value is false. 45 | // The definition takes effect only when defined alongside type being array (outside the items). 46 | Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` 47 | } 48 | 49 | func (o *XML) validateSpec(path string, validator *Validator) []*validationError { 50 | return nil // nothing to validate 51 | } 52 | 53 | type XMLBuilder struct { 54 | spec *Extendable[XML] 55 | } 56 | 57 | func NewXMLBuilder() *XMLBuilder { 58 | return &XMLBuilder{ 59 | spec: NewExtendable[XML](&XML{}), 60 | } 61 | } 62 | 63 | func (b *XMLBuilder) Build() *Extendable[XML] { 64 | return b.spec 65 | } 66 | 67 | func (b *XMLBuilder) Extensions(v map[string]any) *XMLBuilder { 68 | b.spec.Extensions = v 69 | return b 70 | } 71 | 72 | func (b *XMLBuilder) AddExt(name string, value any) *XMLBuilder { 73 | b.spec.AddExt(name, value) 74 | return b 75 | } 76 | 77 | func (b *XMLBuilder) Name(v string) *XMLBuilder { 78 | b.spec.Spec.Name = v 79 | return b 80 | } 81 | 82 | func (b *XMLBuilder) Namespace(v string) *XMLBuilder { 83 | b.spec.Spec.Namespace = v 84 | return b 85 | } 86 | 87 | func (b *XMLBuilder) Prefix(v string) *XMLBuilder { 88 | b.spec.Spec.Prefix = v 89 | return b 90 | } 91 | 92 | func (b *XMLBuilder) Attribute(v bool) *XMLBuilder { 93 | b.spec.Spec.Attribute = v 94 | return b 95 | } 96 | 97 | func (b *XMLBuilder) Wrapped(v bool) *XMLBuilder { 98 | b.spec.Spec.Wrapped = v 99 | return b 100 | } 101 | --------------------------------------------------------------------------------