From 420c91b8b547fbb25721357c6c17ebbc6f386a87 Mon Sep 17 00:00:00 2001 From: Chance Date: Thu, 22 Sep 2022 01:16:44 -0400 Subject: [PATCH] Restructuring (#8) - Almost complete rewrite using generics - Adds loading with `$ref` resolution - Adds validation --- .gitignore | 2 +- .vscode/settings.json | 4 + README.md | 151 +- anchor.go | 70 + callback.go | 131 -- callback_test.go | 152 +- callbacks.go | 164 ++ component.go | 237 +++ component_map.go | 235 +++ component_slice.go | 178 ++ components.go | 285 +++- components_test.go | 156 +- contact.go | 75 +- contact_test.go | 88 +- discriminator.go | 99 +- discriminator_test.go | 118 +- doc.go | 6 +- document.go | 292 ++++ document_test.go | 450 +++++ encoding.go | 115 +- encoding_test.go | 113 +- errors.go | 201 +++ example.go | 149 +- example_test.go | 237 +-- extension.go | 84 +- external_docs.go | 100 +- go.mod | 32 +- go.sum | 70 +- header.go | 223 ++- header_test.go | 104 +- in.go | 15 +- info.go | 146 +- json.go | 125 ++ kind.go | 212 +++ license.go | 91 +- link.go | 234 ++- link_test.go | 96 +- load.go | 657 +++++++ load_test.go | 151 ++ location.go | 78 + location_test.go | 41 + map.go | 103 ++ media_type.go | 168 +- media_type_test.go | 148 +- method.go | 14 + node.go | 101 ++ number.go | 53 - oauth_flow.go | 312 +++- obj_map.go | 210 +++ obj_slice.go | 126 ++ openapi.go | 103 -- openapi_test.go | 387 ----- operation.go | 365 +++- operation_ref.go | 159 ++ operation_test.go | 222 +-- parameter.go | 354 ++-- parameter_test.go | 246 +-- parser.go | 1 + path.go | 239 --- path_item.go | 383 +++++ path_test.go | 213 ++- paths.go | 138 ++ primitives.go | 12 + ref.go | 75 + reference.go | 243 +-- reference_test.go | 27 +- request_body.go | 151 +- request_body_test.go | 158 +- response.go | 231 ++- response_test.go | 98 +- schema.go | 1348 ++++++++++----- schema/3.1.0/dialect/base.schema.json | 21 - schema/3.1.0/meta/base.schema.json | 79 - schema/3.1.0/schema-base.json | 24 - schema/3.1.0/schema.json | 1244 -------------- .../jsonschema/2019-09/meta/applicator.json | 56 + schema/jsonschema/2019-09/meta/content.json | 17 + schema/jsonschema/2019-09/meta/core.json | 57 + schema/jsonschema/2019-09/meta/format.json | 14 + .../jsonschema/2019-09/meta/hyper-schema.json | 29 + schema/jsonschema/2019-09/meta/meta-data.json | 37 + .../jsonschema/2019-09/meta/validation.json | 98 ++ schema/jsonschema/2019-09/schema.json | 42 + schema/jsonschema/2020-12/hyper-schema.json | 27 + schema/jsonschema/2020-12/links.json | 85 + .../jsonschema/2020-12/meta/applicator.json | 48 + schema/jsonschema/2020-12/meta/content.json | 17 + schema/jsonschema/2020-12/meta/core.json | 51 + .../2020-12/meta/format-annotation.json | 14 + .../2020-12/meta/format-assertion.json | 14 + .../jsonschema/2020-12/meta/hyper-schema.json | 29 + schema/jsonschema/2020-12/meta/meta-data.json | 37 + .../jsonschema/2020-12/meta/unevaluated.json | 15 + .../jsonschema/2020-12/meta/validation.json | 98 ++ schema/jsonschema/2020-12/schema.json | 58 + schema/openapi/3.1/3.0/schema.json | 1506 +++++++++++++++++ schema/openapi/3.1/dialect/base.schema.json | 25 + schema/openapi/3.1/meta/base.schema.json | 87 + schema/openapi/3.1/schema-base.json | 23 + schema/openapi/3.1/schema.json | 1420 ++++++++++++++++ schema_map.go | 213 +++ schema_map_test.go | 33 + schema_ref.go | 194 +++ schema_slice.go | 144 ++ schema_test.go | 388 +---- scope.go | 316 ++++ security.go | 187 -- security_requirement.go | 151 ++ security_scheme.go | 181 ++ server.go | 149 +- server_variable.go | 106 ++ style.go | 25 + tag.go | 92 +- tag_slice.go | 123 ++ testdata/documents/comprefs.yaml | 29 + testdata/documents/dynamic-refs.yaml | 5 + testdata/documents/petstore.yaml | 294 ++++ .../validation/fail/invalid_schema_types.yaml | 13 + .../validation/fail/no_containers.yaml | 7 + .../validation/fail/server_enum_empty.yaml | 14 + .../documents/validation/fail/servers.yaml | 11 + .../validation/fail/unknown_container.yaml | 8 + .../validation/pass/comp_pathitems.yaml | 6 + .../validation/pass/info_summary.yaml | 6 + .../validation/pass/license_identifier.yaml | 9 + testdata/documents/validation/pass/mega.yaml | 49 + .../validation/pass/minimal_comp.yaml | 5 + .../validation/pass/minimal_hooks.yaml | 5 + .../validation/pass/minimal_paths.yaml | 5 + .../validation/pass/path_no_response.yaml | 7 + .../pass/path_var_empty_pathitem.yaml | 6 + .../documents/validation/pass/schema.yaml | 55 + .../documents/validation/pass/servers.yaml | 10 + .../validation/pass/valid_schema_types.yaml | 14 + testdata/schemas/address.json | 34 + testdata/schemas/calendar.json | 47 + testdata/schemas/card.json | 99 ++ testdata/schemas/enable-toggle.json | 9 + testdata/schemas/geographic-location.json | 20 + testdata/schemas/list-of-strings.json | 12 + testdata/schemas/list-of-t.json | 12 + testdata/schemas/person.json | 21 + .../schemas/petstore-schema-map-test-1.json | 160 ++ testdata/schemas/tree.json | 17 + schema_type.go => type.go | 51 +- validate.go | 107 -- validator.go | 498 ++++++ validator_test.go | 98 ++ visitor.go | 111 ++ xml.go | 119 +- yamlutil/yamlutil.go | 108 -- 151 files changed, 16430 insertions(+), 5780 deletions(-) create mode 100644 anchor.go delete mode 100644 callback.go create mode 100644 callbacks.go create mode 100644 component.go create mode 100644 component_map.go create mode 100644 component_slice.go create mode 100644 document.go create mode 100644 document_test.go create mode 100644 errors.go create mode 100644 json.go create mode 100644 kind.go create mode 100644 load.go create mode 100644 load_test.go create mode 100644 location.go create mode 100644 location_test.go create mode 100644 map.go create mode 100644 method.go create mode 100644 node.go delete mode 100644 number.go create mode 100644 obj_map.go create mode 100644 obj_slice.go delete mode 100644 openapi.go delete mode 100644 openapi_test.go create mode 100644 operation_ref.go create mode 100644 parser.go delete mode 100644 path.go create mode 100644 path_item.go create mode 100644 paths.go create mode 100644 primitives.go create mode 100644 ref.go delete mode 100644 schema/3.1.0/dialect/base.schema.json delete mode 100644 schema/3.1.0/meta/base.schema.json delete mode 100644 schema/3.1.0/schema-base.json delete mode 100644 schema/3.1.0/schema.json create mode 100644 schema/jsonschema/2019-09/meta/applicator.json create mode 100644 schema/jsonschema/2019-09/meta/content.json create mode 100644 schema/jsonschema/2019-09/meta/core.json create mode 100644 schema/jsonschema/2019-09/meta/format.json create mode 100644 schema/jsonschema/2019-09/meta/hyper-schema.json create mode 100644 schema/jsonschema/2019-09/meta/meta-data.json create mode 100644 schema/jsonschema/2019-09/meta/validation.json create mode 100644 schema/jsonschema/2019-09/schema.json create mode 100644 schema/jsonschema/2020-12/hyper-schema.json create mode 100644 schema/jsonschema/2020-12/links.json create mode 100644 schema/jsonschema/2020-12/meta/applicator.json create mode 100644 schema/jsonschema/2020-12/meta/content.json create mode 100644 schema/jsonschema/2020-12/meta/core.json create mode 100644 schema/jsonschema/2020-12/meta/format-annotation.json create mode 100644 schema/jsonschema/2020-12/meta/format-assertion.json create mode 100644 schema/jsonschema/2020-12/meta/hyper-schema.json create mode 100644 schema/jsonschema/2020-12/meta/meta-data.json create mode 100644 schema/jsonschema/2020-12/meta/unevaluated.json create mode 100644 schema/jsonschema/2020-12/meta/validation.json create mode 100644 schema/jsonschema/2020-12/schema.json create mode 100644 schema/openapi/3.1/3.0/schema.json create mode 100644 schema/openapi/3.1/dialect/base.schema.json create mode 100644 schema/openapi/3.1/meta/base.schema.json create mode 100644 schema/openapi/3.1/schema-base.json create mode 100644 schema/openapi/3.1/schema.json create mode 100644 schema_map.go create mode 100644 schema_map_test.go create mode 100644 schema_ref.go create mode 100644 schema_slice.go create mode 100644 scope.go delete mode 100644 security.go create mode 100644 security_requirement.go create mode 100644 security_scheme.go create mode 100644 server_variable.go create mode 100644 style.go create mode 100644 tag_slice.go create mode 100644 testdata/documents/comprefs.yaml create mode 100644 testdata/documents/dynamic-refs.yaml create mode 100644 testdata/documents/petstore.yaml create mode 100644 testdata/documents/validation/fail/invalid_schema_types.yaml create mode 100644 testdata/documents/validation/fail/no_containers.yaml create mode 100644 testdata/documents/validation/fail/server_enum_empty.yaml create mode 100644 testdata/documents/validation/fail/servers.yaml create mode 100644 testdata/documents/validation/fail/unknown_container.yaml create mode 100644 testdata/documents/validation/pass/comp_pathitems.yaml create mode 100644 testdata/documents/validation/pass/info_summary.yaml create mode 100644 testdata/documents/validation/pass/license_identifier.yaml create mode 100644 testdata/documents/validation/pass/mega.yaml create mode 100644 testdata/documents/validation/pass/minimal_comp.yaml create mode 100644 testdata/documents/validation/pass/minimal_hooks.yaml create mode 100644 testdata/documents/validation/pass/minimal_paths.yaml create mode 100644 testdata/documents/validation/pass/path_no_response.yaml create mode 100644 testdata/documents/validation/pass/path_var_empty_pathitem.yaml create mode 100644 testdata/documents/validation/pass/schema.yaml create mode 100644 testdata/documents/validation/pass/servers.yaml create mode 100644 testdata/documents/validation/pass/valid_schema_types.yaml create mode 100644 testdata/schemas/address.json create mode 100644 testdata/schemas/calendar.json create mode 100644 testdata/schemas/card.json create mode 100644 testdata/schemas/enable-toggle.json create mode 100644 testdata/schemas/geographic-location.json create mode 100644 testdata/schemas/list-of-strings.json create mode 100644 testdata/schemas/list-of-t.json create mode 100644 testdata/schemas/person.json create mode 100644 testdata/schemas/petstore-schema-map-test-1.json create mode 100644 testdata/schemas/tree.json rename schema_type.go => type.go (80%) delete mode 100644 validate.go create mode 100644 validator.go create mode 100644 validator_test.go create mode 100644 visitor.go delete mode 100644 yamlutil/yamlutil.go diff --git a/.gitignore b/.gitignore index 66fd13c..c5fe1de 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,6 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out - +__debug_bin # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a1e6d4..f0ec798 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,6 +44,7 @@ "staticcheck": true, }, "files.exclude": { + ".git/**": true, "**/.git": true, "**/.svn": true, "**/.hg": true, @@ -53,4 +54,7 @@ "tabled": true, "tabled/**": true }, + "yaml.schemas": { + "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.1/schema.json": "file:///Users/chance/dev/openapi/testdata/documents/dynamicrefs.yaml" + }, } \ No newline at end of file diff --git a/README.md b/README.md index 6605f21..7b57f6c 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,136 @@ -# openapi +# openapi - an OpenAPI 3.x library for Go -Package openapi is a set of Go types for [OpenAPI Specification -3.1](https://spec.openapis.org/oas/v3.1.0). The primary purpose of the package -is to assist in generation of OpenAPI documentation or to offer building blocks -for code-generation. +openapi is a library for for OpenAPI 3.x ([3.1](https://spec.openapis.org/oas/v3.1.0), +[3.0](https://spec.openapis.org/oas/v3.0.3)). -## Documentation +The primary purpose of the package is to offer building blocks for code and +documentation generation. -[Documentation can be found on pkg.go.dev](https://pkg.go.dev/github.com/chanced/openapi). +:warning: This library is in an alpha state; expect breaking changes and bugs. + +## Features + +- `$ref` resolution +- All keys retain their order from the markup using slices of key/values which + aids with code generation. +- Validation ([see the validation seciton](#validation)) +- All non-primitive nodes have an absolute & relative location +- Strings are [text.Text](https://github.com/chanced/caps) which has case + conversions and `strings` functions as methods. +- Extensions, unknown JSON Schema keywords, examples, and a few other fields + are instances of [jsonx.RawMessage](https://github.com/chanced/jsonx) which + comes with a few helper methods. +- Supports both JSON and YAML + +## Issues + +- **Testing.** The code coverage is abysmal at the moment. As I find time, I'll add coverage. +- **`$dynamicRef` / `$dynamicAnchor`** is not really supported. While the + references are loaded, the dynamic overriding is not. I simply have no idea + how to solve it. If you have ideas, I'd really like to hear them. +- **Validation.** [See the Validation section](#validation). +- **Errors.** Errors and error messages need a lot of work. +- [jsonpointer](https://github.com/chanced/jsonpointer)'s Resolve, Assign, and + Delete do not currently work. I need to update the jsonpointer library + before its interfaces can be implemented for types within this library. +- Values of `$anchor` and `$dynamicAnchor` must be unique to a file. + Conditional `$dynamicAnchor` `$recursiveAnchor` are going to be challenging. + See below. +- `$dynamicRef` and `$recursiveRef` are incredibly tricky with respect to + static analysis, which is what this library was built for. You should avoid + conditional branches with `$dynamicAnchor`s within the same file. If you + need a conditional dynamics, move the branch into its own file and have the + conditional statement reference the branch. + +## Usage + +```go +package main + +import ( + "github.com/chanced/openapi" + "github.com/chanced/uri" + "github.com/santhosh-tekuri/jsonschema/v5" + "embed" + "io" + "path/filepath" + "log" +) + +//go:embed spec +var spec embed.FS + +func main() { + ctx := context.Background() + + c, err := openapi.SetupCompiler(jsonschema.NewCompiler()) // adding schema files + if err != nil { + log.Fatal(err) + } + v, err := openapi.NewValidator(c) + if err != nil { + log.Fatal(err) + } + + fn := func(_ context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error){ + f, err := schema.Open(fp) + if err != nil { + log.Fatal(err) + } + // you can return either JSON or YAML + d, err := io.ReadAll(f) + if err != nil{ + log.fatal(err) + } + // use the uri or the data to determine the Kind + return openapi.KindDocument, d, nil + } + // you can Load either JSON or YAML + // Load validates the Document as well. + doc, err := openapi.Load(ctx, "spec/openapi.yaml", v, fn) + if err != nil{ + log.Fatal(err) + } + _ = doc // *openapi.Document +} +``` ## Validation -Currently, specifications are validated with JSON Schema. Per OpenAPI's -documentation, this may not be enough to properly encapsulate all the nuances -of a specification. However, JSON Schema is able to properly validate the current -OpenAPI 3.1 Specification test suite. +The standard validator (`StdValidator`) currently validates OpenAPI documents +with JSON Schema. Per OpenAPI's documentation, this may not be enough to +properly encapsulate all the nuances of a specification. However, JSON Schema is +able to successfully validate the current OpenAPI 3.1 Specification test suite. + +Validation something that needs work. If you have an edge case that is not +covered, you can implement your own Validator either by wrapping `StdValidator` +or simply creating your own. + +If you do find cases where the current validator is not sufficient, please open +an issue so that the library can be updated with proper coverage in the future. + +Regarding JSON Schema, as of writing this, the only library able to support JSON +Schema 2020-12 is +[github.com/santhosh-tekuri/jsonschema](https://github.com/santhosh-tekuri/jsonschema) +and so the `Compiler`'s interface was modeled after its API. If you would like +to use a different implementation of JSON Schema with the `StdValidator` the +interfaces you need to write an adapter for are: + +```go +type Compiler interface { + AddResource(id string, r io.Reader) error + Compile(url string) (CompiledSchema, error) +} -Please open an issue if you run into an edge case that is not validated adequately. +type CompiledSchema interface { + Validate(data interface{}) error +} +``` ## Contributions -Please feel free to open up an issue or create a pull request if there are features -you'd like to contribute or issues. - -## Dependencies - -- [github.com/santhosh-tekuri/jsonschema/v5](https://github.com/santhosh-tekuri/jsonschema/v5) (used for json schema validation) -- [github.com/evanphx/json-patch/v5](https://github.com/evanphx/json-patch/v5) (used for testing purposes) -- [github.com/stretchr/testify](https://github.com/stretchr/testify) (testing) -- [github.com/tidwall/gjson](https://github.com/tidwall/gjson) (json parsing) -- [github.com/tidwall/sjson](https://github.com/tidwall/sjson) (json manipulation) -- [github.com/wI2L/jsondiff](https://github.com/wI2L/jsondiff) (testing purposes) -- [gopkg.in/yaml.v2](https://github.com/wI2L/jsondiff) (yaml) -- [sigs.k8s.io/yaml](https://sigs.k8s.io/yaml) (yaml) -- [github.com/chanced/cmpjson](https://github.com/chanced/cmpjson) (testing purposes) -- [github.com/chanced/dynamic](https://github.com/chanced/dynamic) (json parsing) -- [github.com/pkg/errors](https://github.com/pkg/errors) (errors) +Please feel free to open up an issue or create a pull request if you find issues +or if there are features you'd like to see added. ## License diff --git a/anchor.go b/anchor.go new file mode 100644 index 0000000..57b0e07 --- /dev/null +++ b/anchor.go @@ -0,0 +1,70 @@ +package openapi + +import "fmt" + +type DuplicateAnchorError struct { + A *Anchor + B *Anchor +} + +func (dae *DuplicateAnchorError) Error() string { + return fmt.Sprintf("duplicate anchor: %s", dae.A.Name) +} + +type AnchorType uint8 + +const ( + AnchorTypeUndefined AnchorType = iota + AnchorTypeRegular // $anchor + AnchorTypeRecursive // $recursiveAnchor + AnchorTypeDynamic // $dynamicAnchor +) + +type Anchor struct { + Location + In *Schema + Name Text + Type AnchorType +} + +type Anchors struct { + Standard map[Text]Anchor // $anchor + Recursive *Anchor // $recursiveAnchor + Dynamic map[Text]Anchor // $dynamicAnchor +} + +func (a *Anchors) merge(b *Anchors, err error) (*Anchors, error) { + if err != nil { + return nil, err + } + if b == nil { + return a, nil + } + + // we do not merge recursive anchors as they must be at the root of the + // document. This method is only called when merging schemas from nested + // components, so we can, and should, drop them from result if not coming + // from a. + + if a == nil { + return &Anchors{ + Standard: b.Standard, + Dynamic: b.Dynamic, + }, nil + } + for k, bv := range b.Standard { + if av, ok := a.Standard[k]; ok { + return nil, &DuplicateAnchorError{&av, &bv} + } + a.Standard[k] = bv + } + + for k, bv := range b.Dynamic { + if av, ok := a.Dynamic[k]; ok { + return nil, &DuplicateAnchorError{&av, &bv} + } + a.Dynamic[k] = bv + } + + return a, nil +} diff --git a/callback.go b/callback.go deleted file mode 100644 index e6e1b97..0000000 --- a/callback.go +++ /dev/null @@ -1,131 +0,0 @@ -package openapi - -import ( - "encoding/json" - - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/gjson" -) - -// CallbackKind indicates whether the CallbackObj is a Callback or a Reference -type CallbackKind uint8 - -const ( - // CallbackKindObj = CallbackObj - CallbackKindObj CallbackKind = iota - // CallbackKindRef = Reference - CallbackKindRef -) - -// CallbackObj is map of possible out-of band callbacks related to the parent -// operation. Each value in the map is a Path Item Object that describes a set -// of requests that may be initiated by the API provider and the expected -// responses. The key value used to identify the path item object is an -// expression, evaluated at runtime, that identifies a URL to use for the -// callback operation. -// -// To describe incoming requests from the API provider independent from another -// API call, use the webhooks field. -type CallbackObj struct { - Paths PathItems `json:"-"` - Extensions `json:"-"` -} - -type callback CallbackObj - -// MarshalJSON marshals JSON -func (c CallbackObj) MarshalJSON() ([]byte, error) { - b, err := json.Marshal(c.Paths) - if err != nil { - return b, err - } - return marshalExtendedJSONInto(b, callback(c)) -} - -// UnmarshalJSON unmarshals JSON -func (c *CallbackObj) UnmarshalJSON(data []byte) error { - *c = CallbackObj{ - Paths: PathItems{}, - Extensions: Extensions{}, - } - var err error - gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { - d := []byte(value.Raw) - if IsExtensionKey(key.String()) { - c.Extensions.SetEncodedExtension(key.String(), d) - } else { - var v Path - v, err = unmarshalPathJSON(d) - c.Paths[key.String()] = v - } - if err != nil { - return false - } - return true - }) - return err -} - -// MarshalYAML marshals YAML -func (c CallbackObj) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(c) -} - -// UnmarshalYAML unmarshals YAML -func (c *CallbackObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, c) -} - -// CallbackKind returns CallbackKindCallback -func (c *CallbackObj) CallbackKind() CallbackKind { return CallbackKindObj } - -// ResolveCallback resolves CallbackObj by returning itself. resolve is not called. -func (c *CallbackObj) ResolveCallback(CallbackResolver) (*CallbackObj, error) { - return c, nil -} - -// Callback can either be a CallbackObj or a Reference -type Callback interface { - ResolveCallback(CallbackResolver) (*CallbackObj, error) - CallbackKind() CallbackKind -} - -// Callbacks is a map of reusable Callback Objects. -type Callbacks map[string]Callback - -// UnmarshalJSON unmarshals JSON -func (c *Callbacks) UnmarshalJSON(data []byte) error { - var o map[string]json.RawMessage - res := make(Callbacks, len(o)) - err := json.Unmarshal(data, &o) - if err != nil { - return err - } - for k, d := range o { - if isRefJSON(d) { - v, err := unmarshalReferenceJSON(d) - if err != nil { - return err - } - res[k] = v - } else { - var v CallbackObj - if err := json.Unmarshal(d, &v); err != nil { - return err - } - res[k] = &v - } - } - *c = res - return nil -} - -// MarshalYAML marshals YAML -func (c Callbacks) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(c) -} - -// UnmarshalYAML unmarshals YAML -func (c *Callbacks) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, c) -} diff --git a/callback_test.go b/callback_test.go index fdc8b6c..9296088 100644 --- a/callback_test.go +++ b/callback_test.go @@ -1,81 +1,81 @@ package openapi_test -import ( - "encoding/json" - "testing" +// import ( +// "encoding/json" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// ) -func TestCallback(t *testing.T) { - assert := require.New(t) - cbj := [][]byte{ - []byte(`{ - "{$request.query.queryUrl}": { - "post": { - "requestBody": { - "description": "Callback payload", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SomePayload" - } - } - } - }, - "responses": { - "200": { - "description": "callback successfully processed" - } - } - } - }, - "x-test": "value" - }`), - } - for _, data := range cbj { - var v openapi.CallbackObj - err := json.Unmarshal(data, &v) - assert.NoError(err) - b, err := json.MarshalIndent(&v, "", " ") - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - } +// func TestCallback(t *testing.T) { +// assert := require.New(t) +// cbj := [][]byte{ +// []byte(`{ +// "{$request.query.queryUrl}": { +// "post": { +// "requestBody": { +// "description": "Callback payload", +// "content": { +// "application/json": { +// "schema": { +// "$ref": "#/components/schemas/SomePayload" +// } +// } +// } +// }, +// "responses": { +// "200": { +// "description": "callback successfully processed" +// } +// } +// } +// }, +// "x-test": "value" +// }`), +// } +// for _, data := range cbj { +// var v openapi.Callback +// err := json.Unmarshal(data, &v) +// assert.NoError(err) +// b, err := json.MarshalIndent(&v, "", " ") +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// } - cbjs := [][]byte{ - []byte(`{ - "myCallback": { - "{$request.query.queryUrl}": { - "post": { - "requestBody": { - "description": "Callback payload", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SomePayload" - } - } - } - }, - "responses": { - "200": { - "description": "callback successfully processed" - } - } - } - } - } - }`), - } - for _, data := range cbjs { - var v openapi.Callbacks - err := json.Unmarshal(data, &v) - assert.NoError(err) - b, err := json.MarshalIndent(&v, "", " ") - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - } -} +// cbjs := [][]byte{ +// []byte(`{ +// "myCallback": { +// "{$request.query.queryUrl}": { +// "post": { +// "requestBody": { +// "description": "Callback payload", +// "content": { +// "application/json": { +// "schema": { +// "$ref": "#/components/schemas/SomePayload" +// } +// } +// } +// }, +// "responses": { +// "200": { +// "description": "callback successfully processed" +// } +// } +// } +// } +// } +// }`), +// } +// for _, data := range cbjs { +// var v openapi.CallbackMap +// err := json.Unmarshal(data, &v) +// assert.NoError(err) +// b, err := json.MarshalIndent(&v, "", " ") +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// } +// } diff --git a/callbacks.go b/callbacks.go new file mode 100644 index 0000000..9e5146b --- /dev/null +++ b/callbacks.go @@ -0,0 +1,164 @@ +package openapi + +import ( + "encoding/json" + "strings" + + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +// CallbacksMap is a map of reusable Callback Objects. +type CallbacksMap = ComponentMap[*Callbacks] + +// Callbacks is map of possible out-of band callbacks related to the parent +// operation. Each value in the map is a Path Item Object that describes a set +// of requests that may be initiated by the API provider and the expected +// responses. The key value used to identify the path item object is an +// expression, evaluated at runtime, that identifies a URL to use for the +// callback operation. +// +// To describe incoming requests from the API provider independent from another +// API call, use the webhooks field. +type Callbacks struct { + Extensions `json:"-"` + PathItems `json:"-"` +} + +func (c *Callbacks) Nodes() []Node { + if c == nil { + return nil + } + return downcastNodes(c.nodes()) +} + +func (c *Callbacks) nodes() []node { + if c == nil { + return nil + } + edges := make([]node, 0, 1) + edges = appendEdges(edges, c.PathItems.nodes()...) + return edges +} + +func (c *Callbacks) ref() Ref { return nil } + +// Edges returns the immediate edges of the Node. This is used to build a +// graph of the OpenAPI document. +// + +// kind returns KindCallback +func (*Callbacks) Kind() Kind { return KindCallbacks } +func (*Callbacks) mapKind() Kind { return KindCallbacksMap } +func (Callbacks) sliceKind() Kind { return KindUndefined } +func (c *Callbacks) isNil() bool { + return c == nil +} + +func (c *Callbacks) Anchors() (*Anchors, error) { + if c == nil { + return nil, nil + } + return c.PathItems.Anchors() +} + +func (c *Callbacks) Refs() []Ref { + if c == nil { + return nil + } + return c.PathItems.Refs() +} + +// // ResolveNodeByPointer performs a l +// func (c *Callbacks) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return c.resolveNodeByPointer(ptr) +// } + +// func (c *Callbacks) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return c, nil +// } +// nxt, tok, _ := ptr.Next() +// item := c.Items.Get(Text(tok)) +// if item == nil { +// return nil, newErrNotFound(c.Location.AbsoluteLocation(), tok) +// } +// return item.resolveNodeByPointer(nxt) +// } + +func (c *Callbacks) location() Location { + return c.Location +} + +// MarshalJSON marshals JSON +func (c Callbacks) MarshalJSON() ([]byte, error) { + type callback Callbacks + return marshalExtendedJSON(callback(c)) +} + +// UnmarshalJSON unmarshals JSON +func (c *Callbacks) UnmarshalJSON(data []byte) error { + *c = Callbacks{ + Extensions: Extensions{}, + } + + var err error + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + if strings.HasPrefix(key.String(), "x-") { + c.SetRawExtension(Text(key.String()), []byte(value.Raw)) + } else { + var v PathItem + err = json.Unmarshal([]byte(value.Raw), &v) + c.Set(Text(key.String()), &v) + } + return err == nil + }) + return err +} + +func (c *Callbacks) setLocation(loc Location) error { + if c == nil { + return nil + } + c.Location = loc + return c.PathItems.setLocation(loc) +} + +func (c Callbacks) MarshalYAML() (interface{}, error) { + j, err := c.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (c *Callbacks) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, c) +} + +// func (c *Callbacks) Walk(v Visitor) error { +// if v == nil { +// return nil +// } +// v, err := v.Visit(c) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } + +// return c.Items.Walk(v) +// } +func (c *Callbacks) refable() {} + +var _ node = (*Callbacks)(nil) diff --git a/component.go b/component.go new file mode 100644 index 0000000..e00a09b --- /dev/null +++ b/component.go @@ -0,0 +1,237 @@ +package openapi + +import ( + "encoding/json" + "fmt" + + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + +type Component[T refable] struct { + Location + Reference *Reference[T] + Object T +} + +func (c *Component[T]) nodes() []node { + if c == nil { + return nil + } + if c.IsReference() { + return appendEdges(nil, c.Reference) + } + return appendEdges(nil, c.Object) +} + +// Edges returns the immediate edges of the Node. This is used to build a +// graph of the OpenAPI document. +// + +// IsResolved implements Ref +func (c *Component[T]) IsResolved() bool { + if c == nil || c.Reference == nil { + return true + } + return !c.Object.isNil() +} + +// URI implements Ref +func (c *Component[T]) URI() *uri.URI { + if !c.IsReference() { + return nil + } + return c.Reference.Ref +} + +// MakeReference converts the Component into a refernce, altering the path of +// all nested nodes. +func (c *Component[T]) MakeReference(ref uri.URI) error { + if c.Object.isNil() { + return fmt.Errorf("cannot make reference to nil object") + } + c.Reference.dst = &c.Object + c.Reference.Ref = &ref + loc, err := NewLocation(ref) + if err != nil { + return err + } + + return c.Object.setLocation(loc) +} + +func (c *Component[T]) Kind() Kind { + switch c.ObjectKind() { + case KindExample: + return KindExampleComponent + case KindHeader: + return KindHeaderComponent + case KindServer: + return KindServerComponent + case KindLink: + return KindLinkComponent + case KindResponse: + return KindResponseComponent + case KindParameter: + return KindParameterComponent + case KindPathItem: + return KindPathItemComponent + case KindRequestBody: + return KindRequestBodyComponent + case KindCallbacks: + return KindCallbacksComponent + case KindSecurityScheme: + return KindSecuritySchemeComponent + default: + return KindUndefined + } +} + +func (c *Component[T]) location() Location { + if c.Reference != nil { + return c.Reference.Location + } + return c.Object.location() +} + +// IsRef returns false +// + +// IsReference returns true if this Component contains a Reference +func (c *Component[T]) IsReference() bool { return !c.Reference.isNil() } + +func (c *Component[T]) Refs() []Ref { + if c == nil { + return nil + } + if c.IsReference() { + return []Ref{c.Reference} + } + return c.Object.Refs() +} + +func (*Component[T]) mapKind() Kind { + var t T + return t.mapKind() +} + +func (*Component[T]) sliceKind() Kind { + var t T + return t.sliceKind() +} + +func (c Component[T]) MarshalJSON() ([]byte, error) { + if c.Reference != nil { + return json.Marshal(c.Reference) + } + if any(c.Object) != nil { + return c.Object.MarshalJSON() + } + return nil, nil +} + +// ComponentKind returns the Kind of the containing Object, regardless of if it +// is referenced or not. +func (c *Component[T]) ObjectKind() Kind { + return c.Object.Kind() +} + +func (c *Component[T]) UnmarshalJSON(data []byte) error { + if isRefJSON(data) { + var ref Reference[T] + if err := json.Unmarshal(data, &ref); err != nil { + return err + } + + ref.ReferencedKind = c.ObjectKind() + ref.dst = &c.Object + c.Reference = &ref + + *c = Component[T]{ + Reference: &ref, + } + return nil + } + var obj T + + k := obj.Kind() + _ = k + + if err := json.Unmarshal(data, &obj); err != nil { + return err + } + *c = Component[T]{ + Object: obj, + } + return nil +} + +func (c *Component[T]) MarshalYAML() (interface{}, error) { + j, err := c.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (c *Component[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, c) +} + +func (c *Component[T]) setLocation(loc Location) error { + if c == nil { + return nil + } + c.Location = loc + if c.Reference != nil { + return c.Reference.setLocation(loc) + } else if !c.Object.isNil() { + return c.Object.setLocation(loc) + } + return nil +} + +func (c *Component[T]) Anchors() (*Anchors, error) { + if c == nil { + return nil, nil + } + if c.Reference != nil { + return nil, nil + } + return c.Object.Anchors() +} + +func (c *Component[T]) isNil() bool { return c == nil } + +var _ node = (*Component[*Response])(nil) + +// func (c *Component[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return c.resolveNodeByPointer(ptr) +// } + +// func (c *Component[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return c, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "$ref": +// if nxt.IsRoot() { +// return c.Reference, nil +// } +// return nil, newErrNotResolvable(c.Location.AbsoluteLocation(), tok) +// default: +// // TODO: this may need to change. Not sure when I need to perform these +// // resolutions just yet. If before population, Object may be nil at this call. +// return c.Object.resolveNodeByPointer(nxt) +// } +// } diff --git a/component_map.go b/component_map.go new file mode 100644 index 0000000..10624fe --- /dev/null +++ b/component_map.go @@ -0,0 +1,235 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +// ComponentEntry is an entry in a ComponentMap consisting of a Key/Value pair for +// an object consiting of Component[T]s +type ComponentEntry[V refable] struct { + Key Text + Component *Component[V] +} + +// ComponentMap is a pseudo map consisting of Components with type T. +// +// Unlike a regular map, ComponentMap maintains the order of the map's +// fields. +// +// Under the hood, ComponentMap is of a slice of ComponentField[T] +type ComponentMap[T refable] struct { + Location + Items []*ComponentEntry[T] +} + +func (cm *ComponentMap[T]) nodes() []node { + if cm == nil { + return nil + } + var edges []node + for _, item := range cm.Items { + edges = appendEdges(edges, item.Component) + } + return edges +} + +func (cm *ComponentMap[T]) Refs() []Ref { + if cm == nil { + return nil + } + var refs []Ref + for _, item := range cm.Items { + refs = append(refs, item.Component.Refs()...) + } + return refs +} + +func (cm ComponentMap[T]) Map() map[Text]*Component[T] { + m := make(map[Text]*Component[T], len(cm.Items)) + for _, item := range cm.Items { + m[item.Key] = item.Component + } + return m +} + +func (*ComponentMap[T]) Kind() Kind { + var t T + return t.mapKind() +} +func (*ComponentMap[T]) mapKind() Kind { return KindUndefined } +func (*ComponentMap[T]) sliceKind() Kind { return KindUndefined } + +func (cm *ComponentMap[T]) UnmarshalJSON(data []byte) error { + if !jsonx.IsObject(data) { + return &json.UnmarshalTypeError{ + Value: jsonx.TypeOf(data).String(), + Type: reflect.TypeOf(cm), + Struct: "ComponentMap", + } + } + var err error + *cm = ComponentMap[T]{ + Items: make([]*ComponentEntry[T], 0), + } + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + var comp Component[T] + err = comp.UnmarshalJSON([]byte(value.Raw)) + cm.Items = append(cm.Items, &ComponentEntry[T]{ + Key: Text(key.String()), + Component: &comp, + }) + return err == nil + }) + return err +} + +func (cm *ComponentMap[T]) isNil() bool { + return cm == nil +} + +// MarshalJSON marshals JSON +func (cm ComponentMap[T]) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.WriteByte('{') + for _, field := range cm.Items { + if b.Len() > 1 { + b.WriteByte(',') + } + jsonx.EncodeAndWriteString(&b, field.Key) + b.WriteByte(':') + cb, err := field.Component.MarshalJSON() + if err != nil { + return nil, err + } + b.Write(cb) + } + b.WriteByte('}') + return b.Bytes(), nil +} + +func (cm *ComponentMap[T]) Get(key Text) *Component[T] { + if cm == nil || cm.Items == nil { + return nil + } + for _, v := range cm.Items { + if v.Key == key { + return v.Component + } + } + return nil +} + +// Set sets the value of the key in the ComponentMap +func (cm *ComponentMap[T]) Set(key Text, value *Component[T]) { + if cm == nil { + *cm = ComponentMap[T]{} + } + for i, v := range cm.Items { + if v.Key == key { + cm.Items[i] = &ComponentEntry[T]{ + Key: key, + Component: value, + } + } + } + cm.Items = append(cm.Items, &ComponentEntry[T]{ + Key: key, + Component: value, + }) +} + +func (cm *ComponentMap[T]) Del(key Text) { + for i, v := range cm.Items { + if v.Key == key { + cm.Items = append(cm.Items[:i], cm.Items[i+1:]...) + return + } + } +} + +func (cm *ComponentMap[T]) MarshalYAML() (interface{}, error) { + j, err := cm.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (cm *ComponentMap[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, cm) +} + +func (cm *ComponentMap[T]) setLocation(loc Location) error { + if cm == nil { + return nil + } + cm.Location = loc + for _, kv := range cm.Items { + if err := kv.Component.setLocation(loc.AppendLocation(kv.Key.String())); err != nil { + return err + } + } + + return nil +} + +func (cm *ComponentMap[T]) Anchors() (*Anchors, error) { + if cm == nil { + return nil, nil + } + var anchors *Anchors + var err error + + for _, kv := range cm.Items { + if anchors, err = kv.Component.Anchors(); err != nil { + return nil, err + } + } + return anchors, nil +} + +var _ node = (*ComponentMap[*Response])(nil) + +// func (cm ComponentMap[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return cm.resolveNodeByPointer(ptr) +// } + +// func (c *ComponentMap[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return c, nil +// } +// nxt, tok, _ := ptr.Next() +// n := c.Get(Text(tok)) + +// if nxt.IsRoot() { +// if n == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// if n.Reference != nil { +// return n.Reference, nil +// } +// if !n.Object.isNil() { +// return n.Object, nil +// } +// return nil, newErrNotFound(c.Location.AbsoluteLocation(), tok) +// } +// if n == nil { +// return nil, newErrNotFound(c.Location.AbsoluteLocation(), tok) +// } +// return n.resolveNodeByPointer(nxt) +// } diff --git a/component_slice.go b/component_slice.go new file mode 100644 index 0000000..2f5fb8b --- /dev/null +++ b/component_slice.go @@ -0,0 +1,178 @@ +package openapi + +import ( + "encoding/json" + "strconv" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +// ComponentSlice is a slice of Components of type T +type ComponentSlice[T refable] struct { + Location `json:"-"` + Items []*Component[T] `json:"-"` +} + +func (cs *ComponentSlice[T]) nodes() []node { + if cs == nil { + return nil + } + edges := make([]node, 0, len(cs.Items)) + for _, item := range cs.Items { + edges = appendEdges(edges, item) + } + return edges +} + +func (*ComponentSlice[T]) ref() Ref { return nil } + +func (cs *ComponentSlice[T]) Refs() []Ref { + if cs == nil { + return nil + } + var refs []Ref + for _, item := range cs.Items { + refs = append(refs, item.Refs()...) + } + return refs +} + +func (ComponentSlice[T]) Kind() Kind { + var t T + return t.Kind() +} + +// func (cs ComponentSlice[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } + +// return cs.resolveNodeByPointer(ptr) +// } + +// func (cs *ComponentSlice[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return cs, nil +// } +// nxt, tok, _ := ptr.Next() +// idx, err := tok.Int() +// if err != nil { +// return nil, err +// } +// if idx < 0 || idx >= len(cs.Items) { +// return nil, newErrNotFound(cs.Location.AbsoluteLocation(), tok) +// } +// return cs.Items[idx].resolveNodeByPointer(nxt) +// } + +func (cs ComponentSlice[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(cs.Items) +} + +func (cs *ComponentSlice[T]) UnmarshalJSON(data []byte) error { + var items []*Component[T] + if err := json.Unmarshal(data, &items); err != nil { + return err + } + *cs = ComponentSlice[T]{ + Items: items, + } + return nil +} + +func (*ComponentSlice[T]) mapKind() Kind { return KindUndefined } + +func (ComponentSlice[T]) sliceKind() Kind { + var t T + return t.sliceKind() +} + +func (cs *ComponentSlice[T]) setLocation(loc Location) error { + if cs == nil { + return nil + } + cs.Location = loc + for i, c := range cs.Items { + if err := c.setLocation(loc.AppendLocation(strconv.Itoa(i))); err != nil { + return err + } + } + return nil +} + +func (cs *ComponentSlice[T]) Anchors() (*Anchors, error) { + if cs == nil { + return nil, nil + } + var anchors *Anchors + var err error + for _, item := range cs.Items { + if anchors, err = item.Anchors(); err != nil { + return nil, err + } + } + return anchors, nil +} + +func (cs *ComponentSlice[T]) MarshalYAML() (interface{}, error) { + j, err := cs.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (cs *ComponentSlice[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, cs) +} + +func (cs *ComponentSlice[T]) isNil() bool { return cs == nil } + +var _ node = (*ComponentSlice[*Response])(nil) + +// func (cs *ComponentSlice[T]) Walk(v Visitor) error { +// var t T +// var err error +// v, err = v.Visit(cs) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } +// switch t.Kind() { +// case KindParameter: +// return cs.walkParameters(v) +// case KindServer: +// return cs.walkServers(v) +// default: + +// } +// } + +// func (cs *ComponentSlice[T]) walkParameters(v Visitor) error { +// var err error +// ps, ok := (any)(cs).(*ComponentSlice[*Parameter]) +// if !ok { +// // shouldn't happen +// panic(fmt.Sprintf("%T is not a *ComponentSlice[*Parameter]", cs)) +// } +// v, err = v.VisitParameterSlice(ps) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } +// for _, p := range ps.Items { +// if err = p.Walk(v); err != nil { +// return err +// } +// } +// } diff --git a/components.go b/components.go index 34d046a..07dcd45 100644 --- a/components.go +++ b/components.go @@ -1,43 +1,265 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +// ComponentMap is a pseudo map consisting of Components with type T. + +func newComponent[T refable](ref *Reference[T], obj T) Component[T] { + return Component[T]{ + Reference: ref, + Object: obj, + } +} // Components holds a set of reusable objects for different aspects of the OAS. // All objects defined within the components object will have no effect on the // API unless they are explicitly referenced from properties outside the // components object. type Components struct { - // An object to hold reusable Schema Objects. - Schemas *Schemas `json:"schemas,omitempty"` - // An object to hold reusable Response Objects. - Responses *Responses `json:"responses,omitempty"` - // An object to hold reusable Parameter Objects. - Parameters *Parameters `json:"parameters,omitempty"` - // An object to hold reusable Example Objects. - Examples *Examples `json:"examples,omitempty"` - // An object to hold reusable Request Body Objects. - RequestBodies *RequestBodies `json:"requestBodies,omitempty"` - // An object to hold reusable Header Objects. - Headers *Headers `json:"headers,omitempty"` - // An object to hold reusable Security Scheme Objects. - SecuritySchemes *SecuritySchemes `json:"securitySchemes,omitempty"` - // An object to hold reusable Link Objects. - Links *Links `json:"links,omitempty"` - // An object to hold reusable Callback Objects. - Callbacks *Callbacks `json:"callbacks,omitempty"` - // An object to hold reusable Path Item Object. - PathItems *PathItems `json:"pathItems,omitempty"` + // OpenAPI extensions Extensions `json:"-"` + Location `json:"-"` + + Schemas *SchemaMap `json:"schemas,omitempty"` + Responses *ResponseMap `json:"responses,omitempty"` + Parameters *ParameterMap `json:"parameters,omitempty"` + RequestBodies *RequestBodyMap `json:"requestBodies,omitempty"` + Headers *HeaderMap `json:"headers,omitempty"` + SecuritySchemes *SecuritySchemeMap `json:"securitySchemes,omitempty"` + Links *LinkMap `json:"links,omitempty"` + Callbacks *CallbacksMap `json:"callbacks,omitempty"` + PathItems *PathItemMap `json:"pathItems,omitempty"` + Examples *ExampleMap `json:"examples,omitempty"` // +} + +func (*Components) Kind() Kind { return KindComponents } + +func (c *Components) Refs() []Ref { + if c == nil { + return nil + } + var refs []Ref + if c.Schemas != nil { + refs = append(refs, c.Schemas.Refs()...) + } + if c.Responses != nil { + refs = append(refs, c.Responses.Refs()...) + } + if c.Parameters != nil { + refs = append(refs, c.Parameters.Refs()...) + } + if c.Examples != nil { + refs = append(refs, c.Examples.Refs()...) + } + if c.RequestBodies != nil { + refs = append(refs, c.RequestBodies.Refs()...) + } + if c.Headers != nil { + refs = append(refs, c.Headers.Refs()...) + } + if c.SecuritySchemes != nil { + refs = append(refs, c.SecuritySchemes.Refs()...) + } + if c.Links != nil { + refs = append(refs, c.Links.Refs()...) + } + if c.Callbacks != nil { + refs = append(refs, c.Callbacks.Refs()...) + } + if c.PathItems != nil { + refs = append(refs, c.PathItems.Refs()...) + } + return refs +} + +// func (c *Components) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// if c == nil { +// return nil, nil +// } +// return c.resolveNodeByPointer(ptr) +// } + +// func (c *Components) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return c, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "schemas": +// if c.Schemas == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Schemas.resolveNodeByPointer(nxt) +// case "responses": +// if c.Responses == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Responses.resolveNodeByPointer(nxt) +// case "parameters": +// if c.Parameters == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Parameters.resolveNodeByPointer(nxt) +// case "examples": +// if c.Examples == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Examples.resolveNodeByPointer(nxt) +// case "requestBodies": +// if c.RequestBodies == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.RequestBodies.resolveNodeByPointer(nxt) +// case "headers": +// if c.Headers == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Headers.resolveNodeByPointer(nxt) +// case "securitySchemes": +// if c.SecuritySchemes == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.SecuritySchemes.resolveNodeByPointer(nxt) +// case "links": +// if c.Links == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Links.resolveNodeByPointer(nxt) +// case "callbacks": +// if c.Callbacks == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.Callbacks.resolveNodeByPointer(nxt) +// case "pathItems": +// if c.PathItems == nil { +// return nil, newErrNotFound(c.AbsoluteLocation(), tok) +// } +// return c.PathItems.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(c.AbsoluteLocation(), tok) +// } +// } +func (c *Components) nodes() []node { + if c == nil { + return nil + } + edges := appendEdges(nil, c.Schemas) + edges = appendEdges(edges, c.Responses) + edges = appendEdges(edges, c.Parameters) + edges = appendEdges(edges, c.Examples) + edges = appendEdges(edges, c.RequestBodies) + edges = appendEdges(edges, c.Headers) + edges = appendEdges(edges, c.SecuritySchemes) + edges = appendEdges(edges, c.Links) + edges = appendEdges(edges, c.Callbacks) + edges = appendEdges(edges, c.PathItems) + return edges +} + +func (c *Components) isNil() bool { + return c == nil +} + +func (*Components) mapKind() Kind { return KindUndefined } +func (*Components) sliceKind() Kind { return KindUndefined } + +func (c *Components) Anchors() (*Anchors, error) { + if c == nil { + return nil, nil + } + var err error + var anchors *Anchors + if anchors, err = anchors.merge(c.Schemas.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Responses.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Parameters.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Examples.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.RequestBodies.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Headers.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.SecuritySchemes.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Links.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.Callbacks.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(c.PathItems.Anchors()); err != nil { + return nil, err + } + + return anchors, nil +} + +func (c *Components) setLocation(loc Location) error { + if c == nil { + return nil + } + c.Location = loc + var err error + if err = c.Schemas.setLocation(loc.AppendLocation("schemas")); err != nil { + return err + } + if err = c.Responses.setLocation(loc.AppendLocation("responses")); err != nil { + return err + } + if err = c.Parameters.setLocation(loc.AppendLocation("parameters")); err != nil { + return err + } + if err = c.Examples.setLocation(loc.AppendLocation("examples")); err != nil { + return err + } + if err = c.RequestBodies.setLocation(loc.AppendLocation("requestBodies")); err != nil { + return err + } + if err = c.Headers.setLocation(loc.AppendLocation("headers")); err != nil { + return err + } + if err = c.SecuritySchemes.setLocation(loc.AppendLocation("securitySchemes")); err != nil { + return err + } + if err = c.Links.setLocation(loc.AppendLocation("links")); err != nil { + return err + } + if err = c.Callbacks.setLocation(loc.AppendLocation("callbacks")); err != nil { + return err + } + if err = c.PathItems.setLocation(loc.AppendLocation("pathItems")); err != nil { + return err + } + + return nil } -type components Components // MarshalJSON marshals JSON func (c Components) MarshalJSON() ([]byte, error) { + type components Components return marshalExtendedJSON(components(c)) } // UnmarshalJSON unmarshals JSON func (c *Components) UnmarshalJSON(data []byte) error { + type components Components var v components if err := unmarshalExtendedJSON(data, &v); err != nil { return err @@ -46,12 +268,21 @@ func (c *Components) UnmarshalJSON(data []byte) error { return nil } -// MarshalYAML marshals YAML func (c Components) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(c) + j, err := c.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML -func (c *Components) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, c) +// UnmarshalYAML implements yaml.Unmarshaler +func (c *Components) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, c) } + +var _ node = (*Components)(nil) diff --git a/components_test.go b/components_test.go index b1c23e9..43c9699 100644 --- a/components_test.go +++ b/components_test.go @@ -1,146 +1,38 @@ package openapi_test import ( + "bytes" "encoding/json" - "fmt" + "io" "testing" - "github.com/chanced/cmpjson" "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" - yaml "sigs.k8s.io/yaml" ) -func TestComponents(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "schemas": { - "GeneralError": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - } - }, - "Tag": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - } - } - }, - "parameters": { - "skipParam": { - "name": "skip", - "in": "query", - "description": "number of items to skip", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - "limitParam": { - "name": "limit", - "in": "query", - "description": "max records to return", - "required": true, - "schema" : { - "type": "integer", - "format": "int32" - } - } - }, - "responses": { - "NotFound": { - "description": "Entity not found." - }, - "IllegalInput": { - "description": "Illegal input for operation." - }, - "GeneralError": { - "description": "General Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GeneralError" - } - } - } - } - }, - "securitySchemes": { - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "header" - }, - "petstore_auth": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://example.org/api/oauth/dialog", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - } - } - } - }`, +func TestComponentsMarshaling(t *testing.T) { + f, err := testdata.Open("testdata/schemas/petstore-schema-map-test-1.json") + if err != nil { + t.Fatal(err) } + defer f.Close() + fc, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + b := bytes.Buffer{} - for _, d := range j { - data := []byte(d) - var c openapi.Components - err := json.Unmarshal(data, &c) - assert.NoError(err) - b, err := json.MarshalIndent(c, "", " ") - assert.NoError(err) - d, err := jsondiff.CompareJSON(data, b) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), d.String()) - - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yc openapi.Components - err = yaml.Unmarshal(y, &yc) - assert.NoError(err) - yb, err := json.MarshalIndent(yc, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n-----------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) + b.Write([]byte(`{"schemas":`)) + b.Write(fc) + b.Write([]byte(`}`)) + var c openapi.Components + err = c.UnmarshalJSON(b.Bytes()) + if err != nil { + t.Fatal(err) + } + cb, err := json.MarshalIndent(c, "", " ") + if err != nil { + t.Error(err) } + _ = cb } diff --git a/contact.go b/contact.go index db59968..e0fde98 100644 --- a/contact.go +++ b/contact.go @@ -1,40 +1,93 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" + + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) // Contact information for the exposed API. type Contact struct { + Extensions `json:"-"` + Location `json:"-"` // The identifying name of the contact person/organization. - Name string `json:"name,omitempty"` + Name Text `json:"name,omitempty"` // The URL pointing to the contact information. This MUST be in the form of // a URL. - URL string `json:"url,omitempty"` + URL *uri.URI `json:"url,omitempty"` // The email address of the contact person/organization. This MUST be in the // form of an email address. - Emails string `json:"email,omitempty"` - Extensions `json:"-"` + Emails Text `json:"email,omitempty"` } -type contact Contact + +func (*Contact) Anchors() (*Anchors, error) { return nil, nil } + +// Kind returns KindContact +func (*Contact) Kind() Kind { return KindContact } + +func (*Contact) Refs() []Ref { return nil } + +// func (c *Contact) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return c.resolveNodeByPointer(ptr) +// } + +// func (c *Contact) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return c, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(c.AbsoluteLocation(), tok) +// } + +func (*Contact) nodes() []node { return nil } +func (c *Contact) isNil() bool { return c == nil } +func (c *Contact) location() Location { return c.Location } + +func (c *Contact) setLocation(loc Location) error { + if c != nil { + c.Location = loc + } + return nil +} + +func (*Contact) sliceKind() Kind { return KindUndefined } +func (*Contact) mapKind() Kind { return KindUndefined } // MarshalJSON marshals JSON func (c Contact) MarshalJSON() ([]byte, error) { + type contact Contact return marshalExtendedJSON(contact(c)) } // UnmarshalJSON unmarshals JSON func (c *Contact) UnmarshalJSON(data []byte) error { + type contact Contact var v contact err := unmarshalExtendedJSON(data, &v) *c = Contact(v) return err } -// MarshalYAML marshals YAML func (c Contact) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(c) + j, err := c.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML -func (c *Contact) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, c) +// UnmarshalYAML implements yaml.Unmarshaler +func (c *Contact) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, c) } + +var _ node = (*Contact)(nil) diff --git a/contact_test.go b/contact_test.go index 80408ac..0f5ab67 100644 --- a/contact_test.go +++ b/contact_test.go @@ -1,52 +1,52 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// "github.com/wI2L/jsondiff" +// yaml "sigs.k8s.io/yaml" +// ) -func TestContact(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "name": "API Support", - "url": "https://www.example.com/support", - "email": "support@example.com" - }`, - } +// func TestContact(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "name": "API Support", +// "url": "https://www.example.com/support", +// "email": "support@example.com" +// }`, +// } - for _, d := range j { - data := []byte(d) - var c openapi.Contact - err := json.Unmarshal(data, &c) - assert.NoError(err) - b, err := json.MarshalIndent(c, "", " ") - assert.NoError(err) - p, err := jsondiff.CompareJSON(data, b) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), p) +// for _, d := range j { +// data := []byte(d) +// var c openapi.Contact +// err := json.Unmarshal(data, &c) +// assert.NoError(err) +// b, err := json.MarshalIndent(c, "", " ") +// assert.NoError(err) +// p, err := jsondiff.CompareJSON(data, b) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), p) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yc openapi.Contact - err = yaml.Unmarshal(y, &yc) - assert.NoError(err) - yb, err := json.MarshalIndent(yc, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yc openapi.Contact +// err = yaml.Unmarshal(y, &yc) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yc, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } diff --git a/discriminator.go b/discriminator.go index 57ca6c6..0d966c4 100644 --- a/discriminator.go +++ b/discriminator.go @@ -1,7 +1,10 @@ package openapi import ( - "github.com/chanced/openapi/yamlutil" + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) // Discriminator can be used to aid in serialization, deserialization, and @@ -10,27 +13,60 @@ import ( // schema which is used to inform the consumer of the document of an alternative // schema based on the value associated with it. type Discriminator struct { + Extensions `json:"-"` + Location `json:"-"` + // The name of the property in the payload that will hold the discriminator // value. // // *required - PropertyName string `json:"propertyName"` + PropertyName Text `json:"propertyName"` // An object to hold mappings between payload values and schema names or // references. - Mapping map[string]string `json:"mapping,omitempty"` + Mapping *Map[Text] `json:"mapping,omitempty"` +} - Extensions `json:"-"` +func (d *Discriminator) Clone() *Discriminator { + if d == nil { + return nil + } + var m *Map[Text] + if d.Mapping != nil { + m := Map[Text]{ + Items: make([]KeyValue[Text], len(d.Mapping.Items)), + } + copy(m.Items, d.Mapping.Items) + } + return &Discriminator{ + Extensions: d.Extensions, + Location: Location{ + absolute: *d.Location.absolute.Clone(), + relative: d.Location.relative, + }, + PropertyName: d.PropertyName.Clone(), + Mapping: m, + } } -type discriminator Discriminator +func (d *Discriminator) setLocation(loc Location) error { + if d == nil { + return nil + } + d.Location = loc + return nil +} // MarshalJSON marshals d into JSON func (d Discriminator) MarshalJSON() ([]byte, error) { + type discriminator Discriminator + return marshalExtendedJSON(discriminator(d)) } // UnmarshalJSON unmarshals json into d func (d *Discriminator) UnmarshalJSON(data []byte) error { + type discriminator Discriminator + v := discriminator{} if err := unmarshalExtendedJSON(data, &v); err != nil { return err @@ -39,13 +75,54 @@ func (d *Discriminator) UnmarshalJSON(data []byte) error { return nil } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML func (d Discriminator) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(d) + j, err := d.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals yaml into s -func (d *Discriminator) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, d) +// UnmarshalYAML implements yaml.Unmarshaler +func (d *Discriminator) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, d) } + +func (d *Discriminator) Anchors() (*Anchors, error) { return nil, nil } + +func (*Discriminator) Kind() Kind { return KindDiscriminator } + +func (*Discriminator) Refs() []Ref { return nil } + +// func (d *Discriminator) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return d.resolveNodeByPointer(ptr) +// } + +// func (d *Discriminator) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return d, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(d.Location.AbsoluteLocation(), tok) +// } + +func (d *Discriminator) Nodes() []Node { + if d == nil { + return nil + } + return downcastNodes(d.nodes()) +} +func (d *Discriminator) nodes() []node { return nil } + +func (d *Discriminator) isNil() bool { return d == nil } +func (*Discriminator) mapKind() Kind { return KindUndefined } +func (*Discriminator) sliceKind() Kind { return KindUndefined } + +var _ node = (*Discriminator)(nil) diff --git a/discriminator_test.go b/discriminator_test.go index f0cd223..4a0b329 100644 --- a/discriminator_test.go +++ b/discriminator_test.go @@ -1,61 +1,61 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" - yaml "sigs.k8s.io/yaml" -) - -func TestDiscriminator(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "propertyName": "petType", - "mapping": { - "dog": "#/components/schemas/Dog", - "monster": "https://gigantic-server.com/schemas/Monster/schema.json" - }, - "x-ext": "ext val", - "x-ext2": 2 - }`, - } - for _, d := range j { - data := []byte(d) - var dis openapi.Discriminator - err := json.Unmarshal(data, &dis) - assert.NoError(err) - b, err := json.MarshalIndent(dis, "", " ") - assert.NoError(err) - diff, err := jsondiff.CompareJSON(data, b) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), diff.String()) - div := &openapi.Discriminator{} - div.PropertyName = "prop" - - _, err = json.MarshalIndent(div, "", " ") - assert.NoError(err) - - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Discriminator - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - - } - -} +// import ( +// "encoding/json" +// "fmt" +// "testing" + +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// "github.com/wI2L/jsondiff" +// yaml "sigs.k8s.io/yaml" +// ) + +// func TestDiscriminator(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "propertyName": "petType", +// "mapping": { +// "dog": "#/components/schemas/Dog", +// "monster": "https://gigantic-server.com/schemas/Monster/schema.json" +// }, +// "x-ext": "ext val", +// "x-ext2": 2 +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var dis openapi.Discriminator +// err := json.Unmarshal(data, &dis) +// assert.NoError(err) +// b, err := json.MarshalIndent(dis, "", " ") +// assert.NoError(err) +// diff, err := jsondiff.CompareJSON(data, b) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), diff.String()) +// div := &openapi.Discriminator{} +// div.PropertyName = "prop" + +// _, err = json.MarshalIndent(div, "", " ") +// assert.NoError(err) + +// // testing yaml + +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Discriminator +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) + +// } + +// } diff --git a/doc.go b/doc.go index ad7901c..a8f0cfc 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,4 @@ -// Package openapi is a set of Go types for -// [OpenAPI Specification 3.1](https://spec.openapis.org/oas/v3.1.0). The primary purpose of the package -// is to assist in generation of OpenAPI documentation or to offer building blocks for code-generation. +// Package openapi is a library for [OpenAPI] v3.x. // -// Validation of specifications is performed with JSON Schema. +// [OpenAPI] https://www.openapis.org/ package openapi diff --git a/document.go b/document.go new file mode 100644 index 0000000..9b78ef9 --- /dev/null +++ b/document.go @@ -0,0 +1,292 @@ +package openapi + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver" + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + +// Document root object of the Document document. +type Document struct { + Location `json:"-"` + Extensions `json:"-"` + + // OpenAPI - The OpenAPI Version + // + // This string MUST be the version number of the OpenAPI + // Specification that the OpenAPI document uses. The openapi field SHOULD be + // used by tooling to interpret the OpenAPI document. This is not related to + // the API info.version string. + // + // *required* + OpenAPI *semver.Version `json:"openapi"` + + // Provides metadata about the API. The metadata MAY be used by + // tooling as required. + // + // *required* + Info *Info `json:"info"` + + // The default value for the $schema keyword within Schema Objects contained + // within this OAS document. + JSONSchemaDialect *uri.URI `json:"jsonSchemaDialect,omitempty"` + + // A list of tags used by the document with additional metadata. The order + // of the tags can be used to reflect on their order by the parsing tools. + // Not all tags that are used by the Operation Object must be declared. The + // tags that are not declared MAY be organized randomly or based on the + // tools’ logic. Each tag name in the list MUST be unique. + Tags *TagSlice `json:"tags,omitempty"` + + // An array of Server Objects, which provide connectivity information to a + // target server. If the servers property is not provided, or is an empty + // array, the default value would be a Server Object with a url value of /. + Servers *ServerSlice `json:"servers,omitempty" yaml:"servers,omitempty,omtiempty"` + + // The available paths and operations for the API. + Paths *Paths `json:"paths,omitempty"` + + // The incoming webhooks that MAY be received as part of this API and that + // the API consumer MAY choose to implement. Closely related to the + // callbacks feature, this section describes requests initiated other than + // by an API call, for example by an out of band registration. The key name + // is a unique string to refer to each webhook, while the (optionally + // referenced) Path Item Object describes a request that may be initiated by + // the API provider and the expected responses. An example is available. + Webhooks *PathItemMap `json:"webhooks,omitempty"` + + // An element to hold various schemas for the document. + Components *Components `json:"components,omitempty"` + + // A declaration of which security mechanisms can be used across the API. + // + // The list of values includes alternative security requirement objects that + // can be used. + // + // Only one of the security requirement objects need to be + // satisfied to authorize a request. Individual operations can override this + // definition. + // + // To make security optional, an empty security requirement ({}) + // can be included in the array. + // + Security *SecurityRequirementSlice `json:"security,omitempty"` + + // Additional external documentation. + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` +} + +func (*Document) Kind() Kind { return KindDocument } + +func (d *Document) Refs() []Ref { + var refs []Ref + if d.Info != nil { + refs = append(refs, d.Info.Refs()...) + } + if d.Tags != nil { + refs = append(refs, d.Tags.Refs()...) + } + if d.Servers != nil { + refs = append(refs, d.Servers.Refs()...) + } + if d.Paths != nil { + refs = append(refs, d.Paths.Refs()...) + } + if d.Webhooks != nil { + refs = append(refs, d.Webhooks.Refs()...) + } + if d.Components != nil { + refs = append(refs, d.Components.Refs()...) + } + if d.ExternalDocs != nil { + refs = append(refs, d.ExternalDocs.Refs()...) + } + if d.Security != nil { + refs = append(refs, d.Security.Refs()...) + } + return refs +} + +func (d *Document) nodes() []node { + edges := appendEdges(nil, d.Info) + edges = appendEdges(edges, d.Tags) + edges = appendEdges(edges, d.Servers) + edges = appendEdges(edges, d.Paths) + edges = appendEdges(edges, d.Webhooks) + edges = appendEdges(edges, d.Components) + edges = appendEdges(edges, d.Security) + edges = appendEdges(edges, d.ExternalDocs) + return edges +} + +func (d *Document) isNil() bool { + return d == nil +} + +func (*Document) mapKind() Kind { return KindUndefined } + +func (d *Document) setLocation(loc Location) error { + if d == nil { + return fmt.Errorf("cannot set location on nil Document") + } + d.Location = loc + + if err := d.Info.setLocation(loc.AppendLocation("info")); err != nil { + return err + } + if err := d.Tags.setLocation(loc.AppendLocation("tags")); err != nil { + return err + } + if err := d.Servers.setLocation(loc.AppendLocation("servers")); err != nil { + return err + } + if err := d.Paths.setLocation(loc.AppendLocation("paths")); err != nil { + return err + } + if err := d.Webhooks.setLocation(loc.AppendLocation("webhooks")); err != nil { + return err + } + if err := d.Components.setLocation(loc.AppendLocation("components")); err != nil { + return err + } + if err := d.Security.setLocation(loc.AppendLocation("security")); err != nil { + return err + } + if err := d.ExternalDocs.setLocation(loc.AppendLocation("externalDocs")); err != nil { + return err + } + return nil +} + +func (*Document) sliceKind() Kind { return KindUndefined } + +// MarshalJSON marshals JSON +func (d Document) MarshalJSON() ([]byte, error) { + type document Document + return marshalExtendedJSON(document(d)) +} + +// UnmarshalJSON unmarshals JSON +func (d *Document) UnmarshalJSON(data []byte) error { + type openapi Document + v := openapi{} + err := unmarshalExtendedJSON(data, &v) + *d = Document(v) + return err +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (d Document) MarshalYAML() (interface{}, error) { + j, err := d.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (d *Document) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, d) +} + +func (d *Document) Anchors() (*Anchors, error) { + if d == nil { + return nil, nil + } + var anchors *Anchors + var err error + + if anchors, err = anchors.merge(d.Paths.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Components.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Webhooks.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Servers.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Tags.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Security.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.Info.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(d.ExternalDocs.Anchors()); err != nil { + return nil, err + } + return anchors, nil +} + +var _ node = (*Document)(nil) + +// func (d *Document) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return d.resolveNodeByPointer(ptr) +// } + +// func (d *Document) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return d, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "tags": +// if d.Tags == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Tags.resolveNodeByPointer(nxt) +// case "servers": +// if d.Servers == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Servers.resolveNodeByPointer(nxt) +// case "paths": +// if d.Paths == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Paths.resolveNodeByPointer(nxt) +// case "webhooks": +// if d.Webhooks == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Webhooks.resolveNodeByPointer(nxt) +// case "components": +// if d.Components == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Components.resolveNodeByPointer(nxt) +// case "externalDocs": +// if d.ExternalDocs == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.ExternalDocs.resolveNodeByPointer(nxt) +// case "info": +// if d.Info == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Info.resolveNodeByPointer(nxt) +// case "security": +// if d.Security == nil { +// return nil, newErrNotFound(d.AbsoluteLocation(), tok) +// } +// return d.Security.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(d.AbsoluteLocation(), tok) +// } +// } diff --git a/document_test.go b/document_test.go new file mode 100644 index 0000000..ef7096e --- /dev/null +++ b/document_test.go @@ -0,0 +1,450 @@ +package openapi_test + +import ( + "encoding/json" + "io" + "testing" + + "github.com/chanced/openapi" + "github.com/chanced/transcode" + "github.com/google/go-cmp/cmp" +) + +func TestUnmarshal(t *testing.T) { + f, err := testdata.Open("testdata/documents/petstore.yaml") + if err != nil { + t.Fatal(err) + } + ps, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + petstore, err := transcode.JSONFromYAML(ps) + if err != nil { + t.Fatal(err) + } + var expected interface{} + err = json.Unmarshal(petstore, &expected) + if err != nil { + t.Fatal(err) + } + + var v openapi.Document + err = v.UnmarshalJSON(petstore) + if err != nil { + t.Fatal(err) + } + + output, err := json.MarshalIndent(v, "", " ") + if err != nil { + t.Fatal(err) + } + + var actual interface{} + err = json.Unmarshal(output, &actual) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(expected, actual) { + t.Error(cmp.Diff(expected, actual)) + } + // litter.Dump(v) +} + +func TestValidate(t *testing.T) { + // ps, err := fs.ReadFile(testdata, "testdata/petstore.yaml") + // if err != nil { + // t.Fatal(err) + // } + // err = openapi.Validate(ps) + // if err != nil { + // t.Fatal(err) + // } +} + +// import ( +// "encoding/json" +// "fmt" +// "testing" + +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" + +// // "gopkg.in/yaml.v2" +// yaml "sigs.k8s.io/yaml" +// ) + +// func TestOpenAPI(t *testing.T) { +// pass := []string{ +// `{ +// "openapi": "3.1.0", +// "info": { +// "summary": "My API's summary", +// "title": "My API", +// "version": "1.0.0", +// "license": { +// "name": "Apache 2.0", +// "identifier": "Apache-2.0" +// } +// }, +// "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base", +// "paths": { +// "/": { +// "get": { +// "parameters": [] +// } +// }, +// "/{pathTest}": {} +// }, +// "webhooks": { +// "myWebhook": { +// "$ref": "#/components/pathItems/myPathItem", +// "description": "Overriding description" +// } +// }, +// "components": { +// "securitySchemes": { +// "mtls": { +// "type": "mutualTLS" +// } +// }, +// "pathItems": { +// "myPathItem": { +// "post": { +// "requestBody": { +// "required": true, +// "content": { +// "application/json": { +// "schema": { +// "type": "object", +// "properties": { +// "type": { +// "type": "string" +// }, +// "int": { +// "type": "integer", +// "exclusiveMaximum": 100, +// "exclusiveMinimum": 0 +// }, +// "none": { +// "type": "null" +// }, +// "arr": { +// "type": "array", +// "$comment": "Array without items keyword" +// }, +// "either": { +// "type": [ +// "string", +// "null" +// ] +// } +// }, +// "discriminator": { +// "propertyName": "type", +// "x-extension": true +// }, +// "myArbitraryKeyword": true +// } +// } +// } +// } +// } +// } +// } +// } +// } +// `, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "components": { +// "pathItems": {} +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "summary": "My lovely API", +// "version": "1.0.0", +// "license": { +// "name": "Apache", +// "identifier": "Apache-2.0" +// } +// }, +// "components": {} +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "components": {} +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "webhooks": {} +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": {} +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": { +// "/": { +// "get": {} +// } +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": { +// "/{var}": {} +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": {}, +// "components": { +// "schemas": { +// "model": { +// "type": "object", +// "properties": { +// "one": { +// "description": "type array", +// "type": [ +// "integer", +// "string" +// ] +// }, +// "two": { +// "description": "type 'null'", +// "type": "null" +// }, +// "three": { +// "description": "type array including 'null'", +// "type": [ +// "string", +// "null" +// ] +// }, +// "four": { +// "description": "array with no items", +// "type": "array" +// }, +// "five": { +// "description": "singular example", +// "type": "string", +// "examples": [ +// "exampleValue" +// ] +// }, +// "six": { +// "description": "exclusiveMinimum true", +// "exclusiveMinimum": 10 +// }, +// "seven": { +// "description": "exclusiveMinimum false", +// "minimum": 10 +// }, +// "eight": { +// "description": "exclusiveMaximum true", +// "exclusiveMaximum": 20 +// }, +// "nine": { +// "description": "exclusiveMaximum false", +// "maximum": 20 +// }, +// "ten": { +// "description": "nullable string", +// "type": [ +// "string", +// "null" +// ] +// }, +// "eleven": { +// "description": "x-nullable string", +// "type": [ +// "string", +// "null" +// ] +// }, +// "twelve": { +// "description": "file/binary" +// } +// } +// } +// } +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": {}, +// "servers": [ +// { +// "url": "/v1", +// "description": "Run locally." +// }, +// { +// "url": "https://production.com/v1", +// "description": "Run on production server." +// } +// ] +// }`, `{ +// "openapi": "3.1.1", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "components": { +// "schemas": { +// "anything_boolean": true, +// "nothing_boolean": false, +// "anything_object": {}, +// "nothing_object": { +// "not": {} +// } +// } +// } +// }`, +// } + +// assert := require.New(t) + +// for _, d := range pass { +// data := []byte(d) +// // checking json +// var o openapi.OpenAPI +// err := json.Unmarshal(data, &o) +// assert.NoError(err) +// b, err := json.MarshalIndent(o, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, b) { +// fmt.Println(string(data), "\n------------------------\n", string(b)) +// } + +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) + +// // testing validation +// err = o.Validate() +// assert.NoError(err) +// err = openapi.Validate(data) +// assert.NoError(err) +// // testing yaml + +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.OpenAPI +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) + +// } + +// fail := []string{ +// `{ +// "openapi": "3.1.1", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "components": { +// "schemas": { +// "invalid_null": null, +// "invalid_number": 0, +// "invalid_array": [] +// } +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "servers": [ +// { +// "url": "https://example.com/{var}", +// "variables": { +// "var": { +// "enum": [], +// "default": "a" +// } +// } +// } +// ], +// "components": {} +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "paths": {}, +// "servers": { +// "url": "/v1", +// "description": "Run locally." +// } +// }`, +// `{ +// "openapi": "3.1.0", +// "info": { +// "title": "API", +// "version": "1.0.0" +// }, +// "overlays": {} +// }`, +// } +// for _, d := range fail { +// data := []byte(d) + +// err := openapi.Validate(data) +// assert.Error(err) +// } +// } diff --git a/encoding.go b/encoding.go index a3a6589..cfd4269 100644 --- a/encoding.go +++ b/encoding.go @@ -1,11 +1,23 @@ package openapi import ( - "github.com/chanced/openapi/yamlutil" + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) +// EncodingMap is a ComponentMap between a property name and its encoding information. The +// key, being the property name, MUST exist in the schema as a property. The +// encoding object SHALL only apply to requestBody objects when the media type +// is multipart or application/x-www-form-urlencoded. +type EncodingMap = ComponentMap[*Encoding] + // Encoding definition applied to a single schema property. type Encoding struct { + Extensions `json:"-"` + Location `json:"-"` + // The Content-Type for encoding a specific property. Default value depends // on the property type: // @@ -15,12 +27,12 @@ type Encoding struct { // The value can be a specific media type (e.g. application/json), a // wildcard media type (e.g. image/*), or a comma-separated list of the two // types. - ContentType string `json:"contentType,omitempty"` + ContentType Text `json:"contentType,omitempty"` // A map allowing additional information to be provided as headers, for // example Content-Disposition. Content-Type is described separately and // SHALL be ignored in this section. This property SHALL be ignored if the // request body media type is not a multipart. - Headers Headers `json:"headers,omitempty"` + Headers *HeaderMap `json:"headers,omitempty"` // Describes how a specific property value will be serialized depending on // its type. See Parameter Object for details on the style property. The // behavior follows the same values as query parameters, including default @@ -28,7 +40,7 @@ type Encoding struct { // not application/x-www-form-urlencoded or multipart/form-data. If a value // is explicitly defined, then the value of contentType (implicit or // explicit) SHALL be ignored. - Style Style `json:"style,omitempty"` + Style Text `json:"style,omitempty"` // When this is true, property values of type array or object generate // separate parameters for each value of the array, or key-value-pair of the // map. For other types of properties this property has no effect. When @@ -46,18 +58,81 @@ type Encoding struct { // explicitly defined, then the value of contentType (implicit or explicit) // SHALL be ignored. AllowReserved *bool `json:"allowReserved,omitempty"` +} - Extensions `json:"-"` +func (e *Encoding) Nodes() []Node { + if e == nil { + return nil + } + return downcastNodes(e.nodes()) +} + +func (e *Encoding) nodes() []node { + if e == nil { + return nil + } + return appendEdges(nil, e.Headers) +} + +func (e *Encoding) Refs() []Ref { + if e == nil { + return nil + } + return e.Headers.Refs() +} + +func (e *Encoding) Anchors() (*Anchors, error) { + if e == nil { + return nil, nil + } + return e.Headers.Anchors() +} + +// func (e *Encoding) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// err := ptr.Validate() +// if err != nil { +// return nil, err +// } +// return e.resolveNodeByPointer(ptr) +// } + +// func (e *Encoding) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return e, nil +// } +// nxt, tok, _ := ptr.Next() +// switch nxt { +// case "headers": +// if e.Headers == nil { +// return nil, newErrNotFound(e.Location.AbsoluteLocation(), tok) +// } +// return e.Headers.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(e.Location.AbsoluteLocation(), tok) +// } +// } + +func (*Encoding) Kind() Kind { return KindEncoding } +func (*Encoding) mapKind() Kind { return KindEncodingMap } +func (*Encoding) sliceKind() Kind { return KindUndefined } + +func (e *Encoding) setLocation(loc Location) error { + if e == nil { + return nil + } + e.Location = loc + return e.Headers.setLocation(loc.AppendLocation("headers")) } -type encoding Encoding // MarshalJSON marshals e into JSON func (e Encoding) MarshalJSON() ([]byte, error) { + type encoding Encoding return marshalExtendedJSON(encoding(e)) } // UnmarshalJSON unmarshals json into e func (e *Encoding) UnmarshalJSON(data []byte) error { + type encoding Encoding v := encoding{} if err := unmarshalExtendedJSON(data, &v); err != nil { return err @@ -66,18 +141,26 @@ func (e *Encoding) UnmarshalJSON(data []byte) error { return nil } -// MarshalYAML marshals YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (e Encoding) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(e) + j, err := e.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML -func (e *Encoding) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, e) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (e *Encoding) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, e) } -// Encodings is a map between a property name and its encoding information. The -// key, being the property name, MUST exist in the schema as a property. The -// encoding object SHALL only apply to requestBody objects when the media type -// is multipart or application/x-www-form-urlencoded. -type Encodings map[string]*Encoding +func (e *Encoding) isNil() bool { return e == nil } + +func (*Encoding) refable() {} + +var _ node = (*Encoding)(nil) diff --git a/encoding_test.go b/encoding_test.go index 2838969..b5ba9ca 100644 --- a/encoding_test.go +++ b/encoding_test.go @@ -1,64 +1,63 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// "github.com/wI2L/jsondiff" +// yaml "sigs.k8s.io/yaml" +// ) -func TestEncoding(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "historyMetadata": { - "contentType": "application/xml; charset=utf-8" - }, - "profileImage": { - "contentType": "image/png, image/jpeg", - "headers": { - "X-Rate-Limit-Limit": { - "description": "The number of allowed requests in the current period", - "schema": { - "type": "integer" - } - } - } - } - }`, - } - for _, d := range j { - data := []byte(d) - var e openapi.Encodings - err := json.Unmarshal(data, &e) - assert.NoError(err) - b, err := json.MarshalIndent(e, "", " ") - assert.NoError(err) - d, err := jsondiff.CompareJSON(data, b) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), d.String()) +// func TestEncoding(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "historyMetadata": { +// "contentType": "application/xml; charSlice=utf-8" +// }, +// "profileImage": { +// "contentType": "image/png, image/jpeg", +// "headers": { +// "X-Rate-Limit-Limit": { +// "description": "The number of allowed requests in the current period", +// "schema": { +// "type": "integer" +// } +// } +// } +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var e openapi.Encodings +// err := json.Unmarshal(data, &e) +// assert.NoError(err) +// b, err := json.MarshalIndent(e, "", " ") +// assert.NoError(err) +// d, err := jsondiff.CompareJSON(data, b) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), d.String()) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Encodings - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Encodings +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - assert.True(jsonpatch.Equal(data, b)) - assert.NoError(err) - } -} +// assert.True(jsonpatch.Equal(data, b)) +// assert.NoError(err) +// } +// } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..2973a4a --- /dev/null +++ b/errors.go @@ -0,0 +1,201 @@ +package openapi + +import ( + "errors" + "fmt" + "strings" + + "github.com/Masterminds/semver" + "github.com/chanced/jsonpointer" + "github.com/chanced/uri" +) + +var ( + ErrEmptyRef = errors.New("openapi: empty $ref") + ErrNotFound = fmt.Errorf("openapi: component not found") + ErrNotResolvable = errors.New("openapi: pointer path not resolvable") + + ErrMissingOpenAPIVersion = errors.New("openapi: missing openapi version") + + // ErrInvalidSemVer is returned a version is found to be invalid when + // being parsed. + ErrInvalidSemVer = errors.New("invalid semantic version") + + // ErrInvalidMetadata is returned when the metadata of a semver is an invalid format + ErrInvalidSemVerMetadata = errors.New("invalid semantic version metadata string") + + // ErrInvalidPrerelease is returned when the pre-release of a semver is an invalid format + ErrInvalidSemVerPrerelease = errors.New("invalid semantic version prerelease string") + + ErrInvalidResolution = errors.New("openapi: invalid resolution") +) + +type Error struct { + Err error + ResourceURI uri.URI +} + +func NewError(err error, resource uri.URI) error { + return &Error{ + Err: err, + ResourceURI: resource, + } +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s: [%q]", e.Err, e.ResourceURI.String()) +} + +func (e *Error) Unwrap() error { + return e.Err +} + +func newErrNotFound(uri uri.URI, tok jsonpointer.Token) error { + return NewError(fmt.Errorf("%w: %q", ErrNotFound, tok), uri) +} + +func newErrNotResolvable(uri uri.URI, tok jsonpointer.Token) error { + return NewError(fmt.Errorf("%w: %q", ErrNotResolvable, tok), uri) +} + +type UnsupportedVersionError struct { + Version string `json:"version"` + Errs []error `json:"errors"` +} + +func (e *UnsupportedVersionError) Error() string { + b := strings.Builder{} + b.WriteString("openapi: unsupported version:") + b.WriteString(fmt.Sprintf(" %q:", e.Version)) + for _, err := range e.Errs { + b.WriteString(fmt.Sprintf("\n- %s", err)) + } + return b.String() +} + +func (e *UnsupportedVersionError) As(target interface{}) bool { + for _, v := range e.Errs { + if errors.As(v, target) { + return true + } + } + return false +} + +func (e *UnsupportedVersionError) Is(err error) bool { + for _, v := range e.Errs { + if errors.Is(v, err) { + return true + } + } + return false +} + +type ValidationError struct { + Kind Kind + Err error + URI uri.URI +} + +func NewValidationError(err error, kind Kind, resource uri.URI) error { + var ve *ValidationError + if errors.As(err, &ve) { + return ve + } + + return NewError(&ValidationError{ + Kind: kind, + Err: err, + URI: resource, + }, resource) +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("openapi: error validating %s %s: %s", e.Kind, e.URI, e.Err) +} + +func (e *ValidationError) Unwrap() error { + return e.Err +} + +type SemVerError struct { + Value string + Err error + URI uri.URI +} + +func (e SemVerError) Error() string { + return fmt.Sprintf("openapi: error parsing semver %q for %s: %v", e.Value, e.URI, e.Err) +} + +func (e SemVerError) Unwrap() error { + return e.Err +} + +func (e SemVerError) Is(err error) bool { + if errors.Is(e.Err, err) { + return true + } + switch { + case errors.Is(err, ErrInvalidSemVer): + return errors.Is(err, semver.ErrInvalidSemVer) + case errors.Is(e.Err, ErrInvalidSemVerMetadata): + return errors.Is(err, semver.ErrInvalidMetadata) + case errors.Is(e.Err, ErrInvalidSemVerPrerelease): + return errors.Is(err, semver.ErrInvalidPrerelease) + } + return false +} + +func NewSemVerError(err error, value string, uri uri.URI) error { + if err == nil { + return nil + } + var sv *SemVerError + if errors.As(err, &sv) { + return sv + } + return NewError(&SemVerError{ + Value: value, + Err: translateSemVerErr(err), + URI: uri, + }, uri) +} + +func translateSemVerErr(err error) error { + switch { + case err == nil: + return nil + case errors.Is(err, semver.ErrInvalidSemVer): + return ErrInvalidSemVer + case errors.Is(err, semver.ErrInvalidMetadata): + return ErrInvalidSemVerMetadata + case errors.Is(err, semver.ErrInvalidPrerelease): + return ErrInvalidSemVerPrerelease + } + return err +} + +type ResolutionError struct { + URI uri.URI + Expected Kind + Actual Kind + RefType RefType +} + +func (e *ResolutionError) Error() string { + return fmt.Sprintf("%v cannot resolve %s to %s for %s: %s", ErrInvalidResolution, e.Actual, e.Expected, e.RefType, e.URI) +} + +func (e *ResolutionError) Unwrap() error { + return ErrInvalidResolution +} + +func NewResolutionError(r Ref, expected, actual Kind) error { + return &ResolutionError{ + URI: r.AbsoluteLocation(), + Actual: actual, + Expected: expected, + RefType: r.RefType(), + } +} diff --git a/example.go b/example.go index 1823603..f2391ca 100644 --- a/example.go +++ b/example.go @@ -3,110 +3,125 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" ) -// ExampleKind indicates wheter the ExampleObj is an Example or a Reference -type ExampleKind uint8 +// ExampleMap is an object to hold reusable ExampleMap. +type ExampleMap = ComponentMap[*Example] -const ( - // ExampleKindObj indicates an ExampleObj - ExampleKindObj ExampleKind = iota - // ExampleKindRef indicates a Reference - ExampleKindRef -) - -// Example is either an Example or a Reference -type Example interface { - ResolveExample(ExampleResolver) (*ExampleObj, error) - ExampleKind() ExampleKind -} - -// ExampleObj is an example for various api interactions such as Responses +// Example is an example for various api interactions such as Responses // // In all cases, the example value is expected to be compatible with the type // schema of its associated value. Tooling implementations MAY choose to // validate compatibility automatically, and reject the example value(s) if // incompatible. -type ExampleObj struct { +type Example struct { + Extensions `json:"-"` + Location `json:"-"` + // Short description for the example. - Summary string `json:"summary,omitempty"` + Summary Text `json:"summary,omitempty"` + // Long description for the example. CommonMark syntax MAY be used for rich // text representation. - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` + // Any embedded literal example. The value field and externalValue field are // mutually exclusive. To represent examples of media types that cannot // naturally represented in JSON or YAML, use a string value to contain the // example, escaping where necessary. - Value json.RawMessage `json:"value,omitempty"` + Value jsonx.RawMessage `json:"value,omitempty"` + // A URI that points to the literal example. This provides the capability to // reference examples that cannot easily be included in JSON or YAML // documents. The value field and externalValue field are mutually // exclusive. See the rules for resolving Relative References. - ExternalValue string `json:"externalValue,omitempty"` - Extensions `json:"-"` + ExternalValue *uri.URI `json:"externalValue,omitempty"` } -type example ExampleObj + +func (e *Example) Nodes() []Node { + if e == nil { + return nil + } + return downcastNodes(e.nodes()) +} +func (e *Example) nodes() []node { return nil } + +func (*Example) Refs() []Ref { return nil } + +func (e *Example) Anchors() (*Anchors, error) { return nil, nil } + +func (*Example) Kind() Kind { return KindExample } +func (*Example) mapKind() Kind { return KindExampleMap } +func (*Example) sliceKind() Kind { return KindUndefined } // MarshalJSON marshals JSON -func (e ExampleObj) MarshalJSON() ([]byte, error) { +func (e Example) MarshalJSON() ([]byte, error) { + type example Example + return marshalExtendedJSON(example(e)) } // UnmarshalJSON unmarshals JSON -func (e *ExampleObj) UnmarshalJSON(data []byte) error { +func (e *Example) UnmarshalJSON(data []byte) error { + type example Example var v example if err := unmarshalExtendedJSON(data, &v); err != nil { return err } - *e = ExampleObj(v) + *e = Example(v) return nil } -// MarshalYAML marshals YAML -func (e ExampleObj) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(e) -} - -// UnmarshalYAML unmarshals YAML -func (e *ExampleObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, e) -} - -// ExampleKind returns ExampleKindObj -func (e *ExampleObj) ExampleKind() ExampleKind { return ExampleKindObj } - -// ResolveExample resolves ExampleObj by returning itself. resolve is not called. -func (e *ExampleObj) ResolveExample(ExampleResolver) (*ExampleObj, error) { - return e, nil +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (e Example) MarshalYAML() (interface{}, error) { + j, err := e.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// Examples is an object to hold reusable Examples. -type Examples map[string]Example - -// UnmarshalJSON unmarshals JSON -func (e *Examples) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (e *Example) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { return err } - res := make(Examples, len(dm)) - for k, d := range dm { - if isRefJSON(d) { - v, err := unmarshalReferenceJSON(d) - if err != nil { - return err - } - res[k] = v - continue - } - var v example - if err := unmarshalExtendedJSON(d, &v); err != nil { - return err - } - ev := ExampleObj(v) - res[k] = &ev + return json.Unmarshal(j, e) +} + +func (e *Example) setLocation(loc Location) error { + if e == nil { + return nil } - *e = res + e.Location = loc return nil } +func (e *Example) isNil() bool { return e == nil } + +func (*Example) refable() {} + +var ( + _ node = (*Example)(nil) + + _ node = (*ExampleMap)(nil) +) + +// func (e *Example) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return e.resolveNodeByPointer(ptr) +// } + +// func (e *Example) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return e, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(e.Location.AbsoluteLocation(), tok) +// } diff --git a/example_test.go b/example_test.go index e198596..c52643d 100644 --- a/example_test.go +++ b/example_test.go @@ -1,127 +1,128 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// yaml "sigs.k8s.io/yaml" +// ) -func TestIssue5(t *testing.T) { - assert := require.New(t) - data := `{ - "openapi": "3.1.0", - "info": { - "title": "", - "version": "", - "description": "Test file for loading pre-existing OAS" - }, - "paths": { - "/catalogue/{id}": { - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "style": "simple", - "schema": { - "type": "string" - }, - "examples": { - "an example": { - "value": "someval" - } - } - } - ] - }, - "/catalogue/{id}/details": { - "parameters": [ - { - "name": "id", - "in": "path", - "style": "simple", - "required": true, - "schema": { - "type": "string" - }, - "example": "some-uuid-maybe" - } - ] - } - } - }` +// func TestIssue5(t *testing.T) { +// assert := require.New(t) +// data := `{ +// "openapi": "3.1.0", +// "info": { +// "title": "", +// "version": "", +// "description": "Test file for loading pre-existing OAS" +// }, +// "paths": { +// "/catalogue/{id}": { +// "parameters": [ +// { +// "name": "id", +// "in": "path", +// "required": true, +// "style": "simple", +// "schema": { +// "type": "string" +// }, +// "examples": { +// "an example": { +// "value": "someval" +// } +// } +// } +// ] +// }, +// "/catalogue/{id}/details": { +// "parameters": [ +// { +// "name": "id", +// "in": "path", +// "style": "simple", +// "required": true, +// "schema": { +// "type": "string" +// }, +// "example": "some-uuid-maybe" +// } +// ] +// } +// } +// }` - var oas openapi.OpenAPI - err := json.Unmarshal([]byte(data), &oas) - assert.NoError(err) - pi := oas.Paths.Items["/catalogue/{id}"] - assert.NotNil(pi) - assert.NotNil(pi.Parameters) - assert.Len(*pi.Parameters, 1) - params := *pi.Parameters - param := params[0] - paramobj := param.(*openapi.ParameterObj) - assert.Contains(paramobj.Examples, "an example") - ex := paramobj.Examples["an example"].(*openapi.ExampleObj) - assert.Equal(json.RawMessage(`"someval"`), ex.Value) -} +// var oas openapi.OpenAPI +// err := json.Unmarshal([]byte(data), &oas) +// assert.NoError(err) +// pi := oas.Paths.Items["/catalogue/{id}"] +// assert.NotNil(pi) +// assert.NotNil(pi.Parameters) +// assert.Len(*pi.Parameters, 1) +// params := *pi.Parameters +// param := params[0] +// paramobj := param.Object +// assert.Contains(paramobj.Examples, "an example") +// ex, ok := paramobj.Examples.Get("an example") +// assert.True(ok) +// assert.Equal(json.RawMessage(`"someval"`), ex.Object.Value) +// } -func TestExample(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "foo": { - "summary": "A foo example", - "value": { - "foo": "bar" - } - }, - "bar": { - "summary": "A bar example", - "value": { - "bar": "baz" - } - } - }`, - `{ - "zip-example": { - "$ref": "#/components/examples/zip-example" - } - }`, - `{ - "confirmation-success": { - "$ref": "#/components/examples/confirmation-success" - } - }`, - } - for _, d := range j { - data := []byte(d) - var e openapi.Examples - err := json.Unmarshal(data, &e) - assert.NoError(err) - b, err := json.Marshal(e) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b)) +// func TestExample(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "foo": { +// "summary": "A foo example", +// "value": { +// "foo": "bar" +// } +// }, +// "bar": { +// "summary": "A bar example", +// "value": { +// "bar": "baz" +// } +// } +// }`, +// `{ +// "zip-example": { +// "$ref": "#/components/examples/zip-example" +// } +// }`, +// `{ +// "confirmation-success": { +// "$ref": "#/components/examples/confirmation-success" +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var e openapi.ExampleMap +// err := json.Unmarshal(data, &e) +// assert.NoError(err) +// b, err := json.Marshal(e) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b)) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Examples - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.ExampleMap +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } diff --git a/extension.go b/extension.go index 28e170b..d77d57d 100644 --- a/extension.go +++ b/extension.go @@ -1,11 +1,13 @@ package openapi import ( + "bytes" "encoding/json" - "sort" - "strings" + "fmt" - "github.com/tidwall/sjson" + "github.com/chanced/jsonx" + "github.com/chanced/maps" + "github.com/tidwall/gjson" ) // Extensions for OpenAPI @@ -44,7 +46,7 @@ import ( // This is different from hiding the path itself from the Paths Object, because // the user will be aware of its existence. This allows the documentation // provider to finely control what the viewer can see. -type Extensions map[string]json.RawMessage +type Extensions map[Text]jsonx.RawMessage type extended interface { exts() Extensions @@ -55,7 +57,7 @@ type extender interface { } // Decode decodes all extensions into dst. -func (e Extensions) Decode(dst interface{}) error { +func (e Extensions) DecodeExtensions(dst interface{}) error { b, err := json.Marshal(e) if err != nil { return err @@ -64,8 +66,8 @@ func (e Extensions) Decode(dst interface{}) error { } // DecodeExtension decodes extension at key into dst. -func (e Extensions) DecodeExtension(key string, dst interface{}) error { - if !strings.HasPrefix(key, "x-") { +func (e Extensions) DecodeExtension(key Text, dst interface{}) error { + if !key.HasPrefix("x-") { key = "x-" + key } return json.Unmarshal(e[key], dst) @@ -76,35 +78,35 @@ func (e Extensions) exts() Extensions { return e } func (e *Extensions) setExts(v Extensions) { *e = v } // SetExtension encodes val and sets the result to key -func (e *Extensions) SetExtension(key string, val interface{}) error { +func (e *Extensions) SetExtension(key Text, val interface{}) error { data, err := json.Marshal(val) if err != nil { return err } - e.SetEncodedExtension(key, data) + e.SetRawExtension(key, data) return nil } -// SetEncodedExtension sets val to key -func (e *Extensions) SetEncodedExtension(key string, val []byte) { - if !strings.HasPrefix(key, "x-") { +// SetRawExtension sets the raw JSON encoded val to key +func (e *Extensions) SetRawExtension(key Text, val []byte) { + if !key.HasPrefix("x-") { key = "x-" + key } (*e)[key] = val } // Extension returns an extension by name -func (e Extensions) Extension(name string) (interface{}, bool) { - if !strings.HasPrefix(name, "x-") { - name = "x-" + name +func (e Extensions) Extension(key Text) (interface{}, bool) { + if !key.HasPrefix("x-") { + key = "x-" + key } - v, exists := e[name] + v, exists := e[key] return v, exists } // IsExtensionKey returns true if the key starts with "x-" -func IsExtensionKey(key string) bool { - return strings.HasPrefix(key, "x-") +func IsExtensionKey(key Text) bool { + return key.HasPrefix("x-") } func unmarshalExtendedJSON(data []byte, dst extender) error { @@ -112,15 +114,12 @@ func unmarshalExtendedJSON(data []byte, dst extender) error { if err := json.Unmarshal(data, dst); err != nil { return err } - var jm map[string]json.RawMessage - if err := json.Unmarshal(data, &jm); err != nil { - return err - } - for key, d := range jm { - if strings.HasPrefix(key, "x-") { - ev[key] = d + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + if IsExtensionKey(Text(key.String())) { + ev[Text(key.String())] = jsonx.RawMessage(value.Raw) } - } + return true + }) dst.setExts(ev) return nil } @@ -130,24 +129,29 @@ func marshalExtendedJSON(dst extended) ([]byte, error) { if err != nil { return nil, err } - return marshalExtendedJSONInto(data, dst) + if !jsonx.IsObject(data) { + // this shouldn't happen + return nil, fmt.Errorf("openapi: cannot marshal extensions into non-object") + } + + b := bytes.Buffer{} + b.Write(data[:len(data)-1]) + return marshalExtensionsInto(&b, dst.exts()) } -func marshalExtendedJSONInto(data []byte, obj extended) ([]byte, error) { +func marshalExtensionsInto(b *bytes.Buffer, e Extensions) ([]byte, error) { var err error - - exts := obj.exts() - keys := make([]string, 0, len(exts)) - for k := range exts { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - data, err = sjson.SetBytes(data, k, exts[k]) + for _, kv := range maps.SortByKeys(e) { + if b.Len() > 1 { + b.WriteByte(',') + } + jsonx.EncodeAndWriteString(b, kv.Key) + b.WriteByte(':') + b.Write(kv.Value) if err != nil { - return data, err + return nil, err } } - return data, nil + b.WriteByte('}') + return b.Bytes(), nil } diff --git a/external_docs.go b/external_docs.go index d3e235b..1f6f994 100644 --- a/external_docs.go +++ b/external_docs.go @@ -1,39 +1,117 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" + + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) // ExternalDocs allows referencing an external resource for extended // documentation. type ExternalDocs struct { + Location `json:"-"` + Extensions `json:"-"` + // The URL for the target documentation. This MUST be in the form of a URL. // // *required* - URL string `json:"url"` - // A description of the target documentation. CommonMark syntax MAY be used for rich text representation. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Extensions `json:"-"` + URL *uri.URI `json:"url"` + + // A description of the target documentation. CommonMark syntax MAY be used + // for rich text representation. + Description Text `json:"description,omitempty"` } -type externaldocs ExternalDocs + +func (ed *ExternalDocs) Nodes() []Node { + if ed == nil { + return nil + } + return downcastNodes(ed.nodes()) +} +func (ed *ExternalDocs) nodes() []node { return nil } + +func (*ExternalDocs) Refs() []Ref { return nil } +func (*ExternalDocs) Kind() Kind { return KindExternalDocs } +func (*ExternalDocs) mapKind() Kind { return KindUndefined } +func (*ExternalDocs) sliceKind() Kind { return KindUndefined } + +func (*ExternalDocs) Anchors() (*Anchors, error) { return nil, nil } + +// // ResolveNodeByPointer resolves a Node by a jsonpointer. It validates the pointer and then +// // attempts to resolve the Node. +// // +// // # Errors +// // +// // - [ErrNotFound] indicates that the component was not found +// // +// // - [ErrNotResolvable] indicates that the pointer path can not resolve to a +// // Node +// // +// // - [jsonpointer.ErrMalformedEncoding] indicates that the pointer encoding +// // is malformed +// // +// // - [jsonpointer.ErrMalformedStart] indicates that the pointer is not empty +// // and does not start with a slash +// func (ed *ExternalDocs) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// err := ptr.Validate() +// if err != nil { +// return nil, err +// } +// return ed.resolveNodeByPointer(ptr) +// } + +// func (ed *ExternalDocs) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// tok, _ := ptr.NextToken() +// if !ptr.IsRoot() { +// return nil, newErrNotResolvable(ed.Location.AbsoluteLocation(), tok) +// } +// return ed, nil +// } // MarshalJSON marshals JSON func (ed ExternalDocs) MarshalJSON() ([]byte, error) { + type externaldocs ExternalDocs + return marshalExtendedJSON(externaldocs(ed)) } // UnmarshalJSON unmarshals JSON func (ed *ExternalDocs) UnmarshalJSON(data []byte) error { + type externaldocs ExternalDocs + var v externaldocs err := unmarshalExtendedJSON(data, &v) *ed = ExternalDocs(v) return err } -// MarshalYAML marshals YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (ed ExternalDocs) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(ed) + j, err := ed.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML -func (ed *ExternalDocs) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, ed) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (ed *ExternalDocs) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, ed) } + +func (ed *ExternalDocs) setLocation(loc Location) error { + if ed == nil { + return nil + } + ed.Location = loc + return nil +} +func (ed *ExternalDocs) isNil() bool { return ed == nil } + +var _ node = (*ExternalDocs)(nil) diff --git a/go.mod b/go.mod index f978898..9fdd369 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,23 @@ module github.com/chanced/openapi -go 1.16 +go 1.18 require ( - github.com/chanced/cmpjson v0.0.0-20210415035445-da9262c1f20a - github.com/chanced/dynamic v0.0.0-20210502140838-c010b5fc3e44 - github.com/evanphx/json-patch/v5 v5.6.0 - github.com/google/go-cmp v0.5.6 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 - github.com/stretchr/testify v1.7.0 - github.com/tidwall/gjson v1.12.0 - github.com/tidwall/sjson v1.2.3 - github.com/wI2L/jsondiff v0.1.1 - gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - sigs.k8s.io/yaml v1.3.0 + github.com/Masterminds/semver v1.5.0 + github.com/chanced/caps v0.7.11 + github.com/chanced/jsonpointer v0.0.5 + github.com/chanced/jsonx v0.0.7 + github.com/chanced/maps v0.0.3 + github.com/chanced/transcode v0.2.1 + github.com/chanced/uri v0.2.1 + github.com/google/go-cmp v0.5.9 + github.com/sanity-io/litter v1.5.1 + github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 + github.com/tidwall/gjson v1.14.3 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 803267a..b42b37c 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,36 @@ -github.com/chanced/cmpjson v0.0.0-20210415035445-da9262c1f20a h1:zG6t+4krPXcCKtLbjFvAh+fKN1d0qfD+RaCj+680OU8= -github.com/chanced/cmpjson v0.0.0-20210415035445-da9262c1f20a/go.mod h1:yhcmlFk1hxuZ+5XZbupzT/cEm/eE4ZvWbmsW1+Q/aZE= -github.com/chanced/dynamic v0.0.0-20210502140838-c010b5fc3e44 h1:4NOJMtvZaOA6cI2gkIuXk/2b5KTOvm/R4zyPy/yLCM4= -github.com/chanced/dynamic v0.0.0-20210502140838-c010b5fc3e44/go.mod h1:XVNfXN5kgZST4PQ0W/oBAHJku2OteCeHxjAbvfd0ARM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/evanphx/json-patch/v5 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -github.com/evanphx/json-patch/v5 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/chanced/caps v0.7.11 h1:I9RckhEe2T3+VdYVy451ufgr/tFjX8E3Kdd13IngvMQ= +github.com/chanced/caps v0.7.11/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs= +github.com/chanced/jsonpointer v0.0.5 h1:N9SewxwFmdJ6QejrAhvPKjW42Olv383JeWk7PrDSd5o= +github.com/chanced/jsonpointer v0.0.5/go.mod h1:dw54fmixEiDkp4PqNO5579oBEkwCgjrDHS3cmR70SW0= +github.com/chanced/jsonx v0.0.7 h1:v9Ir6Yra7qTWBxFAMogj4p+Hvh+NDTBqZcvsuvfeC5I= +github.com/chanced/jsonx v0.0.7/go.mod h1:5jZ6w4wNTtpUGlyxVQI/YFdITXR85ya3WL0zF3P21eI= +github.com/chanced/maps v0.0.3 h1:FtiEgfg0KpVP2Jfa5muCkuhGNE+CyBEA9+QX6agd0hM= +github.com/chanced/maps v0.0.3/go.mod h1:+gAhsn8IHMw6nS10PwKiqowwrM3aKLkiLKF4IYzdKQU= +github.com/chanced/transcode v0.2.1 h1:fEUDOvBd51in7CPznJHx1jamudfyezrZiHU9fo4Bfjg= +github.com/chanced/transcode v0.2.1/go.mod h1:PAyT7yNnhwPa3ifKANMi8tcxDIZFHgNTBt8fBeP3KWU= +github.com/chanced/uri v0.2.1 h1:FuCRw9/qodnWwQzh9IwXFNC4W5W5gDfaIcJdjiib1nU= +github.com/chanced/uri v0.2.1/go.mod h1:rQ71Mb+hLjOz5r1f8IcvyBJTbfnBE0pfRoP0flwxPPU= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= -github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/sanity-io/litter v1.5.1 h1:dwnrSypP6q56o3lFxTU+t2fwQ9A+U5qrXVO4Qg9KwVU= +github.com/sanity-io/litter v1.5.1/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 h1:HNLA3HtUIROrQwG1cuu5EYuqk3UEoJ61Dr/9xkd6sok= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.12.0 h1:61wEp/qfvFnqKH/WCI3M8HuRut+mHT6Mr82QrFmM2SY= -github.com/tidwall/gjson v1.12.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8= -github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs= -github.com/wI2L/jsondiff v0.1.1 h1:r2TkoEet7E4JMO5+s1RCY2R0LrNPNHY6hbDeow2hRHw= -github.com/wI2L/jsondiff v0.1.1/go.mod h1:bAbJSAJXZtfOCZ5y3v7Mfb6UQa3DGdGFjQj1cNv8EcM= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/header.go b/header.go index a8a03cc..406b14d 100644 --- a/header.go +++ b/header.go @@ -3,42 +3,38 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// HeaderKind distinguishes between Header and Reference -type HeaderKind uint8 +// HeaderMap holds reusable HeaderMap. +type HeaderMap = ComponentMap[*Header] -const ( - // HeaderKindObj = Header - HeaderKindObj HeaderKind = iota - // HeaderKindRef = Reference - HeaderKindRef -) - -// Header is either a Header or a Reference -type Header interface { - ResolveHeader(HeaderResolver) (*HeaderObj, error) - HeaderKind() HeaderKind -} - -// HeaderObj follows the structure of the Parameter Object with the following +// Header follows the structure of the Parameter Object with the following // changes: -// - name MUST NOT be specified, it is given in the corresponding headers map. -// - in MUST NOT be specified, it is implicitly in header. -// - All traits that are affected by the location MUST be applicable to a -// location of header (for example, style). -type HeaderObj struct { +// - name MUST NOT be specified, it is given in the corresponding headers map. +// - in MUST NOT be specified, it is implicitly in header. +// - All traits that are affected by the location MUST be applicable to a +// location of header (for example, style). +type Header struct { + // OpenAPI extensions + Extensions `json:"-"` + Location `json:"-"` + // A brief description of the parameter. This could contain examples of use. // CommonMark syntax MAY be used for rich text representation. - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` + // Determines whether this parameter is mandatory. If the parameter location // is "path", this property is REQUIRED and its value MUST be true. // Otherwise, the property MAY be included and its default value is false. Required *bool `json:"required,omitempty"` + // Specifies that a parameter is deprecated and SHOULD be transitioned out // of usage. Default value is false. Deprecated *bool `json:"deprecated,omitempty"` + // Sets the ability to pass empty-valued parameters. This is valid only for // query parameters and allows sending a parameter with an empty value. // Default value is false. If style is used, and if behavior is n/a (cannot @@ -46,6 +42,7 @@ type HeaderObj struct { // this property is NOT RECOMMENDED, as it is likely to be removed in a // later revision. AllowEmptyValue *bool `json:"allowEmptyValue,omitempty"` + // Describes how the parameter value will be serialized depending on the // type of the parameter value. // Default values (based on value of in): @@ -53,26 +50,31 @@ type HeaderObj struct { // - for path - simple; // - for header - simple; // - for cookie - form. - Style string `json:"style,omitempty"` + Style Text `json:"style,omitempty"` + // When this is true, parameter values of type array or object generate // separate parameters for each value of the array or key-value pair of the // map. For other types of parameters this property has no effect. When // style is form, the default value is true. For all other styles, the // default value is false. Explode *bool `json:"explode,omitempty"` + // Determines whether the parameter value SHOULD allow reserved characters, // as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without // percent-encoding. This property only applies to parameters with an in // value of query. The default value is false. AllowReserved *bool `json:"allowReserved,omitempty"` + // The schema defining the type used for the parameter. - Schema *SchemaObj `json:"schema,omitempty"` + Schema *Schema `json:"schema,omitempty"` + // Examples of the parameter's potential value. Each example SHOULD // contain a value in the correct format as specified in the parameter // encoding. The examples field is mutually exclusive of the example // field. Furthermore, if referencing a schema that contains an example, // the examples value SHALL override the example provided by the schema. - Examples Examples `json:"examples,omitempty"` + Examples *ExampleMap `json:"examples,omitempty"` + // Example of the parameter's potential value. The example SHOULD match the // specified schema and encoding properties if present. The example field is // mutually exclusive of the examples field. Furthermore, if referencing a @@ -80,92 +82,145 @@ type HeaderObj struct { // example provided by the schema. To represent examples of media types that // cannot naturally be represented in JSON or YAML, a string value can // contain the example with escaping where necessary. - Example json.RawMessage `json:"example,omitempty"` - // OpenAPI extensions - Extensions `json:"-"` + Example jsonx.RawMessage `json:"example,omitempty"` } -type header HeaderObj +func (h *Header) Nodes() []Node { + if h == nil { + return nil + } + return downcastNodes(h.nodes()) +} + +func (h *Header) nodes() []node { + return appendEdges(nil, h.Schema, h.Examples) +} -// HeaderKind distinguishes h as a Header by returning HeaderKindHeader -func (h *HeaderObj) HeaderKind() HeaderKind { return HeaderKindObj } +func (h *Header) Refs() []Ref { + if h == nil { + return nil + } + var refs []Ref + refs = append(refs, h.Schema.Refs()...) + refs = append(refs, h.Examples.Refs()...) + return refs +} -// ResolveHeader resolves HeaderObj by returning itself. resolve is not called. -func (h *HeaderObj) ResolveHeader(HeaderResolver) (*HeaderObj, error) { - return h, nil +func (h *Header) Anchors() (*Anchors, error) { + if h == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = h.Schema.Anchors(); err != nil { + return nil, err + } + if anchors, err = anchors.merge(h.Examples.Anchors()); err != nil { + return nil, err + } + return anchors, nil } +func (*Header) Kind() Kind { return KindHeader } +func (*Header) mapKind() Kind { return KindHeaderMap } +func (*Header) sliceKind() Kind { return KindHeaderSlice } + +func (h Header) MarshalJSON() ([]byte, error) { + type header Header -// MarshalJSON marshals h into JSON -func (h HeaderObj) MarshalJSON() ([]byte, error) { return marshalExtendedJSON(header(h)) } // UnmarshalJSON unmarshals json into h -func (h *HeaderObj) UnmarshalJSON(data []byte) error { +func (h *Header) UnmarshalJSON(data []byte) error { + type header Header + v := header{} err := unmarshalExtendedJSON(data, &v) - *h = HeaderObj(v) + *h = Header(v) return err } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML -func (h HeaderObj) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(h) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (h Header) MarshalYAML() (interface{}, error) { + j, err := h.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals yaml into s -func (h *HeaderObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, h) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (h *Header) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, h) } -// Headers holds reusable HeaderObjs. -type Headers map[string]Header +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (s Schema) MarshalYAML() (interface{}, error) { + j, err := s.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} -// UnmarshalJSON unmarshals JSON data into p -func (h *Headers) UnmarshalJSON(data []byte) error { - var m map[string]json.RawMessage - err := json.Unmarshal(data, &m) - *h = make(Headers, len(m)) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (s *Schema) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) if err != nil { return err } - for i, j := range m { - if isRefJSON(data) { - var v Reference - if err = json.Unmarshal(j, &v); err != nil { - return err - } - (*h)[i] = &v - } else { - var v HeaderObj - if err = json.Unmarshal(j, &v); err != nil { - return err - } - (*h)[i] = &v - } - } - return nil + return json.Unmarshal(j, s) } -// UnmarshalYAML unmarshals YAML data into p -func (h *Headers) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, h) -} +func (h *Header) setLocation(loc Location) error { + if h == nil { + return nil + } + h.Location = loc + if err := h.Examples.setLocation(loc.AppendLocation("examples")); err != nil { + return err + } -// MarshalYAML marshals p into YAML -func (h Headers) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(h) - if err != nil { - return nil, err + if err := h.Schema.setLocation(loc.AppendLocation("schema")); err != nil { + return err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return nil } +func (h *Header) isNil() bool { return h == nil } +func (*Header) refable() {} + +var _ node = (*Header)(nil) + +// +// +// func (h *Header) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return h.resolveNodeByPointer(ptr) +// } + +// func (h *Header) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return h, nil +// } +// nxt, tok, _ := ptr.Next() +// switch nxt { +// case "schema": +// if h.Schema == nil { +// return nil, newErrNotFound(h.Location.AbsoluteLocation(), tok) +// } +// return h.Schema.resolveNodeByPointer(nxt) +// case "examples": +// if h.Examples == nil { +// return nil, newErrNotFound(h.Location.AbsoluteLocation(), tok) +// } +// return h.Examples.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(h.Location.AbsoluteLocation(), tok) +// } +// } diff --git a/header_test.go b/header_test.go index 0da5a99..215d2b3 100644 --- a/header_test.go +++ b/header_test.go @@ -1,54 +1,54 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) - -func TestHeader(t *testing.T) { - assert := require.New(t) - - j := []string{ - `{ - "description": "The number of allowed requests in the current period", - "schema": { - "type": "integer" - }, - "x-header-ext": "value" - }`, - } - for _, d := range j { - data := []byte(d) - var h openapi.HeaderObj - err := json.Unmarshal(data, &h) - assert.NoError(err) - - b, err := json.MarshalIndent(h, "", " ") - assert.NoError(err) - p, err := jsonpatch.CreateMergePatch(data, b) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), string(p)) - - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.HeaderObj - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - - } -} +// import ( +// "encoding/json" +// "fmt" +// "testing" + +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// yaml "sigs.k8s.io/yaml" +// ) + +// func TestHeader(t *testing.T) { +// assert := require.New(t) + +// j := []string{ +// `{ +// "description": "The number of allowed requests in the current period", +// "schema": { +// "type": "integer" +// }, +// "x-header-ext": "value" +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var h openapi.Header +// err := json.Unmarshal(data, &h) +// assert.NoError(err) + +// b, err := json.MarshalIndent(h, "", " ") +// assert.NoError(err) +// p, err := jsonpatch.CreateMergePatch(data, b) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), string(p)) + +// // testing yaml + +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Header +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) + +// } +// } diff --git a/in.go b/in.go index c9f1244..35d29b8 100644 --- a/in.go +++ b/in.go @@ -3,22 +3,17 @@ package openapi const ( // InQuery - Parameters that are appended to the URL. For example, in // /items?id=###, the query parameter is id. - InQuery In = "query" + InQuery Text = "query" // InHeader - Custom headers that are expected as part of the request. Note // that RFC7230 states header names are case insensitive. - InHeader In = "header" + InHeader Text = "header" // InCookie - Used to pass a specific cookie value to the API. - InCookie In = "cookie" + InCookie Text = "cookie" // InPath - Used together with Path Templating, where the parameter value is // actually part of the operation's URL. This does not include the host or // base path of the API. For example, in /items/{itemId}, the path parameter // is itemId. - InPath In = "path" + InPath Text = "path" ) -// In is a location where a paremeter may be located in a request. -type In string - -func (in In) String() string { - return string(in) -} +type In = Text diff --git a/info.go b/info.go index 4a81385..e601d25 100644 --- a/info.go +++ b/info.go @@ -1,55 +1,165 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" + + "github.com/Masterminds/semver" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) // Info provides metadata about the API. The metadata MAY be used by the clients // if needed, and MAY be presented in editing or documentation generation tools // for convenience. type Info struct { + Extensions `json:"-"` + Location `json:"-"` + + // Version of the OpenAPI document (which is distinct from the OpenAPI + // Specification version or the API implementation version). + // + // *required* + Version Text `json:"version"` + // The title of the API. // // *required* - Title string `json:"title" yaml:"title"` + Title Text `json:"title"` + // A short summary of the API. - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Summary Text `json:"summary,omitempty"` + // A description of the API. CommonMark syntax MAY be used for rich text // representation. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - // A URL to the Terms of Service for the API. This MUST be in the form of a URL. - TermsOfService string `json:"termsOfService,omitempty" bson:"termsOfService,omitempty"` + Description Text `json:"description,omitempty"` + + // A URL to the Terms of Service for the API. This MUST be in the form of a + // URL. + TermsOfService Text `json:"termsOfService,omitempty" bson:"termsOfService,omitempty"` + // The contact information for the exposed API. Contact *Contact `json:"contact,omitempty" bson:"contact,omitempty"` + // License information for the exposed API. License *License `json:"license,omitempty" bson:"license,omitempty"` - // Version of the OpenAPI document (which is distinct from the OpenAPI - // Specification version or the API implementation version). - // - // *required* - Version string `json:"version" yaml:"version"` - Extensions `json:"-"` } -type info Info +func (*Info) Anchors() (*Anchors, error) { return nil, nil } + +func (*Info) Kind() Kind { return KindInfo } + +func (*Info) Refs() []Ref { return nil } + +// func (i *Info) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// err := ptr.Validate() +// if err != nil { +// return nil, err +// } +// if ptr.IsRoot() { +// return i, nil +// } +// tok, _ := ptr.NextToken() +// switch tok { +// case "contact": +// if i.Contact == nil { +// return nil, newErrNotFound(i.absolute, tok) +// } +// return i.Contact, nil +// case "license": +// return i.License, nil +// } +// return nil, newErrNotResolvable(i.AbsoluteLocation(), tok) +// } + +// func (i *Info) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return i, nil +// } +// tok, _ := ptr.NextToken() +// switch tok { +// case "contact": +// if i.Contact == nil { +// return nil, newErrNotFound(i.AbsoluteLocation(), tok) +// } +// return i.Contact, nil +// case "license": +// if i.License == nil { +// return nil, newErrNotFound(i.AbsoluteLocation(), tok) +// } +// return i.License, nil +// default: +// return nil, newErrNotResolvable(i.AbsoluteLocation(), tok) +// } +// } + +func (i *Info) nodes() []node { + edges := appendEdges(nil, i.Contact) + edges = appendEdges(edges, i.License) + return edges +} + +func (i *Info) isNil() bool { + return i == nil +} + +func (i *Info) location() Location { + return i.Location +} + +func (i *Info) SemVer() (*semver.Version, error) { + return semver.NewVersion(i.Version.String()) +} + +func (*Info) mapKind() Kind { return KindUndefined } + +func (i *Info) setLocation(loc Location) error { + if i == nil { + return nil + } + i.Location = loc + if err := i.Contact.setLocation(loc.AppendLocation("contact")); err != nil { + return err + } + if err := i.License.setLocation(loc.AppendLocation("license")); err != nil { + return err + } + return nil +} + +func (*Info) sliceKind() Kind { return KindUndefined } // MarshalJSON marshals JSON func (i Info) MarshalJSON() ([]byte, error) { + type info Info + return marshalExtendedJSON(info(i)) } // UnmarshalJSON unmarshals JSON func (i *Info) UnmarshalJSON(data []byte) error { + type info Info var v info err := unmarshalExtendedJSON(data, &v) *i = Info(v) return err } -// MarshalYAML marshals YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (i Info) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(i) + j, err := i.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML data into i -func (i *Info) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, i) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (i *Info) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, i) } + +var _ node = (*Info)(nil) diff --git a/json.go b/json.go new file mode 100644 index 0000000..0e30650 --- /dev/null +++ b/json.go @@ -0,0 +1,125 @@ +package openapi + +import ( + "encoding/json" + "reflect" + "strings" + + "github.com/chanced/jsonx" + "github.com/tidwall/gjson" +) + +type JSONObjEntry struct { + Key Text + Value jsonx.RawMessage +} + +type OrderedJSONObj []JSONObjEntry + +func (j OrderedJSONObj) MarshalJSON() ([]byte, error) { + b := strings.Builder{} + b.WriteByte('{') + for _, e := range j { + if b.Len() > 1 { + b.WriteByte(',') + } + b.WriteString("\"" + e.Key.String() + "\":") + if e.Value == nil { + b.WriteString("null") + } else { + b.Write(e.Value) + } + } + b.WriteByte('}') + return []byte(b.String()), nil +} + +func (j *OrderedJSONObj) UnmarshalJSON(data []byte) error { + t := jsonx.TypeOf(data) + var v OrderedJSONObj + switch t { + case jsonx.TypeNull: + *j = nil + return nil + case jsonx.TypeObject: + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + v = append(v, JSONObjEntry{ + Key: Text(key.String()), + Value: jsonx.RawMessage(value.Raw), + }) + return true + }) + return nil + default: + return &json.UnmarshalTypeError{Value: t.String(), Type: reflect.TypeOf(jsonx.TypeObject)} + } +} + +func (j OrderedJSONObj) Get(key Text) jsonx.RawMessage { + for _, v := range j { + if v.Key == key { + return v.Value + } + } + return nil +} + +// Has returns true if key exists in j +func (j OrderedJSONObj) Has(key Text) bool { + for _, v := range j { + if v.Key == Text(key) { + return true + } + } + return false +} + +func (j OrderedJSONObj) Map() map[string]jsonx.RawMessage { + m := make(map[string]jsonx.RawMessage, len(j)) + for _, v := range j { + m[v.Key.String()] = v.Value + } + return m +} + +// Set concrete object to lp. To add JSON, use SetEncoded +func (j *OrderedJSONObj) Set(key Text, value interface{}) error { + var data []byte + var ok bool + var err error + if data, ok = value.([]byte); !ok { + data, err = json.Marshal(value) + if err != nil { + return err + } + } + for i, v := range *j { + if v.Key == key { + (*j)[i] = JSONObjEntry{ + Key: Text(key), + Value: data, + } + } + } + return nil +} + +// DecodeValue decodes a given parameter by key. +func (j OrderedJSONObj) DecodeValue(key Text, dst interface{}) error { + if g := j.Get(key); g != nil { + return json.Unmarshal(g, dst) + } else { + return json.Unmarshal([]byte("null"), dst) + } +} + +// Decode decodes all of j into dst +// +// For field-level decoding, use DecodeValue +func (j OrderedJSONObj) Decode(dst interface{}) error { + b, err := json.Marshal(j.Map()) + if err != nil { + return err + } + return json.Unmarshal(b, dst) +} diff --git a/kind.go b/kind.go new file mode 100644 index 0000000..780e44e --- /dev/null +++ b/kind.go @@ -0,0 +1,212 @@ +package openapi + +type Kind uint16 + +const ( + KindUndefined Kind = iota + KindDocument // *Document + KindComponents // *Components + KindExample // *Example + KindExampleMap // *ExampleMap + KindExampleComponent // *Component[*Example] + KindSchema // *Schema + KindSchemaSlice // *SchemaSlice + KindSchemaMap // *SchemaMap + KindSchemaRef // *SchemaRef + KindDiscriminator // *Discriminator + KindHeader // *Header + KindHeaderMap // *HeaderMap + KindHeaderSlice // *HeaderSlice + KindHeaderComponent // *Component[*Header] + KindLink // *Link + KindLinkComponent // *Component[*Link] + KindLinkMap // *LinkMap + KindResponse // *Response + KindResponseMap // *ResponseMap + KindResponseComponent // *Component[*Response] + KindParameter // *Parameter + KindParameterComponent // *Component[*Parameter] + KindParameterSlice // *ParameterSlice + KindParameterMap // *ParameterMap + KindPaths // *Paths + KindPathItem // *PathItem + KindPathItemComponent // *Component[*PathItem] + KindPathItemMap // *PathItemMap + KindRequestBody // *RequestBody + KindRequestBodyMap // *RequestBodyMap + KindRequestBodyComponent // *Component[*RequestBody] + KindCallbacks // *Callbacks + KindCallbacksComponent // *Component[*Callbacks] + KindCallbacksMap // *CallbacksMap + KindSecurityRequirementSlice // *SecurityRequirements + KindSecurityRequirement // *SecurityRequirement + KindSecurityRequirementItem // *SecurityRequirementItem + KindSecurityScheme // *SecurityScheme + KindSecuritySchemeComponent // *Component[*SecurityScheme] + KindSecuritySchemeMap // *SecuritySchemeMap + KindOperation // *Operation + KindOperationRef // *OperationRef + KindLicense // *License + KindTag // *Tag + KindTagSlice // *TagSlice + KindMediaType // *MediaType + KindMediaTypeMap // *MediaTypeMap + KindInfo // *Info + KindContact // *Contact + KindEncoding // *Encoding + KindEncodingMap // *EncodingMap + KindExternalDocs // *ExternalDocs + KindReference // *Reference + KindServer // *Server + KindServerComponent // *Component[*Server] + KindServerSlice // *ServerSlice + KindServerVariable // *ServerVariable + KindServerVariableMap // *ServerVariableMap + KindOAuthFlow // *OAuthFlow + KindOAuthFlows // *OAuthFlows + KindXML // *XML + KindScope // *Scope + KindScopes // *Scopes +) + +func (k Kind) String() string { + switch k { + case KindUndefined: + return "Undefined" + case KindDocument: + return "Document" + case KindComponents: + return "Components" + case KindExample: + return "Example" + case KindExampleMap: + return "ExampleMap" + case KindExampleComponent: + return "ExampleComponent" + case KindSchema: + return "Schema" + case KindSchemaSlice: + return "SchemaSlice" + case KindSchemaMap: + return "SchemaMap" + case KindSchemaRef: + return "SchemaRef" + case KindDiscriminator: + return "Discriminator" + case KindHeader: + return "Header" + case KindHeaderMap: + return "HeaderMap" + case KindHeaderSlice: + return "HeaderSlice" + case KindHeaderComponent: + return "HeaderComponent" + case KindLink: + return "Link" + case KindLinkComponent: + return "LinkComponent" + case KindLinkMap: + return "LinkMap" + case KindResponse: + return "Response" + case KindResponseMap: + return "ResponseMap" + case KindResponseComponent: + return "ResponseComponent" + case KindParameter: + return "Parameter" + case KindParameterComponent: + return "ParameterComponent" + case KindParameterSlice: + return "ParameterSlice" + case KindParameterMap: + return "ParameterMap" + case KindPaths: + return "Paths" + case KindPathItem: + return "PathItem" + case KindPathItemComponent: + return "PathItemComponent" + case KindPathItemMap: + return "PathItemMap" + case KindRequestBody: + return "RequestBody" + case KindRequestBodyMap: + return "RequestBodyMap" + case KindRequestBodyComponent: + return "RequestBodyComponent" + case KindCallbacks: + return "Callbacks" + case KindCallbacksComponent: + return "CallbacksComponent" + case KindCallbacksMap: + return "CallbacksMap" + case KindSecurityRequirementSlice: + return "SecurityRequirementSlice" + case KindSecurityRequirement: + return "SecurityRequirement" + case KindSecurityRequirementItem: + return "SecurityRequirementItem" + case KindSecurityScheme: + return "SecurityScheme" + case KindSecuritySchemeComponent: + return "SecuritySchemeComponent" + case KindSecuritySchemeMap: + return "SecuritySchemeMap" + case KindOperation: + return "Operation" + case KindOperationRef: + return "OperationRef" + case KindLicense: + return "License" + case KindTag: + return "Tag" + case KindTagSlice: + return "TagSlice" + case KindMediaType: + return "MediaType" + case KindMediaTypeMap: + return "MediaTypeMap" + case KindInfo: + return "Info" + case KindContact: + return "Contact" + case KindEncoding: + return "Encoding" + case KindEncodingMap: + return "EncodingMap" + case KindExternalDocs: + return "ExternalDocs" + case KindReference: + return "Reference" + case KindServer: + return "Server" + case KindServerComponent: + return "ServerComponent" + case KindServerSlice: + return "ServerSlice" + case KindServerVariable: + return "ServerVariable" + case KindServerVariableMap: + return "ServerVariableMap" + case KindOAuthFlow: + return "OAuthFlow" + case KindOAuthFlows: + return "OAuthFlows" + case KindXML: + return "XML" + case KindScope: + return "Scope" + case KindScopes: + return "Scopes" + default: + return "Invalid" + } +} + +func objSliceKind(n node) Kind { + if sn, ok := n.(objSlicedNode); ok { + return sn.objSliceKind() + } + return KindUndefined +} diff --git a/license.go b/license.go index ff662ce..dab7ee0 100644 --- a/license.go +++ b/license.go @@ -1,16 +1,101 @@ package openapi +import ( + "encoding/json" + + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + // License information for the exposed API. type License struct { + Location `json:"-"` + // The license name used for the API. // // *required* - Name string `json:"name" yaml:"name"` + Name Text `json:"name"` // An SPDX license expression for the API. The identifier field is mutually // exclusive of the url field. - Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` + Identifier Text `json:"identifier,omitempty"` // A URL to the license used for the API. This MUST be in the form of a URL. // The url field is mutually exclusive of the identifier field. - URL string `json:"url,omitempty" yaml:"url,omitempty"` + URL *uri.URI `json:"url,omitempty"` +} + +func (*License) Anchors() (*Anchors, error) { return nil, nil } + +// Kind returns KindLicense +func (*License) Kind() Kind { return KindLicense } +func (*License) sliceKind() Kind { return KindUndefined } +func (*License) mapKind() Kind { return KindUndefined } + +func (*License) nodes() []node { return nil } +func (l *License) isNil() bool { return l == nil } +func (l *License) location() Location { return l.Location } + +func (*License) Refs() []Ref { return nil } + +// func (l *License) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return l.resolveNodeByPointer(ptr) +// } +// +// func (l *License) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return l, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(l.absolute, tok) +// } +// +// MarshalJSON marshals JSON +func (l License) MarshalJSON() ([]byte, error) { + type license License + return json.Marshal(license(l)) } + +// UnmarshalJSON unmarshals JSON +func (l *License) UnmarshalJSON(data []byte) error { + *l = License{} + type license License + var a license + err := json.Unmarshal(data, &a) + if err != nil { + return err + } + *l = License(a) + return nil +} + +// MarshalYAML implements yaml.Marshaler +func (l License) MarshalYAML() (interface{}, error) { + j, err := l.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML implements yaml.Unmarshaler +func (l *License) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, l) +} + +func (l *License) setLocation(loc Location) error { + if l == nil { + return nil + } + l.Location = loc + return nil +} + +var _ node = (*License)(nil) diff --git a/link.go b/link.go index f0e2123..9b34dcc 100644 --- a/link.go +++ b/link.go @@ -2,27 +2,18 @@ package openapi import ( "encoding/json" - "errors" - "fmt" - "reflect" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// LinkKind differentiates a Link and a Reference -type LinkKind uint8 - -// ErrLinkParameterNotFound is returned if a -var ErrLinkParameterNotFound = errors.New("error: link parameter not found") - -const ( - // LinkKindObj = *Link - LinkKindObj LinkKind = iota - // LinkKindRef = *Reference - LinkKindRef +// LinkMap is a Map of either LinkMap or References to LinkMap +type ( + LinkMap = ComponentMap[*Link] ) -// LinkObj represents a possible design-time link for a response. The presence of a +// Link represents a possible design-time link for a response. The presence of a // link does not guarantee the caller's ability to successfully invoke it, // rather it provides a known relationship and traversal mechanism between // responses and other operations. @@ -33,159 +24,144 @@ const ( // For computing links, and providing instructions to execute them, a runtime // expression is used for accessing values in an operation and using them as // parameters while invoking the linked operation. -type LinkObj struct { - // A relative or absolute URI reference to an OAS operation. This field is - // mutually exclusive of the operationId field, and MUST point to an - // Operation Object. Relative operationRef values MAY be used to locate an - // existing Operation Object in the OpenAPI definition. See the rules for - // resolving Relative References. - OperationRef string `json:"operationRef,omitempty"` +type Link struct { + Extensions `json:"-"` + Location `json:"-"` + // The name of an existing, resolvable OAS operation, as defined with a // unique operationId. This field is mutually exclusive of the operationRef // field. - OperationID string `json:"operationId,omitempty"` + OperationID Text `json:"operationId,omitempty"` + + // A relative or absolute URI reference to an OAS operation. + // + // This field is mutually exclusive of the operationId field, and MUST point + // to an Operation Object. + OperationRef *OperationRef `json:"operationRef,omitempty"` + + // A description of the link. CommonMark syntax MAY be used for rich text + // representation. + Description Text `json:"description,omitempty"` + // A map representing parameters to pass to an operation as specified with - // operationId or identified via operationRef. The key is the parameter name + // operationID or identified via operationRef. + // + // The key is the parameter name // to be used, whereas the value can be a constant or an expression to be - // evaluated and passed to the linked operation. The parameter name can be + // evaluated and passed to the linked operation. + // + // The parameter name can be // qualified using the parameter location [{in}.]{name} for operations that // use the same parameter name in different locations (e.g. path.id). - Parameters LinkParameters `json:"parameters,omitempty"` + Parameters OrderedJSONObj `json:"parameters,omitempty"` + // A literal value or {expression} to use as a request body when calling the // target operation. - RequestBody json.RawMessage `json:"requestBody,omitempty"` - // A description of the link. CommonMark syntax MAY be used for rich text - // representation. - Description string `json:"description,omitempty"` - Extensions `json:"-"` + RequestBody jsonx.RawMessage `json:"requestBody,omitempty"` } -type link LinkObj -// MarshalJSON marshals JSON -func (l LinkObj) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(link(l)) +func (l *Link) Nodes() []Node { + if l == nil { + return nil + } + return downcastNodes(l.nodes()) } -// UnmarshalJSON unmarshals JSON -func (l *LinkObj) UnmarshalJSON(data []byte) error { - var lv link - if err := unmarshalExtendedJSON(data, &lv); err != nil { - return err +func (l *Link) nodes() []node { + if l == nil { + return nil } - *l = LinkObj(lv) - return nil + return appendEdges(nil, l.OperationRef) } -// MarshalYAML marshals YAML -func (l LinkObj) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(l) +func (l *Link) Refs() []Ref { + if l == nil { + return nil + } + var refs []Ref + if l.OperationRef != nil { + refs = append(refs, l.OperationRef) + } + return refs } -// UnmarshalYAML unmarshals YAML -func (l *LinkObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, l) -} +func (l *Link) Anchors() (*Anchors, error) { return nil, nil } -// DecodeRequestBody decodes l.RequestBody into dst -// -// dst should be a pointer to a concrete type -func (l *LinkObj) DecodeRequestBody(dst interface{}) error { - return json.Unmarshal(l.RequestBody, dst) -} +// func (l *Link) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return l.resolveNodeByPointer(ptr) +// } -// LinkKind returns LinkKindObj -func (l *LinkObj) LinkKind() LinkKind { return LinkKindObj } +// func (l *Link) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return l, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(l.Location.AbsoluteLocation(), tok) +// } -// ResolveLink resolves LinkObj by returning itself. resolve is not called. -func (l *LinkObj) ResolveLink(LinkResolver) (*LinkObj, error) { - return l, nil -} +func (*Link) mapKind() Kind { return KindLinkMap } +func (*Link) sliceKind() Kind { return KindUndefined } -// Link can either be a Link or a Reference -type Link interface { - ResolveLink(LinkResolver) (*LinkObj, error) - LinkKind() LinkKind +// MarshalJSON marshals JSON +func (l Link) MarshalJSON() ([]byte, error) { + type link Link + return marshalExtendedJSON(link(l)) } -// Links is a map to hold reusable LinkObjs. -type Links map[string]Link - // UnmarshalJSON unmarshals JSON -func (l *Links) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { +func (l *Link) UnmarshalJSON(data []byte) error { + type link Link + var lv link + if err := unmarshalExtendedJSON(data, &lv); err != nil { return err } - res := make(Links, len(dm)) - for k, d := range dm { - if isRefJSON(d) { - v, err := unmarshalReferenceJSON(d) - if err != nil { - return err - } - res[k] = v - continue - } - var v link - if err := unmarshalExtendedJSON(d, &v); err != nil { - return err - } - lv := LinkObj(v) - res[k] = &lv - } - *l = res + *l = Link(lv) return nil } -// LinkParameters is a map representing parameters to pass to an operation as -// specified with operationId or identified via operationRef. The key is the -// parameter name to be used, whereas the value can be a constant or an -// expression to be evaluated and passed to the linked operation. The parameter -// name can be qualified using the parameter location [{in}.]{name} for -// operations that use the same parameter name in different locations (e.g. -// path.id). -type LinkParameters map[string]json.RawMessage - -// Decode decodes all parameters into dst -func (lp LinkParameters) Decode(dst interface{}) error { - data, err := json.Marshal(lp) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (l Link) MarshalYAML() (interface{}, error) { + j, err := l.MarshalJSON() if err != nil { - return err + return nil, err } - return json.Unmarshal(data, dst) + return transcode.YAMLFromJSON(j) } -// DecodeParameter decodes a given parameter by name. -// It returns an error if the parameter can not be found. -func (lp LinkParameters) DecodeParameter(key string, dst interface{}) error { - if v, ok := lp[key]; ok { - if reflect.TypeOf(dst).Kind() == reflect.Ptr { - return json.Unmarshal(v, dst) - } - return json.Unmarshal(v, &dst) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (l *Link) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err } - return fmt.Errorf("%w {%s}", ErrLinkParameterNotFound, key) + return json.Unmarshal(j, l) } -// Has returns true if key exists in lp -func (lp LinkParameters) Has(key string) bool { - _, exists := lp[key] - return exists +// DecodeRequestBody decodes l.RequestBody into dst +// +// dst should be a pointer to a concrete type +func (l *Link) DecodeRequestBody(dst interface{}) error { + return json.Unmarshal(l.RequestBody, dst) } -// Set concrete object to lp. To add JSON, use SetEncoded -func (lp *LinkParameters) Set(key string, value interface{}) error { - data, err := json.Marshal(value) - if err != nil { - return err +func (*Link) Kind() Kind { return KindLink } + +func (l *Link) setLocation(loc Location) error { + if l == nil { + return nil + } + l.Location = loc + if l.OperationRef != nil { + l.OperationRef.Location = loc.AppendLocation("operationRef") } - lp.SetEncoded(key, data) return nil } -// SetEncoded sets the value of key to value. -// -// Value should be a json encoded byte slice -func (lp *LinkParameters) SetEncoded(key string, value []byte) { - (*lp)[key] = json.RawMessage(value) -} +func (l *Link) isNil() bool { return l == nil } + +func (l *Link) refable() {} + +var _ node = (*Link)(nil) diff --git a/link_test.go b/link_test.go index 99bd7d3..135b39e 100644 --- a/link_test.go +++ b/link_test.go @@ -1,60 +1,48 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// func TestLink(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "address": { +// "operationId": "getUserAddress", +// "parameters": { +// "userId": "$request.path.id" +// } +// } +// }`, +// `{ +// "UserRepositories": { +// "operationRef": "https://na2.gigantic-server.com/#/paths/~12.0~1repositories~1{username}/get", +// "parameters": { +// "username": "$response.body#/username" +// } +// } +// }`, +// } - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// for _, d := range j { +// data := []byte(d) +// var ll openapi.LinkMap +// err := json.Unmarshal(data, &ll) +// assert.NoError(err) +// b, err := json.Marshal(ll) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b)) -func TestLink(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "address": { - "operationId": "getUserAddress", - "parameters": { - "userId": "$request.path.id" - } - } - }`, - `{ - "UserRepositories": { - "operationRef": "https://na2.gigantic-server.com/#/paths/~12.0~1repositories~1{username}/get", - "parameters": { - "username": "$response.body#/username" - } - } - }`, - } +// // testing yaml - for _, d := range j { - data := []byte(d) - var ll openapi.Links - err := json.Unmarshal(data, &ll) - assert.NoError(err) - b, err := json.Marshal(ll) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.LinkMap +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Links - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - - } -} +// } +// } diff --git a/load.go b/load.go new file mode 100644 index 0000000..1f5dfb1 --- /dev/null +++ b/load.go @@ -0,0 +1,657 @@ +package openapi + +import ( + "context" + "fmt" + "strings" + + "github.com/Masterminds/semver" + "github.com/chanced/transcode" + "github.com/chanced/uri" + "github.com/tidwall/gjson" +) + +// TryGetSchemaDialect attempts to extract the schema dialect from raw JSON +// data. +// +// TryGetSchemaDialect will check the following fields in order: +// - $schema +// - jsonSchemaDialect +func TryGetSchemaDialect(data []byte) (string, bool) { + id := gjson.GetBytes(data, "$schema") + if id.Exists() { + return id.String(), true + } + id = gjson.GetBytes(data, "jsonSchemaDialect") + if id.Exists() { + return id.String(), true + } + return "", false +} + +// TryGetOpenAPIVersion attempts to extract the OpenAPI version from raw JSON +// data and parse it as a semver.Version. +func TryGetOpenAPIVersion(data []byte) (string, bool) { + v := gjson.GetBytes(data, "openapi") + if v.Exists() { + return v.String(), true + } + return "", false +} + +type LoadOpts struct { + DefaultSchemaDialect *uri.URI +} + +func mergeLoadOpts(opts []LoadOpts) LoadOpts { + var l LoadOpts + for _, o := range opts { + if o.DefaultSchemaDialect != nil { + l.DefaultSchemaDialect = o.DefaultSchemaDialect + } + } + return l +} + +// Load loads an OpenAPI document from a URI and validate it with the provided +// validator. +// +// Loading the raw data for OpenAPI Documents and externally referenced +// referenced JSON Schema components is done through the anonymous function fn. +// It is passed the URI of the resource and if known, the expected +// Kind. fn should return the Kind for the resource and the raw data if +// successful. +// +// Resources that can be referenced are: +// - OpenAPI Document (KindDocument) +// - JSON Schema (KindSchema) +// - Callbacks (KindCallbacks) +// - Example (KindExample) +// - Header (KindHeader) +// - Link (KindLink) +// - Parameter (KindParameter) +// - PathItem (KindPathItem) +// - Operation (KindOperation) +// - Reference (KindReference) +// - RequestBody (KindRequestBody) +// - Response (KindResponse) +// - SecurityScheme (KindSecurityScheme) +// +// fn will invoke fn with a URI containing a fragment; it will only ever +// be called to resolve to the root document data. This is why Kind must be +// returned from fn, as there may not be enough context to infer the shape of +// the data. +// +// Knowing the shape of root document prevents scenarios where we resolve +// "example.json#/foo/bar" and then later encounter a $ref to +// "example.json#/foo". Without knowing the shape of "example.json", we would +// have to extract out "example.json#/foo/bar" from the raw json/yaml, and then +// reparse "#/foo" when we hit the second $ref. As a result, there would then +// exist two references to the same object within the graph. +// +// Finally, being able to parse the root resource is necessary for anchors (i.e. +// $anchor, $dynamicAnchor, $recursiveAnchor) above referenced external +// resources. For example, if we have a reference to "example.json#/foo/bar" +// which has an anchor "#baz", that is located at the root of "example.json", it +// would not be found if example.json were not parsed entirely. +func Load(ctx context.Context, documentURI string, validator Validator, fn func(ctx context.Context, uri uri.URI, kind Kind) (Kind, []byte, error), opts ...LoadOpts) (*Document, error) { + if fn == nil { + panic("fn cannot be nil") + } + if documentURI == "" { + return nil, fmt.Errorf("documentURI cannot be empty") + } + docURI, err := uri.Parse(documentURI) + if err != nil { + return nil, fmt.Errorf("failed to parse documentURI: %w", err) + } + + if docURI.Fragment != "" { + return nil, NewError(fmt.Errorf("documentURI may not contain a fragment: received \"%s\"", docURI), *docURI) + } + l := newLoader(validator, fn, mergeLoadOpts(opts)) + n, err := l.load(ctx, *docURI, KindDocument, nil, nil) + if err != nil { + return nil, err + } + return n.(*Document), nil +} + +func newLoader(v Validator, fn func(context.Context, uri.URI, Kind) (Kind, []byte, error), opts LoadOpts) *loader { + nodes := make(map[string]nodectx) + return &loader{ + validator: v, + fn: fn, + nodes: nodes, + opts: opts, + } +} + +type loader struct { + opts LoadOpts + fn func(context.Context, uri.URI, Kind) (Kind, []byte, error) + validator Validator + doc *Document + nodes map[string]nodectx + dynamicRefs []refctx + refs []refctx +} + +func (l *loader) load(ctx context.Context, location uri.URI, ek Kind, openapi *semver.Version, dialect *uri.URI) (Node, error) { + if n, ok := l.nodes[location.String()]; ok { + return n.node, nil + } + k, data, err := l.loadData(ctx, location, ek) + if err != nil { + return nil, err + } + switch k { + case KindDocument: + return l.loadDocument(ctx, data, location) + case KindSchema: + return l.loadSchema(ctx, data, location, *openapi) + case KindCallbacks, KindExample, KindHeader, KindPathItem, KindOperation, + KindRequestBody, KindResponse, KindLink, KindSecurityScheme: + return l.loadNode(ctx, k, data, *openapi, *dialect) + default: + return nil, NewError(fmt.Errorf("loading %s as an external resource is not currently supported", k), location) + } +} + +func (l *loader) loadData(ctx context.Context, u uri.URI, ek Kind) (Kind, []byte, error) { + k, d, err := l.fn(ctx, u, ek) + if err != nil { + return k, d, err + } + + d, err = transcode.JSONFromYAML(d) + if err != nil { + return 0, nil, fmt.Errorf("failed to transcode data: %w", err) + } + + if k == KindUndefined && ek != KindUndefined { + k = ek + } + if ek != KindUndefined && k != ek { + return k, nil, NewError(fmt.Errorf("expected %s, but received %s", ek, k), u) + } + return k, d, nil +} + +func (l *loader) loadDocument(ctx context.Context, data []byte, u uri.URI) (*Document, error) { + var err error + + vs, ok := TryGetOpenAPIVersion(data) + var v *semver.Version + if ok { + v, err = semver.NewVersion(vs) + if err != nil { + return nil, NewError(fmt.Errorf("failed to parse OpenAPI version: %w", err), u) + } + } + if v == nil { + return nil, NewError(fmt.Errorf("failed to determine OpenAPI version; ensure that the OpenAPI document has an openapi field"), u) + } + + sd, err := l.getJSONSchemaDialect(data, v) + if err != nil { + return nil, NewError(fmt.Errorf("failed to determine OpenAPI schema dialect: %w", err), u) + } + if sd == nil { + return nil, NewError(fmt.Errorf("failed to determine OpenAPI schema dialect"), u) + } + + if err = l.validator.Validate(data, u, KindDocument, *v, *sd); err != nil { + return nil, NewValidationError(err, KindDocument, u) + } + + var doc Document + loc, err := NewLocation(u) + if err != nil { + return nil, NewError(err, u) + } + if err := doc.UnmarshalJSON(data); err != nil { + return nil, NewError(fmt.Errorf("failed to unmarshal OpenAPI Document: %w", err), u) + } + if err = doc.setLocation(loc); err != nil { + return nil, NewError(err, u) + } + + dc := nodectx{ + node: &doc, + openapi: *doc.OpenAPI, + jsonschema: *sd, + } + dc.root = &dc + anchors, err := doc.Anchors() + if err != nil { + return nil, NewError(fmt.Errorf("failed to get anchors: %w", err), u) + } + dc.anchors = anchors + + l.nodes[u.String()] = dc + if err = l.init(&dc, &dc, doc.nodes(), *v, *sd); err != nil { + return nil, err + } + // we only traverse the references after the top-level document is fully + // materialized. + if l.doc == nil { + l.doc = &doc + } else { + return &doc, nil + } + + var r refctx + var nodes []nodectx + for len(l.refs) > 0 { + for len(l.refs) > 0 { + // r, l.refs = l.refs[len(l.refs)-1], l.refs[:len(l.refs)-1] + r, l.refs = l.refs[0], l.refs[1:] + n, err := l.resolveRef(ctx, r) + if err != nil { + return nil, err + } + if n != nil { + nodes = append(nodes, *n) + r.resolved = n + } + + r.root.resolvedRefs = append(r.root.resolvedRefs, r) + } + for _, n := range nodes { + if err = l.init(&dc, n.root, n.nodes(), n.openapi, n.jsonschema); err != nil { + return nil, err + } + } + nodes = nil + } + if err = l.validator.ValidateDocument(&doc); err != nil { + return nil, err + } + return &doc, nil +} + +func (l *loader) resolveRef(ctx context.Context, r refctx) (*nodectx, error) { + u := r.URI() + + if u == nil { + return nil, NewValidationError(fmt.Errorf("openapi: ref URI cannot be empty"), r.Kind(), r.AbsoluteLocation()) + } + + if u.Host == "" && u.Path == "" { + return l.resolveLocalRef(ctx, r) + } else { + return l.resolveRemoteRef(ctx, r) + } +} + +func (l *loader) resolveRemoteRef(ctx context.Context, r refctx) (*nodectx, error) { + u := r.URI() + // let's see if we've already loaded this node + rooturi := *u + au := r.AbsoluteLocation() + if u.Host == "" { + rur := au.ResolveReference(u) + rooturi = *rur + } + rooturi.Fragment = "" + rooturi.RawFragment = "" + + if _, ok := l.nodes[rooturi.String()]; ok { + switch r.RefType() { + case RefTypeSchemaDynamicRef: + r.root.dynamicRefs = append(r.root.dynamicRefs, r) + case RefTypeSchemaRecursiveRef: + r.root.recursiveRefs = append(r.root.recursiveRefs, r) + } + // then we should have the node in stock + uc := r.URI() + if u.Host == "" { + uc = au.ResolveReference(u) + } + if n, ok := l.nodes[uc.String()]; ok { + return &n, r.resolve(n.node) + } else if u.Fragment == "" || strings.HasPrefix(u.Fragment, "/") { + // something went sideways + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + } else { + // we need to load the root resource first we need to load the resource + // we need to check to see if there is a reference pointing to the root first + // so we know what the expected type is + rus := rooturi.String() + for _, x := range l.refs { + if x.URI().String() == rus { + // found it. we load that one first. + if _, err := l.load(ctx, rooturi, x.RefKind(), nil, nil); err != nil { + return nil, err + } + break + } + } + + // now check to see if we've found it. + if _, ok := l.nodes[rooturi.String()]; !ok { + if _, err := l.load(ctx, rooturi, KindUndefined, &r.openapi, &r.jsonschema); err != nil { + return nil, err + } + } + // checking to make sure the root node is loaded + _, ok := l.nodes[rooturi.String()] + if !ok { + // otherwise we return an error + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + } + + switch r.RefType() { + case RefTypeSchemaDynamicRef: + r.root.dynamicRefs = append(r.root.dynamicRefs, r) + case RefTypeSchemaRecursiveRef: + r.root.recursiveRefs = append(r.root.recursiveRefs, r) + } + + // resetting ur + rooturi = *u + if u.Host == "" { + rur := au.ResolveReference(u) + rooturi = *rur + rooturi.Fragment = u.Fragment + } + // we check to see if the node is in stock + us := rooturi.String() + if n, ok := l.nodes[us]; ok { + return &n, r.resolve(n.node) + } + if u.Fragment == "" { + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + + // otherwise we may be dealing with an anchor + a := Text(u.Fragment) + + rn, ok := l.nodes[rooturi.String()] + if !ok { + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + + if a == "" { + // we should have already resolved it? + if err := r.resolve(rn.node); err != nil { + return nil, err + } + return &rn, nil + } + + if a.HasPrefix("/") { + // we aren't dealing with an anchor + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + + as, err := rn.Anchors() + if err != nil { + return nil, NewError(fmt.Errorf("openapi: failed to resolve anchors: %w", err), r.AbsoluteLocation()) + } + + an, ok := as.Standard[a] + if !ok { + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + + x, ok := l.nodes[an.AbsoluteLocation().String()] + if !ok { + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + if err := r.resolve(x.node); err != nil { + return nil, err + } + return &x, nil +} + +func (l *loader) resolveLocalRef(ctx context.Context, r refctx) (*nodectx, error) { + u := r.AbsoluteLocation() + u.Fragment = r.URI().Fragment + u.RawFragment = r.URI().RawFragment + + // if this is a $recursiveRef or a $dynamicRef, we need to cycle + // through all the refs first so we kick the can down the road. + switch r.RefType() { + case RefTypeSchemaDynamicRef: + r.root.dynamicRefs = append(r.root.dynamicRefs, r) + case RefTypeSchemaRecursiveRef: + r.root.recursiveRefs = append(r.root.recursiveRefs, r) + } + // check to see if this node has already been loaded + if n, ok := l.nodes[u.String()]; ok { + // resolve it and move along + if err := r.resolve(n.node); err != nil { + return nil, err + } + return &n, nil + } else if strings.HasPrefix(u.Fragment, "/") || r.ref.RefKind() != KindSchema { + // otherwise something went awry + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + + // we are dealing with an anchor + + switch r.RefType() { + case RefTypeSchemaDynamicRef: + a, ok := r.root.anchors.Dynamic[Text(r.URI().Fragment)] + if !ok { + return nil, NewError(fmt.Errorf("openapi: ref URI not found: %s", u), r.AbsoluteLocation()) + } + err := r.resolve(a.In) + if err != nil { + return nil, fmt.Errorf("openapi: failed to resolve node for anchor \"#%s\": %w", r.URI().Fragment, err) + } + return &nodectx{ + node: a.In, + openapi: r.openapi, + jsonschema: r.jsonschema, + root: r.root, + anchors: r.root.anchors, + }, nil + case RefTypeSchemaRecursiveRef: + a := r.root.anchors.Recursive + if a == nil { + return nil, NewError(fmt.Errorf("openapi: node does not have a $recursiveAnchor but $recursiveRef was found: %s", u), r.root.AbsoluteLocation()) + } + + return &nodectx{ + node: a.In, + openapi: r.openapi, + jsonschema: r.jsonschema, + root: r.root, + anchors: r.root.anchors, + }, nil + case RefTypeSchema: + a, ok := r.root.anchors.Standard[Text(r.URI().Fragment)] + if !ok { + return nil, NewError(fmt.Errorf("openapi: not found: %s", u), r.AbsoluteLocation()) + } + err := r.resolve(a.In) + if err != nil { + return nil, fmt.Errorf("openapi: failed to resolve node for anchor \"#%s\": %w", r.URI().Fragment, err) + } + return &nodectx{ + node: a.In, + openapi: r.openapi, + jsonschema: r.jsonschema, + root: r.root, + anchors: r.root.anchors, + }, nil + default: + return nil, NewError(fmt.Errorf("openapi: anchors are not supported for %s references: #%s", r.RefKind(), u.Fragment), r.AbsoluteLocation()) + } +} + +func (l *loader) getDocumentSchemaDialect(doc *Document) (*uri.URI, error) { + if doc.JSONSchemaDialect != nil { + return doc.JSONSchemaDialect, nil + } + if l.opts.DefaultSchemaDialect != nil { + return l.opts.DefaultSchemaDialect, nil + } + if VersionConstraints3_1.Check(doc.OpenAPI) { + return &JSONSchemaDialect202012, nil + } + // if VersionConstraints3_0.Check(doc.OpenAPI) { + // return &JSONSchemaDialect201909, nil + // } + return nil, fmt.Errorf("failed to determine OpenAPI schema dialect") +} + +func (l *loader) init(node *nodectx, root *nodectx, nodes []node, openapi semver.Version, jsonschema uri.URI) error { + for _, n := range nodes { + nc, err := newNodeCtx(n, root, &openapi, &jsonschema) + if err != nil { + return err + } + + l.nodes[n.AbsoluteLocation().String()] = nc + + if IsRef(n) { + r := n.(ref) + if !r.IsResolved() { + l.refs = append(l.refs, refctx{root: root, in: node, ref: r, openapi: nc.openapi, jsonschema: nc.jsonschema}) + } + return nil + } + if err := l.init(&nc, root, n.nodes(), nc.openapi, nc.jsonschema); err != nil { + return err + } + } + return nil +} + +func (l *loader) loadSchema(ctx context.Context, data []byte, u uri.URI, v semver.Version) (*Schema, error) { + var s Schema + if err := s.UnmarshalJSON(data); err != nil { + return nil, NewError(fmt.Errorf("failed to unmarshal JSON Schema: %w", err), u) + } + loc, err := NewLocation(u) + if err != nil { + return nil, err + } + s.setLocation(loc) + nc := nodectx{node: &s, openapi: v, jsonschema: u} + nc.root = &nc + a, err := s.Anchors() + if err != nil { + return nil, fmt.Errorf("failed to load anchors: %w", err) + } + nc.anchors = a + + if s.ID != nil && s.ID.String() != u.String() { + loc, err := NewLocation(*s.ID) + if err != nil { + return nil, NewError(fmt.Errorf("failed to parse schema ID: %w", err), u) + } + s.setLocation(loc) + l.nodes[loc.String()] = nc + } else { + l.nodes[u.String()] = nc + } + return &s, nil +} + +func (l *loader) loadNode(ctx context.Context, k Kind, data []byte, v semver.Version, s uri.URI) (nodectx, error) { + panic("not impl") +} + +// func (l *loader) resolveDynamicRefs(n *nodectx) error { +// var r refctx +// var sr refctx +// da := n.anchors.Dynamic +// for _, r = range n.resolvedRefs { +// for _, sr = range r.resolved.resolvedRefs { +// if sr.RefType() == RefTypeSchemaDynamicRef { +// a, ok := da[Text(sr.URI().Fragment)] +// if ok { +// x, ok := sr.in.node.(*Schema) +// if !ok { +// return fmt.Errorf("openapi: expected schema but got %T", sr.in.node) +// } +// x.DynamicRef.Resolved = a.In +// err := sr.resolve(a.In) +// if err != nil { +// return fmt.Errorf("openapi: failed to resolve node for anchor \"#%s\": %w", sr.URI().Fragment, err) +// } +// } +// } +// } +// } +// return nil +// } + +func (l *loader) getJSONSchemaDialect(data []byte, v *semver.Version) (*uri.URI, error) { + sds, ok := TryGetSchemaDialect(data) + var sd *uri.URI + var err error + switch { + case ok: + sd, err = uri.Parse(sds) + if err != nil { + return nil, fmt.Errorf("failed to parse JSON Schema dialect: %w", err) + } + case l.opts.DefaultSchemaDialect != nil: + sd = l.opts.DefaultSchemaDialect + case checkVersion(VersionConstraints3_1, v): + sd = &JSONSchemaDialect202012 + // case checkVersion(VersionConstraints3_0, v): + // sd = &JSONSchemaDialect201909 + default: + return nil, nil + } + return sd, nil +} + +type nodectx struct { + node + openapi semver.Version + jsonschema uri.URI + root *nodectx + anchors *Anchors + recursiveRefs []refctx + dynamicRefs []refctx + resolvedRefs []refctx +} +type refctx struct { + ref + in *nodectx + resolved *nodectx + root *nodectx + openapi semver.Version + jsonschema uri.URI +} + +func newNodeCtx(n node, root *nodectx, openapi *semver.Version, jsonschema *uri.URI) (nodectx, error) { + switch t := n.(type) { + case *Document: + + case *Schema: + if t.Schema != nil { + jsonschema = t.Schema + } + } + if jsonschema == nil { + return nodectx{}, fmt.Errorf("failed to determine JSON Schema dialect") + } + if openapi == nil { + return nodectx{}, fmt.Errorf("failed to determine OpenAPI version") + } + return nodectx{ + node: n, + jsonschema: *jsonschema, + openapi: *openapi, + root: root, + }, nil +} + +func checkVersion(c semver.Constraints, v *semver.Version) bool { + if v == nil { + return false + } + return c.Check(v) +} diff --git a/load_test.go b/load_test.go new file mode 100644 index 0000000..4c58495 --- /dev/null +++ b/load_test.go @@ -0,0 +1,151 @@ +package openapi_test + +import ( + "context" + "embed" + "fmt" + "io" + "testing" + + "github.com/Masterminds/semver" + "github.com/chanced/openapi" + "github.com/chanced/transcode" + "github.com/chanced/uri" + "github.com/sanity-io/litter" +) + +//go:embed testdata +var testdata embed.FS + +func TestLoadRefComponent(t *testing.T) { + f, err := testdata.Open("testdata/documents/comprefs.yaml") + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + doc, err := openapi.Load(ctx, "testdata/documents/comprefs.yaml", NoopValidator{}, func(ctx context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error) { + b, err := io.ReadAll(f) + if err != nil { + return 0, nil, err + } + return openapi.KindDocument, b, nil + }) + if err != nil { + t.Error(err) + } + if doc == nil { + t.Errorf("failed to load document") + } + // litter.Dump(doc) + if doc.Components.Responses.Get("Referenced").Object.Description != "/components/responses/Referenced" { + t.Errorf("expected %q got %q", "/components/responses/Referenced", doc.Components.Responses.Get("Referenced").Object.Description) + } + refpath := doc.Paths.Get("/ref") + if refpath.Post.Responses.Get("200").Object.Description != "/components/responses/Referenced" { + t.Errorf("expected %q got %q", "/components/responses/Referenced", doc.Paths.Get("/refs").Post.Responses.Get("200").Object.Description) + } + rb := doc.Components.RequestBodies.Get("Referenced") + if rb.Object.Description != "/components/requestBodies/Referenced" { + t.Errorf("expected requestBody to have description of %q, got %q", "/components/requestBodies/Referenced", rb.Object.Description) + } + rbr := refpath.Post.RequestBody.Object + if rbr.Description != rb.Object.Description { + t.Errorf("expected requestBody to have description of %q, got %q", rb.Object.Description, rbr.Description) + } +} + +func TestLoad(t *testing.T) { + f, err := testdata.Open("testdata/documents/petstore.yaml") + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + doc, err := openapi.Load(ctx, "testdata/documents/petstore.yaml", NoopValidator{}, func(ctx context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error) { + b, err := io.ReadAll(f) + // fmt.Println(string(b)) + if err != nil { + return 0, nil, err + } + return openapi.KindDocument, b, nil + }) + if err != nil { + t.Error(err) + } + if doc == nil { + t.Errorf("failed to load document") + } + // litter.Dump(doc) +} + +func TestDynamicRefs(t *testing.T) { + f, err := testdata.Open("testdata/documents/dynamic-refs.yaml") + if err != nil { + t.Fatal(err) + } + listOfT, err := testdata.Open("testdata/schemas/list-of-t.json") + if err != nil { + t.Fatal(err) + } + listOfStrings, err := testdata.Open("testdata/schemas/list-of-strings.json") + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + doc, err := openapi.Load(ctx, "testdata/documents/dynamic-refs.yaml", NoopValidator{}, func(ctx context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error) { + switch uri.String() { + case "https://json-schema.blog/list-of-t": + d, err := io.ReadAll(listOfT) + return openapi.KindSchema, d, err + case "https://json-schema.blog/list-of-strings": + d, err := io.ReadAll(listOfStrings) + return openapi.KindSchema, d, err + case "testdata/documents/dynamic-refs.yaml": + d, err := io.ReadAll(f) + return openapi.KindDocument, d, err + default: + return 0, nil, fmt.Errorf("uknown uri %q", uri) + } + }) + if err != nil { + t.Error(err) + } + + // litter.Dump(doc) + los := doc.Components.Schemas.Get("ListOfStrings") + if los.Ref.Resolved.Ref.Resolved == nil { + t.Error("expected /list-of-t to be resolved") + } + litter.Dump(los.Ref.Resolved) +} + +type NoopValidator struct{} + +func (NoopValidator) Validate(data []byte, resource uri.URI, kind openapi.Kind, openapi semver.Version, jsonschema uri.URI) error { + return nil +} + +func TestTryGetOpenAPIVersion(t *testing.T) { + f, err := testdata.Open("testdata/documents/petstore.yaml") + if err != nil { + t.Fatal(err) + } + d, _ := io.ReadAll(f) + d, err = transcode.JSONFromYAML(d) + if err != nil { + t.Errorf("failed to transcode data") + } + if len(d) == 0 { + t.Fatal("file was empty") + } + vstr, ok := openapi.TryGetOpenAPIVersion(d) + if !ok { + t.Error("failed to get openapi") + } + if vstr != "3.1.0" { + t.Errorf("expected 3.1.0 got %q", vstr) + } +} + +func (NoopValidator) ValidateDocument(document *openapi.Document) error { return nil } + +var _ openapi.Validator = (*NoopValidator)(nil) diff --git a/location.go b/location.go new file mode 100644 index 0000000..5b8155b --- /dev/null +++ b/location.go @@ -0,0 +1,78 @@ +package openapi + +import ( + "github.com/chanced/jsonpointer" + "github.com/chanced/uri" +) + +// TODO: relToRes needs to be a slice + +func NewLocation(uri uri.URI) (Location, error) { + ptr, err := jsonpointer.Parse(uri.Fragment) + if err != nil { + return Location{}, err + } + loc := Location{ + absolute: uri, + relative: ptr, + } + return loc, nil +} + +type Location struct { + absolute uri.URI + relative jsonpointer.Pointer +} + +func (l Location) String() string { + return l.absolute.String() +} + +func (l Location) AbsoluteLocation() uri.URI { + return l.absolute +} + +// RelativeLocation returns a jsonpointer.Pointer of the path from the +// containing resource file. +func (l Location) RelativeLocation() jsonpointer.Pointer { + return l.relative +} + +func (l Location) AppendLocation(p string) Location { + l.relative = l.relative.AppendString(p) + l.absolute.Fragment = l.relative.String() + l.absolute.RawFragment = l.relative.String() + return l +} + +func (l Location) withURI(uri *uri.URI) (Location, error) { + l.absolute = *uri + if len(l.absolute.Fragment) > 0 { + var err error + l.relative, err = jsonpointer.Parse(l.absolute.Fragment) + if err != nil { + return l, err + } + } + // we dont know what this is yet + l.relative = "" + return l, nil +} + +func (l Location) location() Location { + return l +} + +func (l Location) IsRelativeTo(uri *uri.URI) bool { + if uri == nil { + return false + } + a := l.absolute + a.Fragment = "" + a.RawFragment = "" + u := *uri + u.Fragment = "" + u.RawFragment = "" + + return a.String() == u.String() +} diff --git a/location_test.go b/location_test.go new file mode 100644 index 0000000..2858636 --- /dev/null +++ b/location_test.go @@ -0,0 +1,41 @@ +package openapi_test + +import ( + "testing" + + "github.com/chanced/openapi" + "github.com/chanced/uri" +) + +func TestLocationAppend(t *testing.T) { + u, _ := uri.Parse("https://example.org/schema/demo") + + loc, err := openapi.NewLocation(*u) + if err != nil { + t.Fatal(err) + } + loc = loc.AppendLocation("foo") + expected := "https://example.org/schema/demo#/foo" + if loc.String() != expected { + t.Errorf("expected %q, got %s", expected, loc.String()) + } + + loc = loc.AppendLocation("bar") + expected = "https://example.org/schema/demo#/foo/bar" + if loc.String() != expected { + t.Errorf("expected %q, got %s", expected, loc.String()) + } + u, err = uri.Parse("example.json") + if err != nil { + t.Fatal(err) + } + loc, err = openapi.NewLocation(*u) + if err != nil { + t.Fatal(err) + } + loc = loc.AppendLocation("foo") + expected = "example.json#/foo" + if loc.String() != expected { + t.Errorf("expected %q, got %s", expected, loc.String()) + } +} diff --git a/map.go b/map.go new file mode 100644 index 0000000..3a582c1 --- /dev/null +++ b/map.go @@ -0,0 +1,103 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/chanced/jsonx" + "github.com/tidwall/gjson" +) + +type KeyValue[V any] struct { + Key Text + Value V +} + +type Map[T any] struct { + Items []KeyValue[T] +} + +func (m Map[T]) Get(key Text) (T, bool) { + for _, v := range m.Items { + if v.Key == key { + return v.Value, true + } + } + var t T + return t, false +} + +func (m Map[T]) Has(key Text) bool { + for _, v := range m.Items { + if v.Key == key { + return true + } + } + return false +} + +func (m *Map[T]) Set(key Text, value T) { + if m == nil { + *m = Map[T]{} + } + for i, v := range m.Items { + if v.Key == key { + m.Items[i].Value = value + return + } + } + m.Items = append(m.Items, KeyValue[T]{Key: key, Value: value}) +} + +func (m *Map[T]) Del(key Text) { + if m == nil { + return + } + for i, v := range m.Items { + if v.Key == key { + m.Items = append(m.Items[:i], m.Items[i+1:]...) + return + } + } +} + +func (m Map[T]) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.WriteByte('{') + var err error + var s []byte + for _, v := range m.Items { + if b.Len() > 1 { + b.WriteByte(',') + } + jsonx.EncodeAndWriteString(&b, v.Key.String()) + b.WriteByte(':') + s, err = json.Marshal(v.Value) + if err != nil { + return nil, err + } + b.Write(s) + } + b.WriteByte('}') + return b.Bytes(), nil +} + +func (m *Map[T]) UnmarshalJSON(data []byte) error { + *m = Map[T]{} + var v KeyValue[T] + if !jsonx.IsObject(data) { + return &json.UnmarshalTypeError{Value: jsonx.TypeOf(data).String(), Type: reflect.TypeOf(v), Struct: "Map"} + } + var err error + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + var t T + if err = json.Unmarshal([]byte(value.Raw), &t); err != nil { + return false + } + v = KeyValue[T]{Key: Text(key.String()), Value: t} + m.Items = append(m.Items, v) + return true + }) + return err +} diff --git a/media_type.go b/media_type.go index f559f00..874aac2 100644 --- a/media_type.go +++ b/media_type.go @@ -3,40 +3,123 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/gjson" + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// Content is a map containing descriptions of potential response payloads. The key is +// ContentMap / MediaTypeMap is a map containing descriptions of potential response payloads. The key is // a media type or media type range and the value describes it. For // responses that match multiple keys, only the most specific key is // applicable. e.g. text/plain overrides text/* -type Content map[string]*MediaType +type ( + ContentMap = ObjMap[*MediaType] + MediaTypeMap = ObjMap[*MediaType] +) -// MediaType provides schema and examples for the media type identified by its key. +// MediaType provides schema and examples for the media type identified by its +// key. type MediaType struct { - // The schema defining the content of the request, response, or parameter. - Schema *SchemaObj `json:"schema,omitempty"` + Extensions `json:"-"` + Location `json:"-"` + + // The schema defining the content of the request, response, or parameter. + Schema *Schema `json:"schema,omitempty"` // Example of the media type. The example object SHOULD be in the correct // format as specified by the media type. The example field is mutually // exclusive of the examples field. Furthermore, if referencing a schema // which contains an example, the example value SHALL override the example // provided by the schema. - Example json.RawMessage `json:"example,omitempty"` + Example jsonx.RawMessage `json:"example,omitempty"` // Examples of the media type. Each example object SHOULD match the media // type and specified schema if present. The examples field is mutually // exclusive of the example field. Furthermore, if referencing a schema // which contains an example, the examples value SHALL override the example // provided by the schema. - Examples Examples `json:"examples,omitempty"` + Examples *ExampleMap `json:"examples,omitempty"` // A map between a property name and its encoding information. The key, // being the property name, MUST exist in the schema as a property. The // encoding object SHALL only apply to requestBody objects when the media // type is multipart or application/x-www-form-urlencoded. - Encoding Encodings `json:"encoding,omitempty"` - Extensions `json:"-"` + Encoding *EncodingMap `json:"encoding,omitempty"` +} + +func (mt *MediaType) Nodes() []Node { + if mt == nil { + return nil + } + return downcastNodes(mt.nodes()) +} + +func (mt *MediaType) nodes() []node { + if mt == nil { + return nil + } + return appendEdges(nil, mt.Schema, mt.Examples, mt.Encoding) } +func (mt *MediaType) Refs() []Ref { + if mt == nil { + return nil + } + refs := mt.Schema.Refs() + refs = append(refs, mt.Examples.Refs()...) + refs = append(refs, mt.Encoding.Refs()...) + return refs +} + +func (mt *MediaType) Anchors() (*Anchors, error) { + if mt == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = mt.Schema.Anchors(); err != nil { + return nil, err + } + if anchors, err = anchors.merge(mt.Examples.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(mt.Encoding.Anchors()); err != nil { + return nil, err + } + return anchors, nil +} + +// func (mt *MediaType) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return mt.resolveNodeByPointer(ptr) +// } + +// func (mt *MediaType) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return mt, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "schema": +// if mt.Schema == nil { +// return nil, newErrNotFound(mt.AbsoluteLocation(), tok) +// } +// return mt.Schema.resolveNodeByPointer(nxt) +// case "examples": +// if mt.Examples == nil { +// return nil, newErrNotFound(mt.AbsoluteLocation(), tok) +// } +// return mt.Examples.resolveNodeByPointer(nxt) +// case "encoding": +// if mt.Encoding == nil { +// return nil, newErrNotFound(mt.AbsoluteLocation(), tok) +// } +// return mt.Encoding.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(mt.Location.AbsoluteLocation(), tok) + +// } +// } + // MarshalJSON marshals mt into JSON func (mt MediaType) MarshalJSON() ([]byte, error) { type mediatype MediaType @@ -45,44 +128,55 @@ func (mt MediaType) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals json into mt func (mt *MediaType) UnmarshalJSON(data []byte) error { - type mediatype struct { - Schema *SchemaObj `json:"-"` - Example json.RawMessage `json:"example,omitempty"` - Examples Examples `json:"examples,omitempty"` - Encoding Encodings `json:"encoding,omitempty"` - Extensions `json:"-"` - } + type mediatype MediaType - v := mediatype{} + var v mediatype if err := unmarshalExtendedJSON(data, &v); err != nil { return err } *mt = MediaType(v) - - g := gjson.GetBytes(data, "schema") - if g.Exists() { - s, err := unmarshalSchemaJSON([]byte(g.Raw)) - if err != nil { - return err - } - mt.Schema = s - } return nil } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (mt MediaType) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(mt) + j, err := mt.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (mt *MediaType) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, mt) } -// UnmarshalYAML unmarshals yaml into mt -func (mt *MediaType) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, mt) +func (mt *MediaType) setLocation(loc Location) error { + if mt == nil { + return nil + } + mt.Location = loc + if err := mt.Schema.setLocation(loc.AppendLocation("schema")); err != nil { + return err + } + if err := mt.Examples.setLocation(loc.AppendLocation("examples")); err != nil { + return err + } + if err := mt.Encoding.setLocation(loc.AppendLocation("encoding")); err != nil { + return err + } + + return nil } +func (*MediaType) Kind() Kind { return KindMediaType } +func (*MediaType) mapKind() Kind { return KindMediaTypeMap } +func (*MediaType) sliceKind() Kind { return KindUndefined } + +func (mt *MediaType) isNil() bool { return mt == nil } + +var _ node = (*MediaType)(nil) diff --git a/media_type_test.go b/media_type_test.go index 1a92ef7..87e2159 100644 --- a/media_type_test.go +++ b/media_type_test.go @@ -1,83 +1,83 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// "github.com/wI2L/jsondiff" +// yaml "sigs.k8s.io/yaml" +// ) -func TestMediaType(t *testing.T) { - assert := require.New(t) - j := []string{`{ - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pet" - }, - "examples": { - "cat" : { - "summary": "An example of a cat", - "value": { - "name": "Fluffy", - "petType": "Cat", - "color": "White", - "gender": "male", - "breed": "Persian" - } - }, - "dog": { - "summary": "An example of a dog with a cat's name", - "value" : { - "name": "Puma", - "petType": "Dog", - "color": "Black", - "gender": "Female", - "breed": "Mixed" - } - }, - "frog": { - "$ref": "#/components/examples/frog-example" - } - } - } - }`, - } - for _, d := range j { - data := []byte(d) - var mt openapi.Content - err := json.Unmarshal(data, &mt) - assert.NoError(err) +// func TestMediaType(t *testing.T) { +// assert := require.New(t) +// j := []string{`{ +// "application/json": { +// "schema": { +// "$ref": "#/components/schemas/Pet" +// }, +// "examples": { +// "cat" : { +// "summary": "An example of a cat", +// "value": { +// "name": "Fluffy", +// "petType": "Cat", +// "color": "White", +// "gender": "male", +// "breed": "Persian" +// } +// }, +// "dog": { +// "summary": "An example of a dog with a cat's name", +// "value" : { +// "name": "Puma", +// "petType": "Dog", +// "color": "Black", +// "gender": "Female", +// "breed": "Mixed" +// } +// }, +// "frog": { +// "$ref": "#/components/examples/frog-example" +// } +// } +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var mt openapi.Content +// err := json.Unmarshal(data, &mt) +// assert.NoError(err) - b, err := json.MarshalIndent(mt, "", " ") - assert.NoError(err) +// b, err := json.MarshalIndent(mt, "", " ") +// assert.NoError(err) - p, err := jsondiff.CompareJSON(data, b) - if !jsonpatch.Equal(data, b) { - fmt.Println(string(data)) - fmt.Println(string(b)) - } - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), p.String()) +// p, err := jsondiff.CompareJSON(data, b) +// if !jsonpatch.Equal(data, b) { +// fmt.Println(string(data)) +// fmt.Println(string(b)) +// } +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), p.String()) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Content - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Content +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } diff --git a/method.go b/method.go new file mode 100644 index 0000000..81eb2bf --- /dev/null +++ b/method.go @@ -0,0 +1,14 @@ +package openapi + +import "net/http" + +const ( + MethodGet = Text(http.MethodGet) + MethodPut = Text(http.MethodPut) + MethodPost = Text(http.MethodPost) + MethodDelete = Text(http.MethodDelete) + MethodOptions = Text(http.MethodOptions) + MethodHead = Text(http.MethodHead) + MethodPatch = Text(http.MethodPatch) + MethodTrace = Text(http.MethodTrace) +) diff --git a/node.go b/node.go new file mode 100644 index 0000000..7a8370e --- /dev/null +++ b/node.go @@ -0,0 +1,101 @@ +package openapi + +import ( + "github.com/chanced/jsonpointer" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + +type refable interface { + node + refable() +} + +type Node interface { + // AbsoluteLocation returns the absolute path of the node in URI form. + // This includes the URI path of the resource and the JSON pointer + // of the node. + // + // e.g. openapi.json#/components/schemas/Example + AbsoluteLocation() uri.URI + + // RelativeLocation returns the path as a JSON pointer for the Node. + RelativeLocation() jsonpointer.Pointer + + // Kind returns the Kind for the given Node + Kind() Kind + + // Anchors returns a list of all Anchors in the Node and all decendants. + Anchors() (*Anchors, error) + + // Refs returns a list of all Refs from the Node and all descendants. + Refs() []Ref + + // MarshalJSON marshals JSON + // + // MarshalJSON satisfies the json.Marshaler interface + MarshalJSON() ([]byte, error) + // UnmarshalJSON unmarshals JSON + // + // UnmarshalJSON satisfies the json.Unmarshaler interface + UnmarshalJSON(data []byte) error + + // UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface + MarshalYAML() (interface{}, error) + + // UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface + UnmarshalYAML(value *yaml.Node) error + + // ResolveNodeByPointer resolves a Node by a jsonpointer. It validates the + // pointer and then attempts to resolve the Node. + // + // # Errors + // + // - [ErrNotFound] indicates that the component was not found + // + // - [ErrNotResolvable] indicates that the pointer path can not resolve to a + // Node + // + // - [jsonpointer.ErrMalformedEncoding] indicates that the pointer encoding + // is malformed + // + // - [jsonpointer.ErrMalformedStart] indicates that the pointer is not empty + // and does not start with a slash + // ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) +} + +type node interface { + Node + setLocation(loc Location) error + // init(ctx context.Context, resolver *resolver) error + // resolveNodeByPointer(ctx context.Context, resolver *resolver, p jsonpointer.Pointer) (node, error) + mapKind() Kind + sliceKind() Kind + + // resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) + location() Location + isNil() bool + nodes() []node +} + +type objSlicedNode interface { + node + objSliceKind() Kind +} + +func downcastNodes(n []node) []Node { + nodes := make([]Node, len(n)) + for i, v := range n { + nodes[i] = v + } + return nodes +} + +func appendEdges(nodes []node, elems ...node) []node { + for _, n := range elems { + if !n.isNil() { + nodes = append(nodes, n) + } + } + return nodes +} diff --git a/number.go b/number.go deleted file mode 100644 index 4912b56..0000000 --- a/number.go +++ /dev/null @@ -1,53 +0,0 @@ -package openapi - -import ( - "encoding/json" - "math/big" - "strconv" -) - -// A Number represents a JSON / YAML number literal. -type Number json.Number - -// String returns the literal text of the number. -func (n Number) String() string { return string(n) } - -// Float64 returns the number as a float64. -func (n Number) Float64() (float64, error) { - return strconv.ParseFloat(string(n), 64) -} - -// Int64 returns the number as an int64. -func (n Number) Int64() (int64, error) { - return strconv.ParseInt(string(n), 10, 64) -} - -// BigRat returns a *big.Rat representation of n -func (n Number) BigRat() (*big.Rat, bool) { - return new(big.Rat).SetString(string(n)) -} - -// BigInt returns a new *big.Int from n -func (n Number) BigInt() (*big.Int, bool) { - return new(big.Int).SetString(string(n), 10) -} - -// BigFloat returns a *big.Float -func (n Number) BigFloat(m big.RoundingMode) (*big.Float, error) { - f, _, err := big.ParseFloat(string(n), 10, 256, m) - return f, err -} - -// MarshalJSON marshals json -func (n Number) MarshalJSON() ([]byte, error) { return []byte(n), nil } - -// UnmarshalJSON unmarshals json -func (n *Number) UnmarshalJSON(data []byte) error { - var jn json.Number - err := json.Unmarshal(data, &jn) - if err != nil { - return err - } - *n = Number(jn) - return nil -} diff --git a/oauth_flow.go b/oauth_flow.go index 9e10d66..0edc686 100644 --- a/oauth_flow.go +++ b/oauth_flow.go @@ -1,84 +1,114 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" -// OAuthFlows allows configuration of the supported OAuth Flows. -type OAuthFlows struct { - // Configuration for the OAuth Implicit flow - Implicit *OAuthFlow `json:"implicit,omitempty"` - // Configuration for the OAuth Resource Owner Password flow - Password *OAuthFlow `json:"password,omitempty"` - // Configuration for the OAuth Client Credentials flow. Previously called - // application in OpenAPI 2.0. - ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty"` - // Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. - AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty"` - Extensions `json:"-"` -} - -type oauthflows OAuthFlows - -// MarshalJSON marshals json -func (oaf OAuthFlows) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(oauthflows(oaf)) -} - -// UnmarshalJSON unmarshals json -func (oaf *OAuthFlows) UnmarshalJSON(data []byte) error { - var v oauthflows - if err := unmarshalExtendedJSON(data, &v); err != nil { - return err - } - *oaf = OAuthFlows(v) - return nil -} - -// MarshalYAML marshals YAML -func (oaf OAuthFlows) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(oaf) -} - -// UnmarshalYAML unmarshals YAML -func (oaf *OAuthFlows) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, oaf) -} + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) // OAuthFlow configuration details for a supported OAuth Flow type OAuthFlow struct { + Extensions `json:"-"` + Location `json:"-"` + // The authorization URL to be used for this flow. This MUST be in the form // of a URL. The OAuth2 standard requires the use of TLS. // // Applies to: OAuth2 ("implicit", "authorizationCode") // // *required* - AuthorizationURL string `json:"authorizationUrl,omitempty"` + AuthorizationURL Text `json:"authorizationUrl,omitempty"` // The token URL to be used for this flow. This MUST be in the form of a // URL. The OAuth2 standard requires the use of TLS. // // Applies to: OAuth2Flow ("password", "clientCredentials", "authorizationCode") // // *required* - TokenURL string `json:"tokenUrl,omitempty"` + TokenURL Text `json:"tokenUrl,omitempty"` // The URL to be used for obtaining refresh tokens. This MUST be in the form // of a URL. The OAuth2 standard requires the use of TLS. - RefreshURL string `json:"refreshUrl,omitempty"` + RefreshURL Text `json:"refreshUrl,omitempty"` // The available scopes for the OAuth2 security scheme. A map between the // scope name and a short description for it. The map MAY be empty. // // *required* - Scopes map[string]string `json:"scopes"` - Extensions `json:"-"` + Scopes *Scopes `json:"scopes"` +} + +func (f *OAuthFlow) Refs() []Ref { + if f == nil { + return nil + } + return f.Scopes.Refs() } -type oauthflow OAuthFlow +func (f *OAuthFlow) Anchors() (*Anchors, error) { + if f == nil { + return nil, nil + } + return f.Scopes.Anchors() +} + +// func (f *OAuthFlow) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return f.resolveNodeByPointer(ptr) +// } + +// func (f *OAuthFlow) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return f, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "scopes": +// if f.Scopes == nil { +// return nil, newErrNotFound(f.Location.AbsoluteLocation(), tok) +// } +// return f.Scopes.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(f.Location.AbsoluteLocation(), tok) +// } +// } +func (r *OAuthFlow) Nodes() []Node { + if r == nil { + return nil + } + return downcastNodes(r.nodes()) +} + +func (r *OAuthFlow) nodes() []node { + if r == nil { + return nil + } + return appendEdges(nil, r.Scopes) +} + +func (*OAuthFlow) Kind() Kind { return KindOAuthFlow } +func (*OAuthFlow) mapKind() Kind { return KindUndefined } +func (*OAuthFlow) sliceKind() Kind { return KindUndefined } + +func (o *OAuthFlow) setLocation(loc Location) error { + if o == nil { + return nil + } + o.Location = loc + o.Scopes.setLocation(loc.AppendLocation("scopes")) + return nil +} // MarshalJSON marshals json func (o OAuthFlow) MarshalJSON() ([]byte, error) { + type oauthflow OAuthFlow + return marshalExtendedJSON(oauthflow(o)) } // UnmarshalJSON unmarshals json func (o *OAuthFlow) UnmarshalJSON(data []byte) error { + type oauthflow OAuthFlow var v oauthflow if err := unmarshalExtendedJSON(data, &v); err != nil { return err @@ -87,12 +117,190 @@ func (o *OAuthFlow) UnmarshalJSON(data []byte) error { return nil } -// MarshalYAML marshals YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (o OAuthFlow) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(o) + j, err := o.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (o *OAuthFlow) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, o) +} + +func (o *OAuthFlow) isNil() bool { return o == nil } + +// OAuthFlows allows configuration of the supported OAuth Flows. +type OAuthFlows struct { + Extensions `json:"-"` + Location `json:"-"` + + // Configuration for the OAuth Implicit flow + Implicit *OAuthFlow `json:"implicit,omitempty"` + // Configuration for the OAuth Resource Owner Password flow + Password *OAuthFlow `json:"password,omitempty"` + // Configuration for the OAuth Client Credentials flow. Previously called + // application in OpenAPI 2.0. + ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty"` + // Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. + AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty"` +} + +func (f *OAuthFlows) Nodes() []Node { + if f == nil { + return nil + } + return downcastNodes(f.nodes()) +} + +func (f *OAuthFlows) nodes() []node { + if f == nil { + return nil + } + return appendEdges(nil, f.Implicit, f.Password, f.ClientCredentials, f.AuthorizationCode) +} + +func (f *OAuthFlows) Refs() []Ref { + if f == nil { + return nil + } + var refs []Ref + refs = append(refs, f.Implicit.Refs()...) + refs = append(refs, f.Password.Refs()...) + refs = append(refs, f.ClientCredentials.Refs()...) + refs = append(refs, f.AuthorizationCode.Refs()...) + return refs +} + +func (f *OAuthFlows) isNil() bool { return f == nil } +func (f *OAuthFlows) Anchors() (*Anchors, error) { + if f == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = anchors.merge(f.Implicit.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(f.Password.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(f.ClientCredentials.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(f.AuthorizationCode.Anchors()); err != nil { + return nil, err + } + return anchors, nil +} + +// func (f *OAuthFlows) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return f.resolveNodeByPointer(ptr) +// } + +// func (f *OAuthFlows) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return f, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "implicit": +// if f.Implicit == nil { +// return nil, newErrNotFound(f.Location.AbsoluteLocation(), tok) +// } +// return f.Implicit.resolveNodeByPointer(nxt) +// case "password": +// if f.Password == nil { +// return nil, newErrNotFound(f.Location.AbsoluteLocation(), tok) +// } +// return f.Password.resolveNodeByPointer(nxt) +// case "clientCredentials": +// if f.ClientCredentials == nil { +// return nil, newErrNotFound(f.Location.AbsoluteLocation(), tok) +// } +// return f.ClientCredentials.resolveNodeByPointer(nxt) +// case "authorizationCode": +// if f.AuthorizationCode == nil { +// return nil, newErrNotFound(f.Location.AbsoluteLocation(), tok) +// } +// return f.AuthorizationCode.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(f.Location.AbsoluteLocation(), tok) +// } +// } + +func (*OAuthFlows) Kind() Kind { return KindOAuthFlows } +func (*OAuthFlows) mapKind() Kind { return KindUndefined } +func (*OAuthFlows) sliceKind() Kind { return KindUndefined } + +func (o *OAuthFlows) setLocation(loc Location) error { + if o == nil { + return nil + } + o.Location = loc + if err := o.Implicit.setLocation(loc.AppendLocation("implicit")); err != nil { + return err + } + if err := o.Password.setLocation(loc.AppendLocation("password")); err != nil { + return err + } + if err := o.ClientCredentials.setLocation(loc.AppendLocation("clientCredentials")); err != nil { + return err + } + if err := o.AuthorizationCode.setLocation(loc.AppendLocation("authorizationCode")); err != nil { + return err + } + return nil +} + +// MarshalJSON marshals json +func (o OAuthFlows) MarshalJSON() ([]byte, error) { + type oauthflows OAuthFlows + + return marshalExtendedJSON(oauthflows(o)) } -// UnmarshalYAML unmarshals YAML -func (o *OAuthFlow) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, o) +// UnmarshalJSON unmarshals json +func (f *OAuthFlows) UnmarshalJSON(data []byte) error { + type oauthflows OAuthFlows + var v oauthflows + if err := unmarshalExtendedJSON(data, &v); err != nil { + return err + } + *f = OAuthFlows(v) + return nil } + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (f OAuthFlows) MarshalYAML() (interface{}, error) { + j, err := f.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (f *OAuthFlows) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, f) +} + +var ( + _ node = (*OAuthFlow)(nil) + + _ node = (*OAuthFlows)(nil) +) diff --git a/obj_map.go b/obj_map.go new file mode 100644 index 0000000..93fd94b --- /dev/null +++ b/obj_map.go @@ -0,0 +1,210 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +type ObjMapEntry[T node] struct { + Location + Key Text + Value T +} + +// ObjMap is a map of OpenAPI Objects of type T +type ObjMap[T node] struct { + Location + Items []ObjMapEntry[T] +} + +func (*ObjMap[T]) Kind() Kind { + var t T + return t.Kind() +} +func (*ObjMap[T]) mapKind() Kind { return KindUndefined } +func (*ObjMap[T]) sliceKind() Kind { + var t T + return objSliceKind(t) +} + +func (om *ObjMap[T]) Refs() []Ref { + if om == nil { + return nil + } + refs := []Ref{} + for _, item := range om.Items { + refs = append(refs, item.Value.Refs()...) + } + return refs +} + +func (om *ObjMap[T]) nodes() []node { + if om == nil { + return nil + } + edges := make([]node, 0, len(om.Items)) + for _, item := range om.Items { + edges = appendEdges(edges, item.Value) + } + return edges +} + +func (om *ObjMap[T]) setLocation(loc Location) error { + if om == nil { + return nil + } + om.Location = loc + for _, kv := range om.Items { + if err := kv.Value.setLocation(loc.AppendLocation(string(kv.Key))); err != nil { + return err + } + } + return nil +} + +func (om *ObjMap[T]) Get(key Text) T { + var t T + for _, kv := range om.Items { + if kv.Key == key { + t = kv.Value + break + } + } + return t +} + +func (om *ObjMap[T]) Set(key Text, obj T) { + if om == nil || om.Items == nil { + *om = ObjMap[T]{ + Items: []ObjMapEntry[T]{}, + } + } + for i, kv := range om.Items { + if kv.Key == key { + om.Items[i] = ObjMapEntry[T]{ + Location: om.AppendLocation(key.String()), + Key: key, + Value: obj, + } + return + } + } + om.Items = append(om.Items, ObjMapEntry[T]{ + Location: om.AppendLocation(key.String()), + Key: key, + Value: obj, + }) +} + +func (om *ObjMap[T]) Del(key Text) { +} + +func (om *ObjMap[T]) UnmarshalJSON(data []byte) error { + var t T + var m ObjMap[T] + *om = m + + if !jsonx.IsObject(data) { + return &json.UnmarshalTypeError{ + Value: jsonx.TypeOf(data).String(), + Type: reflect.TypeOf(t), + Struct: "PathItemMap", + } + } + var pi T + var err error + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + if err = json.Unmarshal([]byte(value.Raw), &pi); err != nil { + return false + } + m.Items = append(m.Items, ObjMapEntry[T]{Key: Text(key.String()), Value: pi}) + return true + }) + *om = m + return err +} + +func (om *ObjMap[T]) MarshalJSON() ([]byte, error) { + var err error + b := bytes.Buffer{} + var j []byte + _ = j + b.WriteByte('{') + for _, entry := range om.Items { + if b.Len() > 1 { + b.WriteByte(',') + } + jsonx.EncodeAndWriteString(&b, entry.Key) + b.WriteByte(':') + j, err = entry.Value.MarshalJSON() + if err != nil { + return nil, err + } + b.Write(j) + } + b.WriteByte('}') + return b.Bytes(), err +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (om ObjMap[T]) MarshalYAML() (interface{}, error) { + j, err := om.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (om *ObjMap[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, om) +} + +func (om *ObjMap[T]) Anchors() (*Anchors, error) { + if om == nil { + return nil, nil + } + var anchors *Anchors + var err error + for _, e := range om.Items { + anchors, err = anchors.merge(e.Value.Anchors()) + if err != nil { + return anchors, err + } + } + //∆ + return anchors, nil +} + +func (om *ObjMap[T]) isNil() bool { return om == nil } + +var _ (node) = (*ObjMap[*Server])(nil) + +// func (om *ObjMap[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return om.resolveNodeByPointer(ptr) +// } + +// func (om *ObjMap[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return om, nil +// } +// tok, _ := ptr.NextToken() +// v := om.Get(Text(tok)) +// if v.isNil() { +// return nil, newErrNotFound(om.Location.AbsoluteLocation(), tok) +// } +// return nil, nil +// } diff --git a/obj_slice.go b/obj_slice.go new file mode 100644 index 0000000..f4bac13 --- /dev/null +++ b/obj_slice.go @@ -0,0 +1,126 @@ +package openapi + +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +type ObjSlice[T node] struct { + Location `json:"-"` + Items []T `json:"-"` +} + +// Anchors implements node +func (os *ObjSlice[T]) Anchors() (*Anchors, error) { + if os == nil { + return nil, nil + } + var a *Anchors + var err error + for _, x := range os.Items { + if a, err = a.merge(x.Anchors()); err != nil { + return nil, err + } + } + return a, nil +} + +// Kind implements node +func (os *ObjSlice[T]) Kind() Kind { + var t T + return objSliceKind(t) +} + +func (os *ObjSlice[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(os.Items) +} + +// Refs implements node +func (os *ObjSlice[T]) Refs() []Ref { + if os == nil { + return nil + } + var refs []Ref + for _, x := range os.Items { + refs = append(refs, x.Refs()...) + } + return refs +} + +// // ResolveNodeByPointer implements node +// func (os *ObjSlice[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return os.resolveNodeByPointer(ptr) +// } + +// func (os *ObjSlice[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return os, nil +// } +// nxt, tok, _ := ptr.Next() + +// idx, err := tok.Int() +// if err != nil || idx < 0 { +// return nil, newErrNotResolvable(os.absolute, tok) +// } +// if idx >= len(os.Items) { +// return nil, newErrNotFound(os.AbsoluteLocation(), tok) +// } +// return os.Items[idx].resolveNodeByPointer(nxt) +// } + +// UnmarshalJSON implements node +func (os *ObjSlice[T]) UnmarshalJSON(data []byte) error { + *os = ObjSlice[T]{} + items := []T{} + if err := json.Unmarshal(data, &items); err != nil { + return err + } + os.Items = items + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (os ObjSlice[T]) MarshalYAML() (interface{}, error) { + j, err := os.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (os *ObjSlice[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, os) +} + +func (os *ObjSlice[T]) nodes() []node { + var edges []node + for _, x := range os.Items { + edges = appendEdges(edges, x) + } + return edges +} + +func (os *ObjSlice[T]) setLocation(loc Location) error { + if os == nil { + return nil + } + os.Location = loc + return nil +} + +func (os *ObjSlice[T]) isNil() bool { return os == nil } + +func (*ObjSlice[T]) sliceKind() Kind { return KindUndefined } +func (*ObjSlice[T]) mapKind() Kind { return KindUndefined } + +var _ node = (*ObjSlice[*SecurityRequirement])(nil) diff --git a/openapi.go b/openapi.go deleted file mode 100644 index 52a9d13..0000000 --- a/openapi.go +++ /dev/null @@ -1,103 +0,0 @@ -package openapi - -import ( - "encoding/json" - - "github.com/chanced/openapi/yamlutil" -) - -// OpenAPI root object of the OpenAPI document. -type OpenAPI struct { - // Version - OpenAPI Version - // - // This string MUST be the version number of the OpenAPI - // Specification that the OpenAPI document uses. The openapi field SHOULD be - // used by tooling to interpret the OpenAPI document. This is not related to - // the API info.version string. - Version string `json:"openapi" yaml:"openapi"` - // Provides metadata about the API. The metadata MAY be used by - // tooling as required. - // - // *required* - Info *Info `json:"info" yaml:"info"` - // The default value for the $schema keyword within Schema Objects contained - // within this OAS document. This MUST be in the form of a URI. - JSONSchemaDialect string `json:"jsonSchemaDialect,omitempty" yaml:"jsonSchemaDialect,omitempty"` - // An array of Server Objects, which provide connectivity information to a - // target server. If the servers property is not provided, or is an empty - // array, the default value would be a Server Object with a url value of /. - Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty,omtiempty"` - // The available paths and operations for the API. - Paths *Paths `json:"paths,omitempty" yaml:"paths,omitempty"` - // The incoming webhooks that MAY be received as part of this API and that - // the API consumer MAY choose to implement. Closely related to the - // callbacks feature, this section describes requests initiated other than - // by an API call, for example by an out of band registration. The key name - // is a unique string to refer to each webhook, while the (optionally - // referenced) Path Item Object describes a request that may be initiated by - // the API provider and the expected responses. An example is available. - Webhooks *PathItems `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` - // An element to hold various schemas for the document. - Components *Components `json:"components,omitempty" yaml:"components,omitempty"` - // A list of tags used by the document with additional metadata. The order - // of the tags can be used to reflect on their order by the parsing tools. - // Not all tags that are used by the Operation Object must be declared. The - // tags that are not declared MAY be organized randomly or based on the - // tools’ logic. Each tag name in the list MUST be unique. - Tags []*Tag `json:"tags,omitempty" yaml:"tags,omitempty"` - // A declaration of which security mechanisms can be used across the API. - // - // The list of values includes alternative security requirement objects that - // can be used. - // - // Only one of the security requirement objects need to be - // satisfied to authorize a request. Individual operations can override this - // definition. - // - // To make security optional, an empty security requirement ({}) - // can be included in the array. - // - Security []*SecurityRequirement `json:"security,omitempty" yaml:"security,omitempty"` - // externalDocs Additional external documentation. - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Extensions `json:"-"` -} -type openapi OpenAPI - -// Validate validates an OpenAPI 3.1 specification -func (o OpenAPI) Validate() error { - b, err := json.Marshal(o) - if err != nil { - return err - } - var m map[string]interface{} - if err := json.Unmarshal(b, &m); err != nil { - return err - } - return validate(m) -} - -// MarshalJSON marshals JSON -func (o OpenAPI) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(openapi(o)) -} - -// UnmarshalJSON unmarshals JSON -func (o *OpenAPI) UnmarshalJSON(data []byte) error { - v := openapi{} - err := unmarshalExtendedJSON(data, &v) - *o = OpenAPI(v) - return err -} - -// MarshalYAML marshals o into yaml -func (o OpenAPI) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(o) -} - -// UnmarshalYAML unmarshals YAML data into o -func (o *OpenAPI) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, o) -} - -type OpenAPIs []*OpenAPI diff --git a/openapi_test.go b/openapi_test.go deleted file mode 100644 index 0817c30..0000000 --- a/openapi_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package openapi_test - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - - // "gopkg.in/yaml.v2" - yaml "sigs.k8s.io/yaml" -) - -func TestOpenAPI(t *testing.T) { - pass := []string{ - `{ - "openapi": "3.1.0", - "info": { - "summary": "My API's summary", - "title": "My API", - "version": "1.0.0", - "license": { - "name": "Apache 2.0", - "identifier": "Apache-2.0" - } - }, - "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base", - "paths": { - "/": { - "get": { - "parameters": [] - } - }, - "/{pathTest}": {} - }, - "webhooks": { - "myWebhook": { - "$ref": "#/components/pathItems/myPathItem", - "description": "Overriding description" - } - }, - "components": { - "securitySchemes": { - "mtls": { - "type": "mutualTLS" - } - }, - "pathItems": { - "myPathItem": { - "post": { - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "int": { - "type": "integer", - "exclusiveMaximum": 100, - "exclusiveMinimum": 0 - }, - "none": { - "type": "null" - }, - "arr": { - "type": "array", - "$comment": "Array without items keyword" - }, - "either": { - "type": [ - "string", - "null" - ] - } - }, - "discriminator": { - "propertyName": "type", - "x-extension": true - }, - "myArbitraryKeyword": true - } - } - } - } - } - } - } - } - } - `, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "components": { - "pathItems": {} - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "summary": "My lovely API", - "version": "1.0.0", - "license": { - "name": "Apache", - "identifier": "Apache-2.0" - } - }, - "components": {} - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "components": {} - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "webhooks": {} - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": {} - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": { - "/": { - "get": {} - } - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": { - "/{var}": {} - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": {}, - "components": { - "schemas": { - "model": { - "type": "object", - "properties": { - "one": { - "description": "type array", - "type": [ - "integer", - "string" - ] - }, - "two": { - "description": "type 'null'", - "type": "null" - }, - "three": { - "description": "type array including 'null'", - "type": [ - "string", - "null" - ] - }, - "four": { - "description": "array with no items", - "type": "array" - }, - "five": { - "description": "singular example", - "type": "string", - "examples": [ - "exampleValue" - ] - }, - "six": { - "description": "exclusiveMinimum true", - "exclusiveMinimum": 10 - }, - "seven": { - "description": "exclusiveMinimum false", - "minimum": 10 - }, - "eight": { - "description": "exclusiveMaximum true", - "exclusiveMaximum": 20 - }, - "nine": { - "description": "exclusiveMaximum false", - "maximum": 20 - }, - "ten": { - "description": "nullable string", - "type": [ - "string", - "null" - ] - }, - "eleven": { - "description": "x-nullable string", - "type": [ - "string", - "null" - ] - }, - "twelve": { - "description": "file/binary" - } - } - } - } - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": {}, - "servers": [ - { - "url": "/v1", - "description": "Run locally." - }, - { - "url": "https://production.com/v1", - "description": "Run on production server." - } - ] - }`, `{ - "openapi": "3.1.1", - "info": { - "title": "API", - "version": "1.0.0" - }, - "components": { - "schemas": { - "anything_boolean": true, - "nothing_boolean": false, - "anything_object": {}, - "nothing_object": { - "not": {} - } - } - } - }`, - } - - assert := require.New(t) - - for _, d := range pass { - data := []byte(d) - // checking json - var o openapi.OpenAPI - err := json.Unmarshal(data, &o) - assert.NoError(err) - b, err := json.MarshalIndent(o, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, b) { - fmt.Println(string(data), "\n------------------------\n", string(b)) - } - - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - - // testing validation - err = o.Validate() - assert.NoError(err) - err = openapi.Validate(data) - assert.NoError(err) - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.OpenAPI - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - - } - - fail := []string{ - `{ - "openapi": "3.1.1", - "info": { - "title": "API", - "version": "1.0.0" - }, - "components": { - "schemas": { - "invalid_null": null, - "invalid_number": 0, - "invalid_array": [] - } - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "servers": [ - { - "url": "https://example.com/{var}", - "variables": { - "var": { - "enum": [], - "default": "a" - } - } - } - ], - "components": {} - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "paths": {}, - "servers": { - "url": "/v1", - "description": "Run locally." - } - }`, - `{ - "openapi": "3.1.0", - "info": { - "title": "API", - "version": "1.0.0" - }, - "overlays": {} - }`, - } - for _, d := range fail { - data := []byte(d) - - err := openapi.Validate(data) - assert.Error(err) - } -} diff --git a/operation.go b/operation.go index 40d1fe3..4d6e9bc 100644 --- a/operation.go +++ b/operation.go @@ -1,36 +1,77 @@ package openapi import ( - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) +// OperationItem is an *Operation and the HTTP Method it is associated with +// +// The primary purpose of this type is for use with a Visitor +// type OperationItem struct { +// Operation *Operation +// Method Method +// } + +// func (oi *OperationItem) Walk(v Visitor) error { +// if v == nil { +// return nil +// } +// if oi == nil { +// return nil +// } +// if oi.Operation == nil { +// return nil +// } +// var err error +// v, err = v.VisitOperationItem(oi) +// if v == nil { +// return err +// } +// if err != nil { +// return err +// } +// return oi.Operation.Walk(v) +// } + // Operation describes a single API operation on a path. type Operation struct { - // A list of tags for API documentation control. Tags can be used for - // logical grouping of operations by resources or any other qualifier. - Tags []string `json:"tags,omitempty"` - // A short summary of what the operation does. - Summary string `json:"summary,omitempty"` - // A verbose explanation of the operation behavior. CommonMark syntax MAY be - // used for rich text representation. - Description string `json:"description,omitempty"` - // externalDocs Additional external documentation. - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` + // Location contains information about the location of the node in the + // document or referenced resource + Location `json:"-"` + Extensions `json:"-"` + // Unique string used to identify the operation. The id MUST be unique among // all operations described in the API. The operationId value is // case-sensitive. Tools and libraries MAY use the operationId to uniquely // identify an operation, therefore, it is RECOMMENDED to follow common // programming naming conventions. - OperationID string `json:"operationId,omitempty"` + OperationID Text `json:"operationId,omitempty"` + + // Declares this operation to be deprecated. Consumers SHOULD refrain from + // usage of the declared operation. Default value is false. + Deprecated bool `json:"deprecated,omitempty"` + + // A short summary of what the operation does. + Summary Text `json:"summary,omitempty"` + + // A verbose explanation of the operation behavior. CommonMark syntax MAY be + // used for rich text representation. + Description Text `json:"description,omitempty"` + + // A list of tags for API documentation control. Tags can be used for + // logical grouping of operations by resources or any other qualifier. + Tags Texts `json:"tags,omitempty"` + // A list of parameters that are applicable for this operation. If a // parameter is already defined at the Path Item, the new definition will // override it but can never remove it. The list MUST NOT include duplicated // parameters. A unique parameter is defined by a combination of a name and // location. The list can use the Reference Object to link to parameters // that are defined at the OpenAPI Object's components/parameters. - Parameters *ParameterList `json:"parameters,omitempty"` + Parameters *ParameterSlice `json:"parameters,omitempty"` // The request body applicable for this operation. The requestBody is fully // supported in HTTP methods where the HTTP 1.1 specification RFC7231 has @@ -38,19 +79,18 @@ type Operation struct { // HTTP spec is vague (such as GET, HEAD and DELETE), requestBody is // permitted but does not have well-defined semantics and SHOULD be avoided // if possible. - RequestBody RequestBody `json:"requestBody,omitempty"` + RequestBody *Component[*RequestBody] `json:"requestBody,omitempty"` + // The list of possible responses as they are returned from executing this // operation. - Responses Responses `json:"responses,omitempty"` + Responses *ResponseMap `json:"responses,omitempty"` // A map of possible out-of band callbacks related to the parent operation. // The key is a unique identifier for the Callback Object. Each value in the // map is a Callback Object that describes a request that may be initiated // by the API provider and the expected responses. - Callbacks Callbacks `json:"callbacks,omitempty"` - // Declares this operation to be deprecated. Consumers SHOULD refrain from - // usage of the declared operation. Default value is false. - Deprecated bool `json:"deprecated,omitempty"` + Callbacks *CallbacksMap `json:"callbacks,omitempty"` + // A declaration of which security mechanisms can be used for this // operation. The list of values includes alternative security requirement // objects that can be used. Only one of the security requirement objects @@ -58,65 +98,282 @@ type Operation struct { // an empty security requirement ({}) can be included in the array. This // definition overrides any declared top-level security. To remove a // top-level security declaration, an empty array can be used. - Security SecurityRequirements `json:"security,omitempty"` + Security *SecurityRequirementMap `json:"security,omitempty"` + // An alternative server array to service this operation. If an alternative // server object is specified at the Path Item Object or Root level, it will // be overridden by this value. - Servers []*Server `json:"servers,omitempty"` - Extensions `json:"-"` + Servers *ServerSlice `json:"servers,omitempty"` + + // externalDocs Additional external documentation. + ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` +} + +func (o *Operation) Nodes() []Node { + if o == nil { + return nil + } + return downcastNodes(o.nodes()) } -type operation struct { - Tags []string `json:"tags,omitempty"` - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` - ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"` - OperationID string `json:"operationId,omitempty"` - Parameters *ParameterList `json:"parameters,omitempty"` - RequestBody RequestBody `json:"-"` - Responses Responses `json:"responses,omitempty"` - Callbacks Callbacks `json:"callbacks,omitempty"` - Deprecated bool `json:"deprecated,omitempty"` - Security SecurityRequirements `json:"security,omitempty"` - Servers []*Server `json:"servers,omitempty"` - Extensions `json:"-"` + +func (o *Operation) nodes() []node { + if o == nil { + return nil + } + return appendEdges(nil, + o.Parameters, + o.RequestBody, + o.Responses, + o.Callbacks, + o.Security, + o.Servers, + o.ExternalDocs, + ) +} + +func (*Operation) ref() Ref { return nil } + +func (o *Operation) Refs() []Ref { + if o == nil { + return nil + } + var refs []Ref + refs = append(refs, o.ExternalDocs.Refs()...) + refs = append(refs, o.Parameters.Refs()...) + refs = append(refs, o.RequestBody.Refs()...) + refs = append(refs, o.Responses.Refs()...) + refs = append(refs, o.Callbacks.Refs()...) + refs = append(refs, o.Security.Refs()...) + refs = append(refs, o.Servers.Refs()...) + return refs } +func (o *Operation) isNil() bool { return o == nil } + +func (o *Operation) Anchors() (*Anchors, error) { + if o == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = anchors.merge(o.ExternalDocs.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.Parameters.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.RequestBody.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.Responses.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.Callbacks.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.Security.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(o.Servers.Anchors()); err != nil { + return nil, err + } + return anchors, nil +} + +// // ResolveNodeByPointer resolves a Node by a json pointer +// func (o *Operation) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return o.resolveNodeByPointer(ptr) +// } + +// func (o *Operation) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return o, nil +// } +// nxt, tok, _ := ptr.Next() +// switch nxt { +// case "externalDocs": +// if o.ExternalDocs == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.ExternalDocs.resolveNodeByPointer(nxt) +// case "parameters": +// if o.Parameters == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.Parameters.resolveNodeByPointer(nxt) +// case "requestBody": +// if o.RequestBody == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.RequestBody.resolveNodeByPointer(nxt) +// case "responses": +// if o.Responses == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.Responses.resolveNodeByPointer(nxt) +// case "callbacks": +// if o.Callbacks == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.Callbacks.resolveNodeByPointer(nxt) +// case "security": +// if o.Security == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.Security.resolveNodeByPointer(nxt) +// case "servers": +// if o.Servers == nil { +// return nil, newErrNotFound(o.AbsoluteLocation(), tok) +// } +// return o.Servers.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(o.Location.AbsoluteLocation(), tok) +// } +// } + // MarshalJSON marshals JSON func (o Operation) MarshalJSON() ([]byte, error) { + type operation Operation b, err := marshalExtendedJSON(operation(o)) if err != nil { return b, err } - if o.RequestBody != nil { - b, err = sjson.SetBytes(b, "requestBody", o.RequestBody) - } return b, err } // UnmarshalJSON unmarshals JSON func (o *Operation) UnmarshalJSON(data []byte) error { + type operation Operation var v operation if err := unmarshalExtendedJSON(data, &v); err != nil { return err } - r := gjson.GetBytes(data, "requestBody") - if len(r.Raw) > 0 { - var rb RequestBody - if err := unmarshalRequestBody([]byte(r.Raw), &rb); err != nil { - return err - } - v.RequestBody = rb - } *o = Operation(v) return nil } -// MarshalYAML marshals YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (o Operation) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(o) + j, err := o.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML -func (o *Operation) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, o) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (o *Operation) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, o) } + +func (*Operation) Kind() Kind { return KindOperation } +func (*Operation) mapKind() Kind { return KindUndefined } +func (*Operation) sliceKind() Kind { return KindUndefined } + +func (o *Operation) setLocation(loc Location) error { + if o == nil { + return nil + } + o.Location = loc + var err error + if err = o.ExternalDocs.setLocation(loc.AppendLocation("externalDocs")); err != nil { + return err + } + if err = o.Parameters.setLocation(loc.AppendLocation("parameters")); err != nil { + return err + } + if err = o.RequestBody.setLocation(loc.AppendLocation("requestBody")); err != nil { + return err + } + if err = o.Responses.setLocation(loc.AppendLocation("responses")); err != nil { + return err + } + if err = o.Callbacks.setLocation(loc.AppendLocation("callbacks")); err != nil { + return err + } + if err = o.Security.setLocation(loc.AppendLocation("security")); err != nil { + return err + } + if err = o.Servers.setLocation(loc.AppendLocation("servers")); err != nil { + return err + } + return nil +} + +// func (o *Operation) Walk(v Visitor) error { +// if v == nil { +// return nil +// } +// if o == nil { +// return nil +// } + +// var err error +// v, err = v.Visit(o) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } +// v, err = v.VisitOperation(o) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } + +// if o.Parameters != nil { +// err = o.Parameters.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.RequestBody != nil { +// err = o.RequestBody.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.Responses != nil { +// err = o.Responses.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.Callbacks != nil { +// err = o.Callbacks.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.Security != nil { +// err = o.Security.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.Servers != nil { +// err = o.Servers.Walk(v) +// if err != nil { +// return err +// } +// } +// if o.ExternalDocs != nil { +// err = o.ExternalDocs.Walk(v) +// if err != nil { +// return err +// } +// } +// return nil +// } + +var _ node = (*Operation)(nil) diff --git a/operation_ref.go b/operation_ref.go new file mode 100644 index 0000000..fce9c7d --- /dev/null +++ b/operation_ref.go @@ -0,0 +1,159 @@ +package openapi + +import ( + "encoding/json" + "fmt" + + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + +type OperationRef struct { + Location + Ref *uri.URI + Resolved *Operation +} + +func (*OperationRef) RefType() RefType { return RefTypeOperationRef } +func (*OperationRef) RefKind() Kind { return KindOperation } +func (or *OperationRef) Nodes() []Node { + if or == nil { + return nil + } + return downcastNodes(or.nodes()) +} + +func (or *OperationRef) ResolvedNode() Node { + return or.Resolved +} + +func (or *OperationRef) nodes() []node { + if or == nil { + return nil + } + var edges []node + return appendEdges(edges, or.Resolved) +} + +func (or *OperationRef) refs() []node { + return []node{or.Resolved} +} + +func (or *OperationRef) Refs() []Ref { + return nil +} + +func (or *OperationRef) IsResolved() bool { + return or.Resolved != nil +} + +// URI returns the reference URI +func (or *OperationRef) URI() *uri.URI { + return or.Ref +} + +func (*OperationRef) Anchors() (*Anchors, error) { return nil, nil } + +func (*OperationRef) Kind() Kind { return KindOperationRef } +func (*OperationRef) mapKind() Kind { return KindUndefined } +func (*OperationRef) sliceKind() Kind { return KindUndefined } + +// func (or *OperationRef) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return or.resolveNodeByPointer(ptr) +// } + +// func (or *OperationRef) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return or, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(or.AbsoluteLocation(), tok) +// } + +func (or OperationRef) MarshalJSON() ([]byte, error) { + return json.Marshal(or.Ref) +} + +func (or *OperationRef) UnmarshalJSON(data []byte) error { + var uri uri.URI + if err := json.Unmarshal(data, &uri); err != nil { + return err + } + or.Ref = &uri + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (or OperationRef) MarshalYAML() (interface{}, error) { + j, err := or.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (or *OperationRef) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, or) +} + +func (o *OperationRef) isNil() bool { return o == nil } +func (op *OperationRef) setLocation(loc Location) error { + if op == nil { + return nil + } + op.Location = loc + return nil +} + +func (o *OperationRef) resolve(n Node) error { + if o == nil { + return fmt.Errorf("openapi: OperationRef is nil") + } + if n == nil { + return fmt.Errorf("openapi: node is nil") + } + + switch n.Kind() { + case KindOperation: + o.Resolved = n.(*Operation) + default: + return fmt.Errorf("openapi: cannot resolve %s to %s", n.Kind(), o.Kind()) + } + + if op, ok := n.(*Operation); ok { + o.Resolved = op + return nil + } + + return fmt.Errorf("openapi: failed convert %s to %s", n.Kind(), o.Kind()) +} + +var ( + _ node = (*OperationRef)(nil) + _ Ref = (*OperationRef)(nil) + _ ref = (*OperationRef)(nil) +) + +// func (or *OperationRef) Walk(v Visitor) error { +// if v == nil { +// return nil +// } +// v, err := v.Visit(or) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } +// _, err = v.VisitOperationRef(or) +// return err +// } diff --git a/operation_test.go b/operation_test.go index f648092..65945bb 100644 --- a/operation_test.go +++ b/operation_test.go @@ -1,122 +1,122 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "strconv" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "strconv" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// yaml "sigs.k8s.io/yaml" +// ) -func TestOperation(t *testing.T) { - assert := require.New(t) +// func TestOperation(t *testing.T) { +// assert := require.New(t) - j := []string{ - `{ - "tags": [ - "pet" - ], - "summary": "Updates a pet in the store with form data", - "operationId": "updatePetWithForm", - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be updated", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "type": "object", - "properties": { - "name": { - "description": "Updated name of the pet", - "type": "string" - }, - "status": { - "description": "Updated status of the pet", - "type": "string" - } - }, - "required": ["status"] - } - } - } - }, - "responses": { - "200": { - "description": "Pet updated.", - "content": { - "application/json": {}, - "application/xml": {} - } - }, - "405": { - "description": "Method Not Allowed", - "content": { - "application/json": {}, - "application/xml": {} - } - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }`, - } - for _, d := range j { - data := []byte(d) - var o openapi.Operation - err := json.Unmarshal(data, &o) - assert.NoError(err) - b, err := json.Marshal(o) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// j := []string{ +// `{ +// "tags": [ +// "pet" +// ], +// "summary": "Updates a pet in the store with form data", +// "operationId": "updatePetWithForm", +// "parameters": [ +// { +// "name": "petId", +// "in": "path", +// "description": "ID of pet that needs to be updated", +// "required": true, +// "schema": { +// "type": "string" +// } +// } +// ], +// "requestBody": { +// "content": { +// "application/x-www-form-urlencoded": { +// "schema": { +// "type": "object", +// "properties": { +// "name": { +// "description": "Updated name of the pet", +// "type": "string" +// }, +// "status": { +// "description": "Updated status of the pet", +// "type": "string" +// } +// }, +// "required": ["status"] +// } +// } +// } +// }, +// "responses": { +// "200": { +// "description": "Pet updated.", +// "content": { +// "application/json": {}, +// "application/xml": {} +// } +// }, +// "405": { +// "description": "Method Not Allowed", +// "content": { +// "application/json": {}, +// "application/xml": {} +// } +// } +// }, +// "security": [ +// { +// "petstore_auth": [ +// "write:pets", +// "read:pets" +// ] +// } +// ] +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var o openapi.Operation +// err := json.Unmarshal(data, &o) +// assert.NoError(err) +// b, err := json.Marshal(o) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Operation - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Operation +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } -func TestExtensionSorting(t *testing.T) { - assert := require.New(t) - exp := `{"x-key1":1,"x-key2":2}` - for n := 0; n < 100; n++ { - op := new(openapi.Operation) - op.Extensions = make(openapi.Extensions) - op.Extensions.SetEncodedExtension("key1", []byte("1")) - op.Extensions.SetEncodedExtension("key2", []byte("2")) +// func TestExtensionSorting(t *testing.T) { +// assert := require.New(t) +// exp := `{"x-key1":1,"x-key2":2}` +// for n := 0; n < 100; n++ { +// op := new(openapi.Operation) +// op.Extensions = make(openapi.Extensions) +// op.Extensions.SetRawExtension("key1", []byte("1")) +// op.Extensions.SetRawExtension("key2", []byte("2")) - marshaled, _ := json.Marshal(op) +// marshaled, _ := json.Marshal(op) - assert.Equal(string(marshaled), exp, strconv.Itoa(n)) - } -} +// assert.Equal(string(marshaled), exp, strconv.Itoa(n)) +// } +// } diff --git a/parameter.go b/parameter.go index 17d8f82..28a3067 100644 --- a/parameter.go +++ b/parameter.go @@ -3,25 +3,23 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/gjson" + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// ParameterKind indicates whether the entry is a ParameterDef or a Reference -type ParameterKind uint +// ParameterMap is a map of Parameter +type ParameterMap = ComponentMap[*Parameter] -const ( - // ParameterKindObj is a ParameterObj - ParameterKindObj ParameterKind = iota - // ParameterKindReference indicates the Parameter is a Reference - ParameterKindReference -) - -// Parameter is either a ParameterObject or a ReferenceObject -type Parameter interface { - ParameterKind() ParameterKind - ResolveParameter(ParameterResolver) (*ParameterObj, error) -} +// ParameterSlice is list of parameters that are applicable for a given operation. +// If a parameter is already defined at the Path Item, the new definition will +// override it but can never remove it. The list MUST NOT include duplicated +// parameters. A unique parameter is defined by a combination of a name and +// location. The list can use the Reference Object to link to parameters that +// are defined at the OpenAPI Object's components/parameters. +// +// Can either be a Parameter or a Reference +type ParameterSlice = ComponentSlice[*Parameter] /* * Path Parameters @@ -126,66 +124,47 @@ type Parameter interface { * +-----------------------+------------------------------------------------------------------------------------------------------------------------------------------+ */ -// Style describes how the parameter value will be serialized depending -// on the type of the parameter value. -type Style string - -func (s Style) String() string { - return string(s) -} - -const ( - // StyleForm for - StyleForm Style = "form" - // StyleSimple comma-separated values. Corresponds to the - // {param_name} URI template. - StyleSimple Style = "simple" - // StyleMatrix semicolon-prefixed values, also known as path-style - // expansion. Corresponds to the {;param_name} URI template. - StyleMatrix Style = "matrix" - // StyleLabel dot-prefixed values, also known as label expansion. - // Corresponds to the {.param_name} URI template. - StyleLabel Style = "label" - // StyleDeepObject a simple way of rendering nested objects using - // form parameters (applies to objects only). - StyleDeepObject Style = "deepObject" - // StylePipeDelimited is pipeline-separated array values. Same as - // collectionFormat: pipes in OpenAPI 2.0. Has effect only for non-exploded - // arrays (explode: false), that is, the pipe separates the array values if - // the array is a single parameter, as in arr=a|b|c - StylePipeDelimited Style = "pipeDelimited" -) - -// ParameterObj describes a single operation parameter. +// Parameter describes a single operation parameter. // // A unique parameter is defined by a combination of a name and location. -type ParameterObj struct { +type Parameter struct { + Extensions `json:"-"` + Location `json:"-"` + // The name of the parameter. Parameter names are case sensitive: - // - If In is "path", the name field MUST correspond to a template - // expression occurring within the path field in the Paths Object. - // See Path Templating for further information. - // - If In is "header" and the name field is "Accept", "Content-Type" - // or "Authorization", the parameter definition SHALL be ignored. - // - For all other cases, the name corresponds to the parameter name - // used by the in property. + // + // - If In is "path", the name field MUST correspond to a template + // expression occurring within the path field in the Paths Object. + // See Path Templating for further information. + // + // - If In is "header" and the name field is "Accept", "Content-Type" + // or "Authorization", the parameter definition SHALL be ignored. + // + // - For all other cases, the name corresponds to the parameter name + // used by the in property. // // *required* - Name string `json:"name"` + Name Text `json:"name"` + // The location of the parameter. Possible values are "query", "header", // "path" or "cookie". // // *required* In In `json:"in"` + // A brief description of the parameter. This could contain examples of use. // CommonMark syntax MAY be used for rich text representation. - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` + // Determines whether this parameter is mandatory. If the parameter location // is "path", this property is REQUIRED and its value MUST be true. // Otherwise, the property MAY be included and its default value is false. Required *bool `json:"required,omitempty"` + // Specifies that a parameter is deprecated and SHOULD be transitioned out // of usage. Default value is false. Deprecated bool `json:"deprecated,omitempty"` + // Sets the ability to pass empty-valued parameters. This is valid only for // query parameters and allows sending a parameter with an empty value. // Default value is false. If style is used, and if behavior is n/a (cannot @@ -193,205 +172,190 @@ type ParameterObj struct { // this property is NOT RECOMMENDED, as it is likely to be removed in a // later revision. AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` + // Describes how the parameter value will be serialized depending on the // type of the parameter value. // Default values (based on value of in): - // - for query - form; + // - for query - form; // - for path - simple; // - for header - simple; // - for cookie - form. - Style string `json:"style,omitempty"` + Style Text `json:"style,omitempty"` + // When this is true, parameter values of type array or object generate // separate parameters for each value of the array or key-value pair of the // map. For other types of parameters this property has no effect. When // style is form, the default value is true. For all other styles, the // default value is false. Explode bool `json:"explode,omitempty"` + // Determines whether the parameter value SHOULD allow reserved characters, // as defined by RFC3986 :/?#[]@!$&'()*+,;= to be included without // percent-encoding. This property only applies to parameters with an in // value of query. The default value is false. AllowReserved bool `json:"allowReserved,omitempty"` + // The schema defining the type used for the parameter. - Schema *SchemaObj `json:"schema,omitempty"` + Schema *Schema `json:"schema,omitempty"` + // Examples of the parameter's potential value. Each example SHOULD // contain a value in the correct format as specified in the parameter // encoding. The examples field is mutually exclusive of the example // field. Furthermore, if referencing a schema that contains an example, // the examples value SHALL override the example provided by the schema. - Examples Examples `json:"examples,omitempty"` - Example json.RawMessage `json:"example,omitempty"` + Examples *ExampleMap `json:"examples,omitempty"` + + Example jsonx.RawMessage `json:"example,omitempty"` // For more complex scenarios, the content property can define the media // type and schema of the parameter. A parameter MUST contain either a // schema property, or a content property, but not both. When example or // examples are provided in conjunction with the schema object, the example // MUST follow the prescribed serialization strategy for the parameter. - - Content Content `json:"content,omitempty"` - Extensions `json:"-"` + Content *ContentMap `json:"content,omitempty"` } -// ResolveParameter resolves p by returning itself -func (p *ParameterObj) ResolveParameter(resolve ParameterResolver) (*ParameterObj, error) { - return p, nil +func (p *Parameter) Nodes() []Node { + if p == nil { + return nil + } + return downcastNodes(p.nodes()) } -// ParameterKind indicates that this is a Parameter for unmarshaling -// ParameterObjs by returning ParameterKindParameter -func (p *ParameterObj) ParameterKind() ParameterKind { return ParameterKindObj } - -// MarshalJSON marshals h into JSON -func (p ParameterObj) MarshalJSON() ([]byte, error) { - type parameter ParameterObj - return marshalExtendedJSON(parameter(p)) +func (p *Parameter) nodes() []node { + if p == nil { + return nil + } + return appendEdges(nil, p.Schema, p.Examples, p.Content) } -// UnmarshalJSON unmarshals json into p -func (p *ParameterObj) UnmarshalJSON(data []byte) error { - type parameter struct { - Name string `json:"name"` - In In `json:"in"` - Description string `json:"description,omitempty"` - Required *bool `json:"required,omitempty"` - Deprecated bool `json:"deprecated,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` - Style string `json:"style,omitempty"` - Explode bool `json:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty"` - Schema *SchemaObj `json:"-"` - Examples Examples `json:"examples,omitempty"` - Example json.RawMessage `json:"example,omitempty"` - Content Content `json:"content,omitempty"` - Extensions `json:"-"` +func (p *Parameter) Refs() []Ref { + if p == nil { + return nil } - v := parameter{} - - if err := unmarshalExtendedJSON(data, &v); err != nil { - return err + var refs []Ref + if p.Schema != nil { + refs = append(refs, p.Schema.Refs()...) } - g := gjson.GetBytes(data, "schema") - if g.Exists() { - s, err := unmarshalSchemaJSON([]byte(g.Raw)) - if err != nil { - return err - } - v.Schema = s + if p.Examples != nil { + refs = append(refs, p.Examples.Refs()...) } - *p = ParameterObj(v) - return nil -} - -// UnmarshalYAML unmarshals YAML data into p -func (p *ParameterObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, p) + if p.Content != nil { + refs = append(refs, p.Content.Refs()...) + } + return refs } -// MarshalYAML marshals p into YAML -func (p ParameterObj) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(p) - if err != nil { +func (p *Parameter) Anchors() (*Anchors, error) { + if p == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = anchors.merge(p.Schema.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(p.Content.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(p.Examples.Anchors()); err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return anchors, nil } -// ParameterList is list of parameters that are applicable for a given operation. -// If a parameter is already defined at the Path Item, the new definition will -// override it but can never remove it. The list MUST NOT include duplicated -// parameters. A unique parameter is defined by a combination of a name and -// location. The list can use the Reference Object to link to parameters that -// are defined at the OpenAPI Object's components/parameters. -// -// Can either be a Parameter or a Reference -type ParameterList []Parameter +// func (p *Parameter) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return p.resolveNodeByPointer(ptr) +// } + +// func (p *Parameter) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return p, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "schema": +// if p.Schema == nil { +// return nil, newErrNotFound(p.AbsoluteLocation(), tok) +// } +// return p.Schema.resolveNodeByPointer(nxt) +// case "content": +// if p.Content == nil { +// return nil, newErrNotFound(p.AbsoluteLocation(), tok) +// } +// return p.Content.resolveNodeByPointer(nxt) +// case "examples": +// if p.Examples == nil { +// return nil, newErrNotFound(p.AbsoluteLocation(), tok) +// } +// return p.Examples.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(p.AbsoluteLocation(), tok) +// } +// } -// MarshalJSON marshals JSON -func (p ParameterList) MarshalJSON() ([]byte, error) { - if p != nil { - return json.Marshal([]Parameter(p)) - } - return json.Marshal(make([]Parameter, 0)) +// MarshalJSON marshals h into JSON +func (p Parameter) MarshalJSON() ([]byte, error) { + type parameter Parameter + return marshalExtendedJSON(parameter(p)) } -// UnmarshalJSON unmarshals JSON data into p -func (p *ParameterList) UnmarshalJSON(data []byte) error { - var rd []json.RawMessage - var err error - if err = json.Unmarshal(data, &rd); err != nil { +// UnmarshalJSON unmarshals json into p +func (p *Parameter) UnmarshalJSON(data []byte) error { + type parameter Parameter + var v parameter + + if err := unmarshalExtendedJSON(data, &v); err != nil { return err } - items := make([]Parameter, len(rd)) - for i, d := range rd { - var p Parameter - err = unmarshalParameterJSON(d, &p) - if err != nil { - return err - } - items[i] = p - } - *p = items + *p = Parameter(v) return nil } -func unmarshalParameterJSON(data []byte, dst *Parameter) error { - var err error - if isRefJSON(data) { - var v Reference - err = json.Unmarshal(data, &v) - *dst = &v - } else { - var v ParameterObj - err = json.Unmarshal(data, &v) - *dst = &v - } - return err -} +func (*Parameter) Kind() Kind { return KindParameter } +func (*Parameter) mapKind() Kind { return KindParameterMap } +func (*Parameter) sliceKind() Kind { return KindParameterSlice } -// UnmarshalYAML unmarshals YAML data into p -func (p *ParameterList) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, p) -} - -// MarshalYAML marshals p into YAML -func (p ParameterList) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(p) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (p Parameter) MarshalYAML() (interface{}, error) { + j, err := p.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) } -// Parameters is a map of Parameter -type Parameters map[string]Parameter +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (p *Parameter) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, p) +} -// UnmarshalJSON unmarshals JSON -func (p *Parameters) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { +func (p *Parameter) setLocation(loc Location) error { + if p == nil { + return nil + } + p.Location = loc + if err := p.Schema.setLocation(loc.AppendLocation("schema")); err != nil { + return err + } + if err := p.Content.setLocation(loc.AppendLocation("content")); err != nil { return err } - res := make(Parameters, len(dm)) - for k, d := range dm { - if isRefJSON(d) { - v, err := unmarshalReferenceJSON(d) - if err != nil { - return err - } - res[k] = v - continue - } - var v ParameterObj - if err := unmarshalExtendedJSON(d, &v); err != nil { - return err - } - - res[k] = &v + if err := p.Examples.setLocation(loc.AppendLocation("examples")); err != nil { + return err } - *p = res + return nil } +func (p *Parameter) isNil() bool { return p == nil } + +func (*Parameter) refable() {} + +var _ node = (*Parameter)(nil) diff --git a/parameter_test.go b/parameter_test.go index 42634ae..ba74184 100644 --- a/parameter_test.go +++ b/parameter_test.go @@ -1,120 +1,146 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "os" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/sanity-io/litter" +// "github.com/stretchr/testify/require" +// yaml "gopkg.in/yaml.v3" +// ) -func TestParameter(t *testing.T) { - assert := require.New(t) +// type X struct{} - j := []string{ - `{ - "name": "token", - "in": "header", - "description": "token to be passed as a header", - "required": true, - "schema": { - "type": "array", - "items": { - "type": "integer", - "format": "int64" - } - }, - "style": "simple" - }`, - `{ - "name": "username", - "in": "path", - "description": "username to fetch", - "required": true, - "schema": { - "type": "string" - } - }`, - `{ - "name": "id", - "in": "query", - "description": "ID of the object to fetch", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - }, - "style": "form", - "explode": true - }`, - `{ - "in": "query", - "name": "freeForm", - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "style": "form" - }`, - `{ - "in": "query", - "name": "coordinates", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "lat", - "long" - ], - "properties": { - "lat": { - "type": "number" - }, - "long": { - "type": "number" - } - } - } - } - } - }`, - } - for _, d := range j { - data := []byte(d) - var p openapi.ParameterObj - err := json.Unmarshal(data, &p) - assert.NoError(err) - b, err := json.MarshalIndent(p, "", " ") +// // UnmarshalYAML implements yaml.Unmarshaler +// func (x *X) UnmarshalYAML(value *yaml.Node) error { +// for _, c := range value.Content { +// litter.Dump(c) +// } +// return nil +// } - assert.NoError(err) - if !jsonpatch.Equal(data, b) { - fmt.Println(string(b)) - } +// var _ yaml.Unmarshaler = (*X)(nil) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// func TestSpike(t *testing.T) { +// d, err := os.ReadFile("./test_spec/petstore.yaml") +// if err != nil { +// panic(err) +// } +// x := X{} +// err = yaml.Unmarshal(d, &x) +// if err != nil { +// panic(err) +// } +// } - // testing yaml +// func TestParameter(t *testing.T) { +// assert := require.New(t) - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.ParameterObj - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// j := []string{ +// `{ +// "name": "token", +// "in": "header", +// "description": "token to be passed as a header", +// "required": true, +// "schema": { +// "type": "array", +// "items": { +// "type": "integer", +// "format": "int64" +// } +// }, +// "style": "simple" +// }`, +// `{ +// "name": "username", +// "in": "path", +// "description": "username to fetch", +// "required": true, +// "schema": { +// "type": "string" +// } +// }`, +// `{ +// "name": "id", +// "in": "query", +// "description": "ID of the object to fetch", +// "required": false, +// "schema": { +// "type": "array", +// "items": { +// "type": "string" +// } +// }, +// "style": "form", +// "explode": true +// }`, +// `{ +// "in": "query", +// "name": "freeForm", +// "schema": { +// "type": "object", +// "additionalProperties": { +// "type": "integer" +// } +// }, +// "style": "form" +// }`, +// `{ +// "in": "query", +// "name": "coordinates", +// "content": { +// "application/json": { +// "schema": { +// "type": "object", +// "required": [ +// "lat", +// "long" +// ], +// "properties": { +// "lat": { +// "type": "number" +// }, +// "long": { +// "type": "number" +// } +// } +// } +// } +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var p openapi.Parameter +// err := json.Unmarshal(data, &p) +// assert.NoError(err) +// b, err := json.MarshalIndent(p, "", " ") - } -} +// assert.NoError(err) +// if !jsonpatch.Equal(data, b) { +// fmt.Println(string(b)) +// } + +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) + +// // testing yaml + +// // y, err := yaml.JSONToYAML(data) +// // assert.NoError(err) +// // var yo openapi.Parameter +// // err = yaml.Unmarshal(y, &yo) +// // assert.NoError(err) +// // yb, err := json.MarshalIndent(yo, "", " ") +// // assert.NoError(err) +// // if !jsonpatch.Equal(data, yb) { +// // fmt.Println(string(data), "\n------------------------\n", string(yb)) +// // } +// // assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) + +// } +// } diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..764d6c8 --- /dev/null +++ b/parser.go @@ -0,0 +1 @@ +package openapi diff --git a/path.go b/path.go deleted file mode 100644 index 0dc2932..0000000 --- a/path.go +++ /dev/null @@ -1,239 +0,0 @@ -package openapi - -import ( - "encoding/json" - "strings" - - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/gjson" - "gopkg.in/yaml.v2" -) - -// PathKind indicates whether the PathObj is a Path or a Reference -type PathKind uint8 - -const ( - // PathKindObj = PathObj - PathKindObj PathKind = iota - // PathKindRef = Reference - PathKindRef -) - -// PathValue is relative path to an individual endpoint. The path is appended -// (no relative URL resolution) to the expanded URL from the Server Object's url -// field in order to construct the full URL. PathValue templating is allowed. When -// matching URLs, concrete (non-templated) paths would be matched before their -// templated counterparts. Templated paths with the same hierarchy but different -// templated names MUST NOT exist as they are identical. In case of ambiguous -// matching, it's up to the tooling to decide which one to use. -type PathValue string - -func (pv PathValue) String() string { - str := string(pv) - if len(pv) == 0 { - return "/" - } - if pv[0] != '/' { - return "/" + str - } - return str -} - -// // Params returns all params in the path -// func (pv PathValue) Params() []string { -// panic("not impl") -// } - -// MarshalJSON Marshals PathEntry to JSON -func (pv PathValue) MarshalJSON() ([]byte, error) { - return json.Marshal(pv.String()) -} - -// MarshalYAML Marshals PathEntry to YAML -func (pv PathValue) MarshalYAML() ([]byte, error) { - return yaml.Marshal(pv.String()) -} - -// Paths holds the relative paths to the individual endpoints and their -// operations. The path is appended to the URL from the Server Object in order -// to construct the full URL. The Paths MAY be empty, due to Access Control List -// (ACL) constraints. -type Paths struct { - Items map[PathValue]*PathObj `json:"-"` - Extensions `json:"-"` -} - -// MarshalJSON marshals JSON -func (p Paths) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, len(p.Items)+len(p.Extensions)) - for k, v := range p.Items { - m[k.String()] = v - } - for k, v := range p.Extensions { - m[k] = v - } - return json.Marshal(m) -} - -// UnmarshalJSON unmarshals JSON data into p -func (p *Paths) UnmarshalJSON(data []byte) error { - *p = Paths{ - Items: map[PathValue]*PathObj{}, - Extensions: Extensions{}, - } - var err error - gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { - if strings.HasPrefix(key.String(), "x-") { - p.SetEncodedExtension(key.String(), []byte(value.Raw)) - } else { - var v PathObj - err = json.Unmarshal([]byte(value.Raw), &v) - p.Items[PathValue(key.String())] = &v - } - return err == nil - }) - return err -} - -// PathObj describes the operations available on a single path. A PathObj Item MAY -// be empty, due to ACL constraints. The path itself is still exposed to the -// documentation viewer but they will not know which operations and parameters -// are available. -type PathObj struct { - // Allows for a referenced definition of this path item. The referenced - // structure MUST be in the form of a Path Item Object. In case a Path Item - // Object field appears both in the defined object and the referenced - // object, the behavior is undefined. See the rules for resolving Relative - // References. - Ref string `json:"$ref,omitempty"` - // An optional, string summary, intended to apply to all operations in this path. - Summary string `json:"summary,omitempty"` - // An optional, string description, intended to apply to all operations in - // this path. CommonMark syntax MAY be used for rich text representation. - Description string `json:"description,omitempty"` - // A definition of a GET operation on this path. - Get *Operation `json:"get,omitempty"` - // A definition of a PUT operation on this path. - Put *Operation `json:"put,omitempty"` - // A definition of a POST operation on this path. - Post *Operation `json:"post,omitempty"` - // A definition of a DELETE operation on this path. - Delete *Operation `json:"delete,omitempty"` - // A definition of a OPTIONS operation on this path. - Options *Operation `json:"options,omitempty"` - // A definition of a HEAD operation on this path. - Head *Operation `json:"head,omitempty"` - // A definition of a PATCH operation on this path. - Patch *Operation `json:"patch,omitempty"` - // A definition of a TRACE operation on this path. - Trace *Operation `json:"trace,omitempty"` - // An alternative server array to service all operations in this path. - Servers []*Server `json:"servers,omitempty"` - // A list of parameters that are applicable for all the operations described - // under this path. These parameters can be overridden at the operation - // level, but cannot be removed there. The list MUST NOT include duplicated - // parameters. A unique parameter is defined by a combination of a name and - // location. The list can use the Reference Object to link to parameters - // that are defined at the OpenAPI Object's components/parameters. - Parameters *ParameterList `json:"parameters,omitempty"` - Extensions `json:"-"` -} - -type path PathObj - -// PathKind returns PathKindPath -func (p *PathObj) PathKind() PathKind { return PathKindObj } - -// ResolvePath resolves PathObj by returning itself. resolve is not called. -func (p *PathObj) ResolvePath(PathResolver) (*PathObj, error) { - return p, nil -} - -// MarshalJSON marshals p into JSON -func (p PathObj) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(path(p)) -} - -// UnmarshalJSON unmarshals json into p -func (p *PathObj) UnmarshalJSON(data []byte) error { - var v path - if err := unmarshalExtendedJSON(data, &v); err != nil { - return err - } - *p = PathObj(v) - return nil -} - -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML -func (p PathObj) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(p) -} - -// UnmarshalYAML unmarshals yaml into s -func (p *PathObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, p) -} - -// Path can either be a Path or a Reference -type Path interface { - ResolvePath(PathResolver) (*PathObj, error) - PathKind() PathKind -} - -// PathItems is a map of Paths that can either be a Path or a Reference -type PathItems map[string]Path - -// UnmarshalJSON unmarshals JSON data into rp -func (rp *PathItems) UnmarshalJSON(data []byte) error { - var rd map[string]json.RawMessage - err := json.Unmarshal(data, &rd) - if err != nil { - return err - } - res := PathItems{} - for k, d := range rd { - if isRefJSON(data) { - var v Reference - if err = json.Unmarshal(d, &v); err != nil { - return err - } - res[k] = &v - } else { - var v PathObj - if err = json.Unmarshal(d, &v); err != nil { - return err - } - res[k] = &v - } - } - *rp = res - return nil - -} - -// UnmarshalYAML unmarshals YAML data into rp -func (rp *PathItems) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, rp) -} - -// MarshalYAML marshals rp into YAML -func (rp PathItems) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(rp) - if err != nil { - return nil, err - } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err -} - -func unmarshalPathJSON(data []byte) (Path, error) { - if isRefJSON(data) { - return unmarshalReferenceJSON(data) - } - var p path - err := json.Unmarshal(data, &p) - v := PathObj(p) - return &v, err -} diff --git a/path_item.go b/path_item.go new file mode 100644 index 0000000..5383aff --- /dev/null +++ b/path_item.go @@ -0,0 +1,383 @@ +package openapi + +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +// PathItem describes the operations available on a single path. A PathItem Item MAY +// be empty, due to ACL constraints. The path itself is still exposed to the +// documentation viewer but they will not know which operations and parameters +// are available. +type PathItem struct { + Location `json:"-"` + Extensions `json:"-"` + + // An optional, string summary, intended to apply to all operations in this path. + Summary Text `json:"summary,omitempty"` + + // An optional, string description, intended to apply to all operations in + // this path. CommonMark syntax MAY be used for rich text representation. + Description Text `json:"description,omitempty"` + + // A list of parameters that are applicable for all the operations described + // under this path. These parameters can be overridden at the operation + // level, but cannot be removed there. The list MUST NOT include duplicated + // parameters. A unique parameter is defined by a combination of a name and + // location. The list can use the Reference Object to link to parameters + // that are defined at the OpenAPI Object's components/parameters. + Parameters *ParameterSlice `json:"parameters,omitempty"` + + // A definition of a GET operation on this path. + Get *Operation `json:"get,omitempty"` + + // A definition of a PUT operation on this path. + Put *Operation `json:"put,omitempty"` + + // A definition of a POST operation on this path. + Post *Operation `json:"post,omitempty"` + + // A definition of a DELETE operation on this path. + Delete *Operation `json:"delete,omitempty"` + + // A definition of a OPTIONS operation on this path. + Options *Operation `json:"options,omitempty"` + + // A definition of a HEAD operation on this path. + Head *Operation `json:"head,omitempty"` + + // A definition of a PATCH operation on this path. + Patch *Operation `json:"patch,omitempty"` + + // A definition of a TRACE operation on this path. + Trace *Operation `json:"trace,omitempty"` + + // An alternative server array to service all operations in this path. + Servers *ServerSlice `json:"servers,omitempty"` +} + +func (pi *PathItem) Nodes() []Node { + if pi == nil { + return nil + } + return downcastNodes(pi.nodes()) +} + +func (pi *PathItem) nodes() []node { + if pi == nil { + return nil + } + var edges []node + edges = appendEdges(edges, pi.Servers) + edges = appendEdges(edges, pi.Parameters) + edges = appendEdges(edges, pi.Get) + edges = appendEdges(edges, pi.Put) + edges = appendEdges(edges, pi.Post) + edges = appendEdges(edges, pi.Delete) + edges = appendEdges(edges, pi.Options) + edges = appendEdges(edges, pi.Head) + edges = appendEdges(edges, pi.Patch) + edges = appendEdges(edges, pi.Trace) + return edges +} +func (pi *PathItem) ref() Ref { return nil } + +func (pi *PathItem) Refs() []Ref { + if pi == nil { + return nil + } + var refs []Ref + refs = append(refs, pi.Servers.Refs()...) + refs = append(refs, pi.Parameters.Refs()...) + refs = append(refs, pi.Get.Refs()...) + refs = append(refs, pi.Put.Refs()...) + refs = append(refs, pi.Post.Refs()...) + refs = append(refs, pi.Delete.Refs()...) + refs = append(refs, pi.Options.Refs()...) + refs = append(refs, pi.Head.Refs()...) + refs = append(refs, pi.Patch.Refs()...) + refs = append(refs, pi.Trace.Refs()...) + return refs +} + +func (pi *PathItem) Anchors() (*Anchors, error) { + if pi == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = anchors.merge(pi.Get.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Put.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Post.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Delete.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Options.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Head.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Patch.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Trace.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Servers.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(pi.Parameters.Anchors()); err != nil { + return nil, err + } + return anchors, err +} + +func (*PathItem) mapKind() Kind { return KindPathItemMap } + +func (*PathItem) sliceKind() Kind { return KindUndefined } + +func (p *PathItem) setLocation(loc Location) error { + if p == nil { + return nil + } + p.Location = loc + var err error + if err = p.Delete.setLocation(loc.AppendLocation("delete")); err != nil { + return err + } + if err = p.Get.setLocation(loc.AppendLocation("get")); err != nil { + return err + } + if err = p.Head.setLocation(loc.AppendLocation("head")); err != nil { + return err + } + if err = p.Options.setLocation(loc.AppendLocation("options")); err != nil { + return err + } + if err = p.Patch.setLocation(loc.AppendLocation("patch")); err != nil { + return err + } + if err = p.Post.setLocation(loc.AppendLocation("post")); err != nil { + return err + } + if err = p.Put.setLocation(loc.AppendLocation("put")); err != nil { + return err + } + if err = p.Trace.setLocation(loc.AppendLocation("trace")); err != nil { + return err + } + if err = p.Parameters.setLocation(loc.AppendLocation("parameters")); err != nil { + return err + } + if err = p.Servers.setLocation(loc.AppendLocation("servers")); err != nil { + return err + } + + return nil +} + +// MarshalJSON marshals p into JSON +func (p PathItem) MarshalJSON() ([]byte, error) { + type path PathItem + return marshalExtendedJSON(path(p)) +} + +// UnmarshalJSON unmarshals json into p +func (p *PathItem) UnmarshalJSON(data []byte) error { + type path PathItem + + var v path + if err := unmarshalExtendedJSON(data, &v); err != nil { + return err + } + *p = PathItem(v) + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (p PathItem) MarshalYAML() (interface{}, error) { + j, err := p.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (p *PathItem) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, p) +} + +func (*PathItem) Kind() Kind { return KindPathItem } + +func (pi *PathItem) isNil() bool { return pi == nil } + +func (*PathItem) refable() {} + +var _ node = (*PathItem)(nil) + +// func (pi *PathItem) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } + +// return pi.resolveNodeByPointer(ptr) +// } + +// func (pi *PathItem) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return pi, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "get": +// if pi.Get == nil { +// return nil, newErrNotFound(pi.Location.AbsoluteLocation(), tok) +// } +// return pi.resolveNodeByPointer(nxt) +// case "put": +// if pi.Put == nil { +// return nil, newErrNotFound(pi.Location.AbsoluteLocation(), tok) +// } +// return pi.Put.resolveNodeByPointer(nxt) +// case "post": +// if pi.Post == nil { +// return nil, newErrNotFound(pi.Location.AbsoluteLocation(), tok) +// } +// return pi.Post.resolveNodeByPointer(nxt) +// case "delete": +// if pi.Delete == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Delete.resolveNodeByPointer(nxt) +// case "options": +// if pi.Options == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Options.resolveNodeByPointer(nxt) +// case "head": +// if pi.Head == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Head.resolveNodeByPointer(nxt) +// case "patch": +// if pi.Patch == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Patch.resolveNodeByPointer(nxt) +// case "trace": +// if pi.Trace == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Trace.resolveNodeByPointer(nxt) +// case "servers": +// if pi.Servers == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Servers.resolveNodeByPointer(nxt) +// case "parameters": +// if pi.Parameters == nil { +// return nil, newErrNotFound(pi.AbsoluteLocation(), tok) +// } +// return pi.Parameters.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(pi.Location.AbsoluteLocation(), tok) +// } +// } + +// func (pi *PathItem) Walk(v Visitor) error { +// if v == nil { +// return nil +// } +// var err error +// v, err = v.Visit(pi) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } +// v, err = v.VisitPathItem(pi) +// if err != nil { +// return err +// } +// if v == nil { +// return nil +// } + +// if pi.Parameters != nil { +// if err = pi.Parameters.Walk(v); err != nil { +// return err +// } +// } + +// if pi.Servers != nil { +// if err = pi.Servers.Walk(v); err != nil { +// return err +// } +// } + +// var op OperationItem +// if pi.Get != nil { +// op = OperationItem{Operation: pi.Get, Method: MethodGet} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Put != nil { +// op = OperationItem{Operation: pi.Put, Method: MethodPut} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Post != nil { +// op = OperationItem{Operation: pi.Post, Method: MethodPost} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Delete != nil { +// op = OperationItem{Operation: pi.Delete, Method: MethodDelete} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Options != nil { +// op = OperationItem{Operation: pi.Options, Method: MethodOptions} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Head != nil { +// op = OperationItem{Operation: pi.Head, Method: MethodHead} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Patch != nil { +// op = OperationItem{Operation: pi.Patch, Method: MethodPatch} +// if err = op.Walk(v); err != nil { +// return err +// } +// } +// if pi.Trace != nil { +// if err = pi.Trace.Walk(v); err != nil { +// return err +// } +// } + +// return nil +// } diff --git a/path_test.go b/path_test.go index 74b5731..97f6e5e 100644 --- a/path_test.go +++ b/path_test.go @@ -1,120 +1,107 @@ package openapi_test -import ( - "encoding/json" - "errors" - "fmt" - "testing" +// func TestPath(t *testing.T) { +// assert := require.New(t) - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// j := []string{ +// `{ +// "/users/{id}": { +// "parameters": [ +// { +// "name": "id", +// "in": "path", +// "required": true, +// "description": "the user identifier, as userId", +// "schema": { +// "type": "string" +// } +// } +// ], +// "get": { +// "responses": { +// "200": { +// "description": "the user being returned", +// "content": { +// "application/json": { +// "schema": { +// "type": "object", +// "properties": { +// "uuid": { +// "type": "string", +// "format": "uuid" +// } +// } +// } +// } +// }, +// "links": { +// "address": { +// "operationId": "getUserAddress", +// "parameters": { +// "userId": "$request.path.id" +// } +// } +// } +// } +// } +// } +// }, +// "/users/{userid}/address": { +// "parameters": [ +// { +// "name": "userid", +// "in": "path", +// "required": true, +// "description": "the user identifier, as userId", +// "schema": { +// "type": "string" +// } +// } +// ], +// "get": { +// "operationId": "getUserAddress", +// "responses": { +// "200": { +// "description": "the user's address" +// } +// } +// } +// } +// }`} +// for _, d := range j { +// data := []byte(d) +// var paths openapi.Paths +// err := json.Unmarshal(data, &paths) +// var te *json.UnmarshalTypeError +// if errors.As(err, &te) { +// fmt.Println(te.Field) +// fmt.Println(te.Value) +// fmt.Println(te.Struct) +// } +// assert.NoError(err) -func TestPath(t *testing.T) { - assert := require.New(t) +// b, err := json.MarshalIndent(paths, "", " ") +// assert.NoError(err) +// // patch, err := jsonpatch.CreateMergePatch(j, d) +// assert.NoError(err) +// if !jsonpatch.Equal(data, b) { +// fmt.Println(string(b)) +// } +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - j := []string{ - `{ - "/users/{id}": { - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "description": "the user identifier, as userId", - "schema": { - "type": "string" - } - } - ], - "get": { - "responses": { - "200": { - "description": "the user being returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - } - } - } - } - }, - "links": { - "address": { - "operationId": "getUserAddress", - "parameters": { - "userId": "$request.path.id" - } - } - } - } - } - } - }, - "/users/{userid}/address": { - "parameters": [ - { - "name": "userid", - "in": "path", - "required": true, - "description": "the user identifier, as userId", - "schema": { - "type": "string" - } - } - ], - "get": { - "operationId": "getUserAddress", - "responses": { - "200": { - "description": "the user's address" - } - } - } - } - }`} - for _, d := range j { - data := []byte(d) - var paths openapi.Paths - err := json.Unmarshal(data, &paths) - var te *json.UnmarshalTypeError - if errors.As(err, &te) { - fmt.Println(te.Field) - fmt.Println(te.Value) - fmt.Println(te.Struct) - } - assert.NoError(err) +// // testing yaml - b, err := json.MarshalIndent(paths, "", " ") - assert.NoError(err) - // patch, err := jsonpatch.CreateMergePatch(j, d) - assert.NoError(err) - if !jsonpatch.Equal(data, b) { - fmt.Println(string(b)) - } - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Paths +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.Paths - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - - } -} +// } +// } diff --git a/paths.go b/paths.go new file mode 100644 index 0000000..a3918de --- /dev/null +++ b/paths.go @@ -0,0 +1,138 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +type PathItemEntry struct { + Key string + PathItem *PathItem +} + +type PathItems = ObjMap[*PathItem] + +// PathItemMap is a map of Paths that can either be a Path or a Reference +type PathItemMap = ComponentMap[*PathItem] + +// Paths holds the relative paths to the individual endpoints and their +// operations. The path is appended to the URL from the Server Object in order +// to construct the full URL. The Paths MAY be empty, due to Access Control List +// (ACL) constraints. +type Paths struct { + Extensions `json:"-"` + + // Items are the Path + PathItems `json:"-"` +} + +func (p *Paths) Nodes() []Node { + if p == nil { + return nil + } + return downcastNodes(p.nodes()) +} + +func (p *Paths) Anchors() (*Anchors, error) { + if p == nil { + return nil, nil + } + return p.PathItems.Anchors() +} + +func (p *Paths) nodes() []node { + if p == nil { + return nil + } + return appendEdges(nil, p.PathItems.nodes()...) +} + +func (*Paths) ref() Ref { return nil } +func (p *Paths) setLocation(loc Location) error { + if p == nil { + return nil + } + p.PathItems.setLocation(loc) + return nil +} + +// func (p *Paths) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return p.resolveNodeByPointer(ptr) +// } + +// func (p *Paths) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return p, nil +// } +// nxt, tok, _ := ptr.Next() +// v := p.Items.Get(Text(tok)) +// if v == nil { +// return nil, newErrNotFound(p.Location.AbsoluteLocation(), tok) +// } +// return v.resolveNodeByPointer(nxt) +// } + +func (p *Paths) isNil() bool { return p == nil } + +func (*Paths) Kind() Kind { return KindPaths } +func (*Paths) mapKind() Kind { return KindUndefined } +func (*Paths) sliceKind() Kind { return KindUndefined } + +// MarshalJSON marshals JSON +func (p Paths) MarshalJSON() ([]byte, error) { + j, err := p.PathItems.MarshalJSON() + if err != nil { + return nil, err + } + b := bytes.Buffer{} + // removing the last } as marshalExtensionsInto execpts a buffer without it + b.Write(j[:len(j)-1]) + return marshalExtensionsInto(&b, p.Extensions) +} + +// UnmarshalJSON unmarshals JSON data into p +func (p *Paths) UnmarshalJSON(data []byte) error { + *p = Paths{ + Extensions: Extensions{}, + } + var err error + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + if strings.HasPrefix(key.String(), "x-") { + p.SetRawExtension(Text(key.String()), []byte(value.Raw)) + } else { + var v PathItem + err = json.Unmarshal([]byte(value.Raw), &v) + p.Set(Text(key.String()), &v) + } + return err == nil + }) + return err +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (p Paths) MarshalYAML() (interface{}, error) { + j, err := p.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (p *Paths) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, p) +} + +var _ node = (*Paths)(nil) diff --git a/primitives.go b/primitives.go new file mode 100644 index 0000000..bce6c1c --- /dev/null +++ b/primitives.go @@ -0,0 +1,12 @@ +package openapi + +import ( + "github.com/chanced/caps/text" + "github.com/chanced/jsonx" +) + +type ( + Text = text.Text + Texts = text.Texts + Number = jsonx.Number +) diff --git a/ref.go b/ref.go new file mode 100644 index 0000000..16a805e --- /dev/null +++ b/ref.go @@ -0,0 +1,75 @@ +package openapi + +import "github.com/chanced/uri" + +const ( + RefTypeUndefined RefType = iota + RefTypeComponent + RefTypeSchema + RefTypeSchemaDynamicRef + RefTypeSchemaRecursiveRef + RefTypeOperationRef +) + +type RefType uint8 + +func (rk RefType) String() string { + switch rk { + case RefTypeComponent: + return "Reference" + case RefTypeSchema: + return "SchemaRef" + case RefTypeSchemaDynamicRef: + return "SchemaRef" + case RefTypeSchemaRecursiveRef: + return "SchemaRef" + case RefTypeOperationRef: + return "OperationRef" + default: + return "Undefined" + } +} + +type Ref interface { + Node + URI() *uri.URI + IsResolved() bool + ResolvedNode() Node + // ReferencedKind returns the Kind for the referenced node + RefKind() Kind + // RefType returns the RefType for the reference + RefType() RefType +} + +type ref interface { + Ref + resolve(v Node) error +} + +// IsRef returns true for the following types: +// - *Reference +// - *SchemaRef +// - *OperationRef +func IsRef(node Node) bool { + switch node.Kind() { + case KindReference, KindSchemaRef, KindOperationRef: + _, ok := node.(Ref) + if !ok { + panic("node is not a Ref. This is a bug. Please report it to github.com/chanced/openapi") + } + return true + default: + return false + } +} + +var ( + _ Ref = (*SchemaRef)(nil) + _ ref = (*SchemaRef)(nil) + + _ Ref = (*Reference[*Response])(nil) + _ ref = (*Reference[*Response])(nil) + + _ Ref = (*OperationRef)(nil) + _ ref = (*OperationRef)(nil) +) diff --git a/reference.go b/reference.go index 20278ed..1b28366 100644 --- a/reference.go +++ b/reference.go @@ -3,38 +3,17 @@ package openapi import ( "encoding/json" "errors" + "fmt" + "reflect" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/transcode" + "github.com/chanced/uri" "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" ) -type ParameterResolver func(ref string) (*ParameterObj, error) - -type ResponseResolver func(ref string) (*ResponseObj, error) - -type ExampleResolver func(ref string) (*ExampleObj, error) - -type HeaderResolver func(ref string) (*HeaderObj, error) - -type RequestBodyResolver func(ref string) (*RequestBodyObj, error) - -type CallbackResolver func(ref string) (*CallbackObj, error) - -type PathResolver func(ref string) (*PathObj, error) - -type SecuritySchemeResolver func(ref string) (*SecuritySchemeObj, error) - -type LinkResolver func(ref string) (*LinkObj, error) - -type SchemaResolver func(ref string) (*SchemaObj, error) - -// Referencable is any object type which could also be a Reference -type Referencable interface { - IsRef() bool -} - // ErrNotReference indicates not a reference -var ErrNotReference = errors.New("error: data is not a Reference") +var ErrNotReference = errors.New("openapi: data is not a Reference") // Reference is simple object to allow referencing other components in the // OpenAPI document, internally and externally. @@ -45,137 +24,173 @@ var ErrNotReference = errors.New("error: data is not a Reference") // // See the [rules for resolving Relative // References](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#relativeReferencesURI). -type Reference struct { +type Reference[T refable] struct { // The reference identifier. This MUST be in the form of a URI. // // *required* - Ref string `yaml:"$ref" json:"$ref"` + Ref *uri.URI `json:"$ref"` + // A short summary which by default SHOULD override that of the referenced // component. If the referenced object-type does not allow a summary field, // then this field has no effect. - Summary string `yaml:"summary,omitempty" json:"summary,omitempty"` + Summary Text `json:"summary,omitempty"` + // A description which by default SHOULD override that of the referenced // component. CommonMark syntax MAY be used for rich text representation. If // the referenced object-type does not allow a description field, then this // field has no effect. - Description string `yaml:"description" json:"description,omitempty"` -} + Description Text `json:"description,omitempty"` -// MarshalYAML marshals YAML -func (r Reference) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(r) -} + // Location of the Reference + Location `json:"-"` -// UnmarshalYAML unmarshals YAML -func (r *Reference) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, r) -} + ReferencedKind Kind `json:"-"` -// ParameterKind returns ParameterKindReference -func (r *Reference) ParameterKind() ParameterKind { - return ParameterKindReference -} + Resolved T `json:"-"` -// ResponseKind distinguishes Reference by returning HeaderKindRef -func (r *Reference) ResponseKind() ResponseKind { - return ResponseKindRef -} + dst interface{} -// ExampleKind distinguishes Reference by returning HeaderKindRef -func (r *Reference) ExampleKind() ExampleKind { - return ExampleKindRef + resolved bool } -// HeaderKind distinguishes Reference by returning HeaderKindRef -func (r *Reference) HeaderKind() HeaderKind { - return HeaderKindRef +func (r *Reference[T]) Nodes() []Node { + if r == nil { + return nil + } + return downcastNodes(r.nodes()) } -// RequestBodyKind returns RequestBodyKindRef -func (r *Reference) RequestBodyKind() RequestBodyKind { - return RequestBodyKindRef -} +func (r *Reference[T]) nodes() []node { + if r == nil { + return nil + } -// CallbackKind returns CallbackKindRef -func (r *Reference) CallbackKind() CallbackKind { - return CallbackKindRef + return appendEdges(nil, r.ResolvedNode().(node)) } +func (r *Reference[T]) RefKind() Kind { return r.ReferencedKind } -// PathKind returns PathKindRef -func (r *Reference) PathKind() PathKind { - return PathKindRef +func (r *Reference[T]) URI() *uri.URI { + if r == nil { + return nil + } + return r.Ref } -// SecuritySchemeKind returns SecuritySchemeKindRef -func (r *Reference) SecuritySchemeKind() SecuritySchemeKind { - return SecuritySchemeKindRef -} +func (r *Reference[T]) IsResolved() bool { return r.resolved } -// LinkKind returns LinkKindRef -func (r *Reference) LinkKind() LinkKind { - return LinkKindRef -} +// resolve resolves the reference +// +// TODO: make this a bit less panicky +func (r *Reference[T]) resolve(v Node) error { + if r == nil { + return fmt.Errorf("openapi: Reference is nil") + } + if r.dst == nil { + return fmt.Errorf("openapi: Reference dst is nil") + } + if v.Kind() != r.ReferencedKind { + return NewResolutionError(r, r.ReferencedKind, v.Kind()) + } -// ResolveParameter resolves r by invoking resolve -func (r *Reference) ResolveParameter(resolve ParameterResolver) (*ParameterObj, error) { - return resolve(r.Ref) -} + rd := reflect.ValueOf(r.dst) + rv := reflect.ValueOf(v) + if rv.Type().AssignableTo(rd.Type().Elem()) { + rd.Elem().Set(rv) + } else { + return fmt.Errorf("%s is not assignable to %s", rv.Type().String(), rd.Type().String()) + } -// ResolveResponse resolves r by invoking resolve -func (r *Reference) ResolveResponse(resolve ResponseResolver) (*ResponseObj, error) { - return resolve(r.Ref) + r.Resolved = v.(T) + + return nil } -// ResolveExample resolves r by invoking resolve -func (r *Reference) ResolveExample(resolve ExampleResolver) (*ExampleObj, error) { - return resolve(r.Ref) +// Referenced returns the resolved referenced Node +func (r *Reference[T]) ResolvedNode() Node { + return r.Resolved } -// ResolveHeader resolves r by invoking resolve -func (r *Reference) ResolveHeader(resolve HeaderResolver) (*HeaderObj, error) { - return resolve(r.Ref) +// Refs returns nil as instances of Reference do not contain the referenced +// object +func (*Reference[T]) Refs() []Ref { return nil } + +func (r *Reference[T]) Anchors() (*Anchors, error) { return nil, nil } + +func (*Reference[T]) RefType() RefType { return RefTypeComponent } + +// func (r *Reference[T]) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return r.resolveNodeByPointer(ptr) +// } + +// func (r *Reference[T]) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return r, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(r.Location.AbsoluteLocation(), tok) +// } + +func (r Reference[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(reference[T](r)) } -// ResolveRequestBody resolves r by invoking resolve -func (r *Reference) ResolveRequestBody(resolve RequestBodyResolver) (*RequestBodyObj, error) { - return resolve(r.Ref) +type reference[T refable] Reference[T] + +func (r *Reference[T]) UnmarshalJSON(data []byte) error { + var v reference[T] + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *r = Reference[T](v) + return nil } -// ResolveCallback resolves r by invoking resolve -func (r *Reference) ResolveCallback(resolve CallbackResolver) (*CallbackObj, error) { - return resolve(r.Ref) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (r Reference[T]) MarshalYAML() (interface{}, error) { + j, err := r.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// ResolvePath resolves r by invoking resolve -func (r *Reference) ResolvePath(resolve PathResolver) (*PathObj, error) { - return resolve(r.Ref) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (r *Reference[T]) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, r) } -// ResolveSecurityScheme resolves r by invoking resolve -func (r *Reference) ResolveSecurityScheme(resolve SecuritySchemeResolver) (*SecuritySchemeObj, error) { - return resolve(r.Ref) +func (r *Reference[T]) String() string { + return r.Ref.String() } -// ResolveLink resolves r by invoking resolve -func (r *Reference) ResolveLink(resolve LinkResolver) (*LinkObj, error) { - return resolve(r.Ref) +func (r *Reference[T]) setLocation(loc Location) error { + if r == nil { + return nil + } + r.Location = loc + return nil } + +func (r *Reference[T]) Kind() Kind { return KindReference } +func (*Reference[T]) mapKind() Kind { return KindUndefined } +func (*Reference[T]) sliceKind() Kind { return KindUndefined } + +func (r *Reference[T]) isNil() bool { return r == nil } + func isRefJSON(data []byte) bool { r := gjson.GetBytes(data, "$ref") return r.Str != "" } -func unmarshalReferenceJSON(data []byte) (*Reference, error) { - if !isRefJSON(data) { - return nil, ErrNotReference - } - var r Reference - return &r, json.Unmarshal(data, &r) -} - -var _ SecurityScheme = (*Reference)(nil) -var _ Path = (*Reference)(nil) -var _ Response = (*Reference)(nil) -var _ Example = (*Reference)(nil) -var _ Parameter = (*Reference)(nil) -var _ Header = (*Reference)(nil) +var ( + _ node = (*Reference[*Response])(nil) + _ Ref = (*Reference[*Response])(nil) + _ ref = (*Reference[*Response])(nil) +) diff --git a/reference_test.go b/reference_test.go index 60ae416..51a70eb 100644 --- a/reference_test.go +++ b/reference_test.go @@ -2,22 +2,19 @@ package openapi_test import ( "testing" - - "github.com/chanced/openapi" - "github.com/stretchr/testify/require" ) func TestReference(t *testing.T) { - assert := require.New(t) - r := "http://example.com/test.json" - var v openapi.Callback = &openapi.Reference{ - Ref: r, - } - ran := false - v.ResolveCallback(func(ref string) (*openapi.CallbackObj, error) { - ran = true - assert.Equal(r, ref) - return &openapi.CallbackObj{}, nil - }) - assert.True(ran) + // assert := require.New(t) + // r := "http://example.com/test.json" + // var v openapi.Callback = &openapi.Reference{ + // Ref: r, + // } + // ran := false + // v.ResolveNodeByPointerCallback(func(ref string) (*openapi.Callback, error) { + // ran = true + // assert.Equal(r, ref) + // return &openapi.Callback{}, nil + // }) + // assert.True(ran) } diff --git a/request_body.go b/request_body.go index 3279260..d8de6e4 100644 --- a/request_body.go +++ b/request_body.go @@ -3,108 +3,131 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// RequestBodyKind distinguishes a RequestBodyObj as either a RequestBody or -// Reference -type RequestBodyKind int +// RequestBodyMap is a map of RequestBody +type RequestBodyMap = ComponentMap[*RequestBody] -const ( - // RequestBodyKindObj = RequestBodyObj - RequestBodyKindObj RequestBodyKind = iota - // RequestBodyKindRef = Reference - RequestBodyKindRef -) +// RequestBody describes a single request body. +type RequestBody struct { + Location `json:"-"` + Extensions `json:"-"` -// RequestBodyObj describes a single request body. -type RequestBodyObj struct { // A brief description of the request body. This could contain examples of // use. CommonMark syntax MAY be used for rich text representation. - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` // The content of the request body. The key is a media type or media type range and the value describes it. For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text // // *required* - Content Content `json:"content,omitempty"` + Content *ContentMap `json:"content,omitempty"` // Determines if the request body is required in the request. Defaults to false. Required bool `json:"required,omitempty"` +} - Extensions `json:"-"` +func (rb *RequestBody) Nodes() []Node { + if rb == nil { + return nil + } + return downcastNodes(rb.nodes()) } -type requestbody RequestBodyObj +func (rb *RequestBody) nodes() []node { + if rb == nil { + return nil + } + return appendEdges(nil, rb.Content) +} -// RequestBodyKind returns RequestBodyKindRequestBody -func (rb *RequestBodyObj) RequestBodyKind() RequestBodyKind { return RequestBodyKindObj } +func (rb *RequestBody) Refs() []Ref { + if rb == nil { + return nil + } + return rb.Content.Refs() +} + +func (rb *RequestBody) isNil() bool { return rb == nil } -// ResolveRequestBody resolves RequestBodyObj by returning itself. resolve is not called. -func (rb *RequestBodyObj) ResolveRequestBody(RequestBodyResolver) (*RequestBodyObj, error) { - return rb, nil +func (rb *RequestBody) Anchors() (*Anchors, error) { + if rb == nil { + return nil, nil + } + return rb.Content.Anchors() } +// func (rb *RequestBody) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return rb.resolveNodeByPointer(ptr) +// } + +// func (rb *RequestBody) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return rb, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "content": +// if rb.Content == nil { +// return nil, newErrNotFound(rb.AbsoluteLocation(), tok) +// } +// return rb.Content.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(rb.Location.AbsoluteLocation(), tok) +// } +// } + +func (*RequestBody) Kind() Kind { return KindRequestBody } +func (*RequestBody) mapKind() Kind { return KindRequestBodyMap } +func (*RequestBody) sliceKind() Kind { return KindUndefined } + // MarshalJSON marshals h into JSON -func (rb RequestBodyObj) MarshalJSON() ([]byte, error) { +func (rb RequestBody) MarshalJSON() ([]byte, error) { + type requestbody RequestBody + return marshalExtendedJSON(requestbody(rb)) } // UnmarshalJSON unmarshals json into rb -func (rb *RequestBodyObj) UnmarshalJSON(data []byte) error { +func (rb *RequestBody) UnmarshalJSON(data []byte) error { + type requestbody RequestBody + var v requestbody err := unmarshalExtendedJSON(data, &v) - *rb = RequestBodyObj(v) + *rb = RequestBody(v) return err } -// UnmarshalYAML unmarshals YAML data into rb -func (rb *RequestBodyObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, rb) -} - -// MarshalYAML marshals rb into YAML -func (rb RequestBodyObj) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(rb) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (rb RequestBody) MarshalYAML() (interface{}, error) { + j, err := rb.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) } -// RequestBody can either be a RequestBody or a Reference -type RequestBody interface { - ResolveRequestBody(RequestBodyResolver) (*RequestBodyObj, error) - RequestBodyKind() RequestBodyKind -} - -func unmarshalRequestBody(data []byte, rb *RequestBody) error { - if isRefJSON(data) { - v, err := unmarshalReferenceJSON(data) - *rb = v +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (rb *RequestBody) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { return err } - var v RequestBodyObj - err := json.Unmarshal(data, &v) - *rb = &v - return err + return json.Unmarshal(j, rb) } -// RequestBodies is a map of RequestBody -type RequestBodies map[string]RequestBody - -// UnmarshalJSON unmarshals JSON -func (rb RequestBodies) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { - return err +func (rb *RequestBody) setLocation(loc Location) error { + if rb == nil { + return nil } - for k, d := range dm { - var v RequestBody - if err := unmarshalRequestBody(d, &v); err != nil { - return err - } - rb[k] = v + rb.Location = loc + if err := rb.Content.setLocation(loc.AppendLocation("content")); err != nil { + return err } - return nil } +func (*RequestBody) refable() {} + +var _ node = (*RequestBody)(nil) diff --git a/request_body_test.go b/request_body_test.go index 7290d52..c41cb7d 100644 --- a/request_body_test.go +++ b/request_body_test.go @@ -1,86 +1,86 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// yaml "sigs.k8s.io/yaml" +// ) -func TestRequestBody(t *testing.T) { - assert := require.New(t) - j := []string{ - `{ - "description": "user to add to the system", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - }, - "examples": { - "user" : { - "summary": "User Example", - "externalValue": "https://foo.bar/examples/user-example.json" - } - } - }, - "application/xml": { - "schema": { - "$ref": "#/components/schemas/User" - }, - "examples": { - "user" : { - "summary": "User example in XML", - "externalValue": "https://foo.bar/examples/user-example.xml" - } - } - }, - "text/plain": { - "examples": { - "user" : { - "summary": "User example in Plain text", - "externalValue": "https://foo.bar/examples/user-example.txt" - } - } - }, - "*/*": { - "examples": { - "user" : { - "summary": "User example in other format", - "externalValue": "https://foo.bar/examples/user-example.whatever" - } - } - } - } - }`, - } - for _, d := range j { - data := []byte(d) - var rb openapi.RequestBodyObj - err := json.Unmarshal(data, &rb) - assert.NoError(err) - b, err := json.Marshal(rb) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// func TestRequestBody(t *testing.T) { +// assert := require.New(t) +// j := []string{ +// `{ +// "description": "user to add to the system", +// "content": { +// "application/json": { +// "schema": { +// "$ref": "#/components/schemas/User" +// }, +// "examples": { +// "user" : { +// "summary": "User Example", +// "externalValue": "https://foo.bar/examples/user-example.json" +// } +// } +// }, +// "application/xml": { +// "schema": { +// "$ref": "#/components/schemas/User" +// }, +// "examples": { +// "user" : { +// "summary": "User example in XML", +// "externalValue": "https://foo.bar/examples/user-example.xml" +// } +// } +// }, +// "text/plain": { +// "examples": { +// "user" : { +// "summary": "User example in Plain text", +// "externalValue": "https://foo.bar/examples/user-example.txt" +// } +// } +// }, +// "*/*": { +// "examples": { +// "user" : { +// "summary": "User example in other format", +// "externalValue": "https://foo.bar/examples/user-example.whatever" +// } +// } +// } +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var rb openapi.RequestBody +// err := json.Unmarshal(data, &rb) +// assert.NoError(err) +// b, err := json.Marshal(rb) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.RequestBodyObj - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.RequestBody +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } diff --git a/response.go b/response.go index 0bdca9e..9e315f0 100644 --- a/response.go +++ b/response.go @@ -3,26 +3,11 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) -// ResponseKind is an indicator for either a ResponseObj or a Reference -type ResponseKind int - -const ( - // ResponseKindObj = Response - ResponseKindObj ResponseKind = iota - // ResponseKindRef = Reference - ResponseKindRef -) - -// Response is either a Response or a Reference -type Response interface { - ResolveResponse(ResponseResolver) (*ResponseObj, error) - ResponseKind() ResponseKind -} - -// Responses is a container for the expected responses of an operation. The +// ResponseMap is a container for the expected responses of an operation. The // container maps a HTTP response code to the expected response. // // The documentation is not necessarily expected to cover all possible HTTP @@ -31,119 +16,187 @@ type Response interface { // known errors. // // The default MAY be used as a default response object for all HTTP codes that -// are not covered individually by the Responses Object. +// are not covered individually by the ResponseMap Object. // -// The Responses Object MUST contain at least one response code, and if only one +// The ResponseMap Object MUST contain at least one response code, and if only one // response code is provided it SHOULD be the response for a successful // operation call. -type Responses map[string]Response - -// UnmarshalJSON unmarshals JSON data into r -func (r *Responses) UnmarshalJSON(data []byte) error { - var m map[string]json.RawMessage - err := json.Unmarshal(data, &m) - if err != nil { - return err - } - if *r == nil { - *r = make(Responses, len(m)) - } - rv := *r - for k, j := range m { - v, err := unmarshalResponse(j) - if err != nil { - return err - } - rv[k] = v - } - return nil -} - -// UnmarshalYAML unmarshals YAML data into r -func (r *Responses) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, r) -} +type ResponseMap = ComponentMap[*Response] -// MarshalYAML marshals r into YAML -func (r Responses) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(r) - if err != nil { - return nil, err - } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err -} - -// ResponseObj describes a single response from an API Operation, including +// Response describes a single response from an API Operation, including // design-time, static links to operations based on the response. -type ResponseObj struct { +type Response struct { // A description of the response. CommonMark syntax MAY be used for rich // text representation. // // *required* - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` // Maps a header name to its definition. RFC7230 states header names are // case insensitive. If a response header is defined with the name // "Content-Type", it SHALL be ignored. - Headers Headers `json:"headers,omitempty"` + Headers *HeaderMap `json:"headers,omitempty"` // A map containing descriptions of potential response payloads. The key is // a media type or media type range and the value describes it. For // responses that match multiple keys, only the most specific key is // applicable. e.g. text/plain overrides text/* - Content Content `json:"content,omitempty"` + Content *ContentMap `json:"content,omitempty"` // A map of operations links that can be followed from the response. The key // of the map is a short name for the link, following the naming constraints // of the names for Component Objects. - Links Links `json:"links,omitempty"` + Links *LinkMap `json:"links,omitempty"` Extensions `json:"-"` + + Location `json:"-"` } -type response ResponseObj +func (r *Response) Nodes() []Node { + if r == nil { + return nil + } + return downcastNodes(r.nodes()) +} -// ResponseKind returns ResponseKindResponse, indicates that this is a Response -func (r *ResponseObj) ResponseKind() ResponseKind { return ResponseKindObj } +func (r *Response) nodes() []node { + if r == nil { + return nil + } + return appendEdges(nil, r.Headers, r.Content, r.Links) +} -// ResolveResponse resolves ResponseObj by returning itself. resolve is not called. -func (r *ResponseObj) ResolveResponse(ResponseResolver) (*ResponseObj, error) { - return r, nil +func (r *Response) Refs() []Ref { + if r == nil { + return nil + } + var refs []Ref + refs = append(refs, r.Headers.Refs()...) + refs = append(refs, r.Content.Refs()...) + refs = append(refs, r.Links.Refs()...) + return refs +} + +func (r *Response) Anchors() (*Anchors, error) { + if r == nil { + return nil, nil + } + var anchors *Anchors + var err error + if anchors, err = anchors.merge(r.Headers.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(r.Content.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(r.Links.Anchors()); err != nil { + return nil, err + } + return anchors, nil } // MarshalJSON marshals r into JSON -func (r ResponseObj) MarshalJSON() ([]byte, error) { +func (r Response) MarshalJSON() ([]byte, error) { + type response Response return marshalExtendedJSON(response(r)) } // UnmarshalJSON unmarshals json into r -func (r *ResponseObj) UnmarshalJSON(data []byte) error { +func (r *Response) UnmarshalJSON(data []byte) error { + type response Response var v response err := unmarshalExtendedJSON(data, &v) - *r = ResponseObj(v) + *r = Response(v) return err } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML -func (r ResponseObj) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(r) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (r Response) MarshalYAML() (interface{}, error) { + j, err := r.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals yaml into s -func (r *ResponseObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, r) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (r *Response) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, r) } -func unmarshalResponse(data []byte) (Response, error) { - if isRefJSON(data) { - return unmarshalReferenceJSON(data) +func (*Response) Kind() Kind { return KindResponse } +func (*Response) mapKind() Kind { return KindResponseMap } +func (*Response) sliceKind() Kind { return KindUndefined } + +func (r *Response) isNil() bool { return r == nil } + +func (r *Response) setLocation(loc Location) error { + if r == nil { + return nil + } + r.Location = loc + if err := r.Headers.setLocation(loc.AppendLocation("headers")); err != nil { + return err } - var v ResponseObj - err := json.Unmarshal(data, &v) - return &v, err + if err := r.Content.setLocation(loc.AppendLocation("content")); err != nil { + return err + } + if err := r.Links.setLocation(loc.AppendLocation("links")); err != nil { + return err + } + return nil } + +func (*Response) refable() {} + +var _ node = (*Response)(nil) + +// ResolveNodeByPointer resolves a Node by a jsonpointer. It validates the pointer and then +// attempts to resolve the Node. +// +// # Errors +// +// - [ErrNotFound] indicates that the component was not found +// +// - [ErrNotResolvable] indicates that the pointer path can not resolve to a +// Node +// +// - [jsonpointer.ErrMalformedEncoding] indicates that the pointer encoding +// is malformed +// +// - [jsonpointer.ErrMalformedStart] indicates that the pointer is not empty +// and does not start with a slash +// func (r *Response) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// err := ptr.Validate() +// if err != nil { +// return nil, err +// } +// return r.resolveNodeByPointer(ptr) +// } + +// func (r *Response) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return r, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "headers": +// if r.Headers == nil { +// return nil, newErrNotFound(r.AbsoluteLocation(), tok) +// } +// return r.Headers.resolveNodeByPointer(nxt) +// case "content": +// if r.Content == nil { +// return nil, newErrNotFound(r.AbsoluteLocation(), tok) +// } +// return r.Content.resolveNodeByPointer(nxt) +// case "links": +// if r.Links == nil { +// return nil, newErrNotFound(r.AbsoluteLocation(), tok) +// } +// return r.Links.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(r.Location.AbsoluteLocation(), tok) +// } +// } diff --git a/response_test.go b/response_test.go index 6cbbd3c..61d8d99 100644 --- a/response_test.go +++ b/response_test.go @@ -1,57 +1,57 @@ package openapi_test -import ( - "encoding/json" - "fmt" - "testing" +// import ( +// "encoding/json" +// "fmt" +// "testing" - "github.com/chanced/cmpjson" - "github.com/chanced/openapi" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" -) +// "github.com/chanced/cmpjson" +// "github.com/chanced/openapi" +// jsonpatch "github.com/evanphx/json-patch/v5" +// "github.com/stretchr/testify/require" +// yaml "sigs.k8s.io/yaml" +// ) -func TestResponse(t *testing.T) { - assert := require.New(t) +// func TestResponse(t *testing.T) { +// assert := require.New(t) - j := []string{ - `{ - "description": "A complex object array response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VeryComplexType" - } - } - } - } - }`, - } - for _, d := range j { - data := []byte(d) - var v openapi.ResponseObj - err := json.Unmarshal(data, &v) - assert.NoError(err) - b, err := json.Marshal(v) - assert.NoError(err) - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) +// j := []string{ +// `{ +// "description": "A complex object array response", +// "content": { +// "application/json": { +// "schema": { +// "type": "array", +// "items": { +// "$ref": "#/components/schemas/VeryComplexType" +// } +// } +// } +// } +// }`, +// } +// for _, d := range j { +// data := []byte(d) +// var v openapi.Response +// err := json.Unmarshal(data, &v) +// assert.NoError(err) +// b, err := json.Marshal(v) +// assert.NoError(err) +// assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - // testing yaml +// // testing yaml - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.ResponseObj - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) +// y, err := yaml.JSONToYAML(data) +// assert.NoError(err) +// var yo openapi.Response +// err = yaml.Unmarshal(y, &yo) +// assert.NoError(err) +// yb, err := json.MarshalIndent(yo, "", " ") +// assert.NoError(err) +// if !jsonpatch.Equal(data, yb) { +// fmt.Println(string(data), "\n------------------------\n", string(yb)) +// } +// assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - } -} +// } +// } diff --git a/schema.go b/schema.go index 610eef2..0669ae5 100644 --- a/schema.go +++ b/schema.go @@ -1,103 +1,89 @@ package openapi import ( + "bytes" "encoding/json" "errors" + "reflect" "strings" - "github.com/chanced/dynamic" - "github.com/chanced/openapi/yamlutil" - "github.com/tidwall/sjson" - "gopkg.in/yaml.v2" + "github.com/chanced/caps/text" + "github.com/chanced/jsonx" + "github.com/chanced/maps" + "github.com/chanced/uri" ) -// SchemaKind indicates whether the *SchemaObj is a SchemaObj, Reference, or Boolean -type SchemaKind uint8 - -const ( - // SchemaKindObj = *SchemaObj - SchemaKindObj SchemaKind = iota - // SchemaKindBool = *Boolean - SchemaKindBool -) - -// Schemas is a map of Schemas -type Schemas map[string]*SchemaObj - -// UnmarshalJSON unmarshals JSON -func (s *Schemas) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { - return err - } - res := make(Schemas, len(dm)) - - for k, d := range dm { - v, err := unmarshalSchemaJSON(d) - if err != nil { - return err - } - res[k] = v - } - *s = res - return nil -} - -// SchemaObj allows the definition of input and output data types. These types can -// be objects, but also primitives and arrays. This object is a superset of the -// [JSON SchemaObj Specification Draft +// Schema allows the definition of input and output data types. These types can +// be objects, but also primitives and arrays. This object is a superSlice of the +// [JSON Schema Specification Draft // 2020-12](https://tools.ietf.org/html/draft-bhutton-json-schema-00). // -// For more information about the properties, see [JSON SchemaObj +// For more information about the properties, see [JSON Schema // Core](https://tools.ietf.org/html/draft-bhutton-json-schema-00) and [JSON -// SchemaObj +// Schema // Validation](https://tools.ietf.org/html/draft-bhutton-json-schema-validation-00). // -// Unless stated otherwise, the property definitions follow those of JSON SchemaObj -// and do not add any additional semantics. Where JSON SchemaObj indicates that +// Unless stated otherwise, the property definitions follow those of JSON Schema +// and do not add any additional semantics. Where JSON Schema indicates that // behavior is defined by the application (e.g. for annotations), OAS also // defers the definition of semantics to the application consuming the OpenAPI // document. // -// The OpenAPI SchemaObj Object +// The OpenAPI Schema Object // [dialect](https://tools.ietf.org/html/draft-bhutton-json-schema-00#section-4.3.3) // is defined as requiring the [OAS base vocabulary](#baseVocabulary), in -// addition to the vocabularies as specified in the JSON SchemaObj draft 2020-12 +// addition to the vocabularies as specified in the JSON Schema draft 2020-12 // [general purpose // meta-schema](https://tools.ietf.org/html/draft-bhutton-json-schema-00#section-8). // -// The OpenAPI SchemaObj Object dialect for this version of the specification is +// The OpenAPI Schema Object dialect for this version of the specification is // identified by the URI `https://spec.openapis.org/oas/3.1/dialect/base` (the // "OAS dialect schema id"). // -// The following properties are taken from the JSON SchemaObj specification but +// The following properties are taken from the JSON Schema specification but // their definitions have been extended by the OAS: // // - description - [CommonMark syntax](https://spec.commonmark.org/) MAY be used // for rich text representation. - format - See [Data Type -// Formats](#dataTypeFormat) for further details. While relying on JSON SchemaObj's +// Formats](#dataTypeFormat) for further details. While relying on JSON Schema's // defined formats, the OAS offers a few additional predefined formats. // -// In addition to the JSON SchemaObj properties comprising the OAS dialect, the -// SchemaObj Object supports keywords from any other vocabularies, or entirely +// In addition to the JSON Schema properties comprising the OAS dialect, the +// Schema Object supports keywords from any other vocabularies, or entirely // arbitrary properties. -// A SchemaObj represents compiled version of json-schema. -type SchemaObj struct { - // Always will be assigned if the schema value is a boolean - Always *bool `json:"-"` - Schema string `json:"$schema,omitempty"` +// A Schema represents compiled version of json-schema. +type Schema struct { + Extensions `json:"-"` + Location `json:"-"` + + Schema *uri.URI `json:"$schema,omitempty"` + // The value of $id is a URI-reference without a fragment that resolves // against the Retrieval URI. The resulting URI is the base URI for the // schema. // // https://json-schema.org/understanding-json-schema/structuring.html?highlight=id#id - ID string `json:"$id,omitempty"` + ID *uri.URI `json:"$id,omitempty"` + + // A less common way to identify a subschema is to create a named anchor in + // the schema using the $anchor keyword and using that name in the URI + // fragment. Anchors must start with a letter followed by any number of + // letters, digits, -, _, :, or .. + // + // https://json-schema.org/understanding-json-schema/structuring.html?highlight=anchor#anchor + Anchor Text `json:"$anchor,omitempty"` + + DynamicAnchor Text `json:"$dynamicAnchor,omitempty"` + + RecursiveAnchor *bool `json:"$recursiveAnchor,omitempty"` + // At its core, JSON *SchemaObj defines the following basic types: // // "string", "number", "integer", "object", "array", "boolean", "null" // // https://json-schema.org/understanding-json-schema/reference/type.html#type Type Types `json:"type,omitempty"` + // The "$ref" keyword is an applicator that is used to reference a // statically identified schema. Its results are the results of the // referenced schema. [CREF5] @@ -111,19 +97,18 @@ type SchemaObj struct { // https://json-schema.org/draft/2020-12/json-schema-core.html#ref // // https://json-schema.org/understanding-json-schema/structuring.html?highlight=ref#ref - Ref string `json:"$ref,omitempty"` - // The "$defs" keyword reserves a location for schema authors to inline - // re-usable JSON Schemas into a more general schema. The keyword does not - // directly affect the validation result. - // - // This keyword's value MUST be an object. Each member value of this object - // MUST be a valid JSON *SchemaObj. - // - // https://json-schema.org/draft/2020-12/json-schema-core.html#defs + Ref *SchemaRef `json:"$ref,omitempty"` + + // The "$dynamicRef" keyword is an applicator that allows for deferring the + // full resolution until runtime, at which point it is resolved each time it + // is encountered while evaluating an instance. // - // https://json-schema.org/understanding-json-schema/structuring.html?highlight=defs#defs - Definitions Schemas `json:"$defs,omitempty"` - // The format keyword allows for basic semantic identification of certain kinds of string values that are commonly used. For example, because JSON doesn’t have a “DateTime” type, dates need to be encoded as strings. format allows the schema author to indicate that the string value should be interpreted as a date. By default, format is just an annotation and does not effect validation. + // https://json-schema.org/draft/2020-12/json-schema-core.html#dynamic-ref + DynamicRef *SchemaRef `json:"$dynamicRef,omitempty"` + + RecursiveRef *SchemaRef `json:"$recursiveRef,omitempty"` + + // The format keyword allows for basic semantic identification of certain Kinds of string values that are commonly used. For example, because JSON doesn’t have a “DateTime” type, dates need to be encoded as strings. format allows the schema author to indicate that the string value should be interpreted as a date. By default, format is just an annotation and does not effect validation. // // Optionally, validator implementations can provide a configuration option to // enable format to function as an assertion rather than just an annotation. @@ -133,30 +118,23 @@ type SchemaObj struct { // Expressions can do. // // https://json-schema.org/understanding-json-schema/reference/string.html#format - Format string `json:"format,omitempty"` - DynamicAnchor string `json:"$dynamicAnchor,omitempty"` - // The "$dynamicRef" keyword is an applicator that allows for deferring the - // full resolution until runtime, at which point it is resolved each time it - // is encountered while evaluating an instance. - // - // https://json-schema.org/draft/2020-12/json-schema-core.html#dynamic-ref - DynamicRef string `json:"$dynamicRef,omitempty"` - // A less common way to identify a subschema is to create a named anchor in - // the schema using the $anchor keyword and using that name in the URI - // fragment. Anchors must start with a letter followed by any number of - // letters, digits, -, _, :, or .. - // - // https://json-schema.org/understanding-json-schema/structuring.html?highlight=anchor#anchor - Anchor string `json:"$anchor,omitempty"` + Format Text `json:"format,omitempty"` + // The const keyword is used to restrict a value to a single value. // // https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=const#constant-values - Const json.RawMessage `json:"const,omitempty"` + Const jsonx.RawMessage `json:"const,omitempty"` + + Required Texts `json:"required,omitempty"` + + Properties *SchemaMap `json:"properties,omitempty"` + // The enum keyword is used to restrict a value to a fixed set of values. It // must be an array with at least one element, where each element is unique. // // https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=const#enumerated-values - Enum []string `json:"enum,omitempty"` + Enum Texts `json:"enum,omitempty"` + // The $comment keyword is strictly intended for adding comments to a // schema. Its value must always be a string. Unlike the annotations title, // description, and examples, JSON schema implementations aren’t allowed to @@ -166,45 +144,57 @@ type SchemaObj struct { // of the schema. // // https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=const#comments - Comments string `json:"$comment,omitempty"` + Comments Text `json:"$comment,omitempty"` // The not keyword declares that an instance validates if it doesn’t // validate against the given subschema. // // https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=not#not - Not *SchemaObj `json:"not,omitempty"` + Not *Schema `json:"not,omitempty"` + // validate against allOf, the given data must be valid against all of the // given subschemas. // // https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=anyof#anyof - AllOf SchemaSet `json:"allOf,omitempty"` + AllOf *SchemaSlice `json:"allOf,omitempty"` + // validate against anyOf, the given data must be valid against any (one or // more) of the given subschemas. // // https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=allof#allof - AnyOf SchemaSet `json:"anyOf,omitempty"` + AnyOf *SchemaSlice `json:"anyOf,omitempty"` + // alidate against oneOf, the given data must be valid against exactly one of the given subschemas. // // https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=oneof#oneof - OneOf SchemaSet `json:"oneOf,omitempty"` + OneOf *SchemaSlice `json:"oneOf,omitempty"` + // if, then and else keywords allow the application of a subschema based on // the outcome of another schema, much like the if/then/else constructs // you’ve probably seen in traditional programming languages. // // https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else - If *SchemaObj `json:"if,omitempty"` + If *Schema `json:"if,omitempty"` + // https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else - Then *SchemaObj `json:"then,omitempty"` + Then *Schema `json:"then,omitempty"` + // https://json-schema.org/understanding-json-schema/reference/conditionals.html#if-then-else - Else *SchemaObj `json:"else,omitempty"` - MinProperties *int `json:"minProperties,omitempty"` - MaxProperties *int `json:"maxProperties,omitempty"` - Required []string `json:"required,omitempty"` - Properties Schemas `json:"properties,omitempty"` - PropertyNames *SchemaObj `json:"propertyNames,omitempty"` - RegexProperties *bool `json:"regexProperties,omitempty"` - PatternProperties Schemas `json:"patternProperties,omitempty"` - AdditionalProperties *SchemaObj `json:"additionalProperties,omitempty"` + + Else *Schema `json:"else,omitempty"` + + MinProperties *Number `json:"minProperties,omitempty"` + + MaxProperties *Number `json:"maxProperties,omitempty"` + + PropertyNames *Schema `json:"propertyNames,omitempty"` + + RegexProperties *bool `json:"regexProperties,omitempty"` + + PatternProperties *SchemaMap `json:"patternProperties,omitempty"` + + AdditionalProperties *Schema `json:"additionalProperties,omitempty"` + // The dependentRequired keyword conditionally requires that certain // properties must be present if a given property is present in an object. // For example, suppose we have a schema representing a customer. If you @@ -215,154 +205,432 @@ type SchemaObj struct { // dependentRequired keyword is an object. Each entry in the object maps // from the name of a property, p, to an array of strings listing properties // that are required if p is present. - DependentRequired map[string][]string `json:"dependentRequired,omitempty"` + DependentRequired *Map[Texts] `json:"dependentRequired,omitempty"` + // The dependentSchemas keyword conditionally applies a subschema when a // given property is present. This schema is applied in the same way allOf // applies schemas. Nothing is merged or extended. Both schemas apply // independently. - DependentSchemas Schemas `json:"dependentSchemas,omitempty"` - UnevaluatedProperties *SchemaObj `json:"unevaluatedProperties,omitempty"` - UniqueObjs *bool `json:"uniqueObjs,omitempty"` + + DependentSchemas *SchemaMap `json:"dependentSchemas,omitempty"` + + UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"` + + UniqueItems *bool `json:"uniqueItems,omitempty"` + // List validation is useful for arrays of arbitrary length where each item // matches the same schema. For this kind of array, set the items keyword to // a single schema that will be used to validate all of the items in the // array. - Items *SchemaObj `json:"items,omitempty"` - UnevaluatedObjs *SchemaObj `json:"unevaluatedObjs,omitempty"` - AdditionalObjs *SchemaObj `json:"additionalObjs,omitempty"` - PrefixObjs SchemaSet `json:"prefixObjs,omitempty"` - Contains *SchemaObj `json:"contains,omitempty"` - MinContains *Number `json:"minContains,omitempty"` - MaxContains *Number `json:"maxContains,omitempty"` - MinLength *Number `json:"minLength,omitempty"` - MaxLength *Number `json:"maxLength,omitempty"` - Pattern *Regexp `json:"pattern,omitempty"` - ContentEncoding string `json:"contentEncoding,omitempty"` - ContentMediaType string `json:"contentMediaType,omitempty"` - Minimum *Number `json:"minimum,omitempty"` - ExclusiveMinimum *Number `json:"exclusiveMinimum,omitempty"` - Maximum *Number `json:"maximum,omitempty"` - ExclusiveMaximum *Number `json:"exclusiveMaximum,omitempty"` - MultipleOf *Number `json:"multipleOf,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Default json.RawMessage `json:"default,omitempty"` - ReadOnly *bool `json:"readOnly,omitempty"` - WriteOnly *bool `json:"writeOnly,omitempty"` - Examples []json.RawMessage `json:"examples,omitempty"` - Example json.RawMessage `json:"example,omitempty"` - Deprecated *bool `json:"deprecated,omitempty"` - ExternalDocs string `json:"externalDocs,omitempty"` - // Deprecated: renamed to dynamicAnchor - RecursiveAnchor *bool `json:"$recursiveAnchor,omitempty"` - // Deprecated: renamed to dynamicRef - RecursiveRef string `json:"$recursiveRef,omitempty"` + // + // https://json-schema.org/understanding-json-schema/reference/array.html#items + Items *Schema `json:"items,omitempty"` + + UnevaluatedItems *Schema `json:"unevaluatedItems,omitempty"` + + AdditionalItems *Schema `json:"additionalItems,omitempty"` + + PrefixItems *SchemaSlice `json:"prefixItems,omitempty"` + + Contains *Schema `json:"contains,omitempty"` + + MinContains *Number `json:"minContains,omitempty"` + + MaxContains *Number `json:"maxContains,omitempty"` + + MinLength *Number `json:"minLength,omitempty"` + + MaxLength *Number `json:"maxLength,omitempty"` + + Pattern *Regexp `json:"pattern,omitempty"` + + ContentEncoding Text `json:"contentEncoding,omitempty"` + + ContentMediaType Text `json:"contentMediaType,omitempty"` + + Minimum *Number `json:"minimum,omitempty"` + + ExclusiveMinimum *Number `json:"exclusiveMinimum,omitempty"` + + Maximum *Number `json:"maximum,omitempty"` + + ExclusiveMaximum *Number `json:"exclusiveMaximum,omitempty"` + + MultipleOf *Number `json:"multipleOf,omitempty"` + + Title Text `json:"title,omitempty"` + + Description Text `json:"description,omitempty"` + Default jsonx.RawMessage `json:"default,omitempty"` + + ReadOnly *bool `json:"readOnly,omitempty"` + + WriteOnly *bool `json:"writeOnly,omitempty"` + + Examples []jsonx.RawMessage `json:"examples,omitempty"` + + Example jsonx.RawMessage `json:"example,omitempty"` + + Deprecated *bool `json:"deprecated,omitempty"` + + ExternalDocs Text `json:"externalDocs,omitempty"` + + // When request bodies or response payloads may be one of a number of + // different schemas, a discriminator object can be used to aid in + // serialization, deserialization, and validation. The discriminator is a + // specific object in a schema which is used to inform the consumer of the + // document of an alternative schema based on the value associated with it. + // + // This object MAY be extended with Specification Extensions. + // + // The discriminator object is legal only when using one of the composite + // keywords oneOf, anyOf, allOf. + // + // 3.1: + // + // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#discriminatorObject + // + // 3.0: + // + // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#discriminatorObject Discriminator *Discriminator `json:"discriminator,omitempty"` + // This MAY be used only on properties schemas. It has no effect on root // schemas. Adds additional metadata to describe the XML representation of // this property. - XML *XML `json:"xml,omitempty"` - Extensions `json:"-"` - Keywords map[string]json.RawMessage `json:"-"` + XML *XML `json:"xml,omitempty"` + + // The "$defs" keyword reserves a location for schema authors to inline + // re-usable JSON Schemas into a more general schema. The keyword does not + // directly affect the validation result. + // + // This keyword's value MUST be an object. Each member value of this object + // MUST be a valid JSON *SchemaObj. + // + // https://json-schema.org/draft/2020-12/json-schema-core.html#defs + // + // https://json-schema.org/understanding-json-schema/structuring.html?highlight=defs#defs + Definitions *SchemaMap `json:"$defs,omitempty"` + + Keywords map[Text]jsonx.RawMessage `json:"-"` } -type schema SchemaObj +func (s *Schema) Nodes() []Node { + if s == nil { + return nil + } + return downcastNodes(s.nodes()) +} -// Detail returns a ptr to the *SchemaObj -func (s SchemaObj) Detail() *SchemaObj { - return &s +func (s *Schema) nodes() []node { + return appendEdges(nil, s.Ref, + s.DynamicRef, + s.RecursiveRef, + s.Properties, + s.Not, + s.AllOf, + s.AnyOf, + s.OneOf, + s.If, + s.Then, + s.Else, + s.PropertyNames, + s.PatternProperties, + s.AdditionalProperties, + s.DependentSchemas, + s.UnevaluatedProperties, + s.Items, + s.UnevaluatedItems, + s.AdditionalItems, + s.PrefixItems, + s.Contains, + s.Discriminator, + s.XML, + s.Definitions, + ) } -// MarshalJSON marshals JSON -func (s SchemaObj) MarshalJSON() ([]byte, error) { - if s.Always != nil { - return json.Marshal(s.Always) +func (s *Schema) Refs() []Ref { + if s == nil { + return nil } - data, err := marshalExtendedJSON(schema(s)) - if s.Keywords != nil { - for k, v := range s.Keywords { - data, err = sjson.SetBytes(data, k, v) - if err != nil { - return data, err - } - } + var refs []Ref + if s.Ref != nil { + refs = append(refs, s.Ref) + } + if s.DynamicRef != nil { + refs = append(refs, s.DynamicRef) } - return data, err + if s.RecursiveRef != nil { + refs = append(refs, s.RecursiveRef) + } + refs = append(refs, s.Definitions.Refs()...) + refs = append(refs, s.Not.Refs()...) + refs = append(refs, s.AllOf.Refs()...) + refs = append(refs, s.AnyOf.Refs()...) + refs = append(refs, s.OneOf.Refs()...) + refs = append(refs, s.If.Refs()...) + refs = append(refs, s.Then.Refs()...) + refs = append(refs, s.Else.Refs()...) + refs = append(refs, s.Properties.Refs()...) + refs = append(refs, s.PropertyNames.Refs()...) + refs = append(refs, s.PatternProperties.Refs()...) + refs = append(refs, s.AdditionalProperties.Refs()...) + refs = append(refs, s.DependentSchemas.Refs()...) + refs = append(refs, s.UnevaluatedProperties.Refs()...) + refs = append(refs, s.Items.Refs()...) + refs = append(refs, s.UnevaluatedItems.Refs()...) + refs = append(refs, s.AdditionalItems.Refs()...) + refs = append(refs, s.PrefixItems.Refs()...) + refs = append(refs, s.Contains.Refs()...) + refs = append(refs, s.XML.Refs()...) + + return refs } -// SchemaKind returns SchemaKindObj -func (s *SchemaObj) SchemaKind() SchemaKind { return SchemaKindObj } +func (s *Schema) Anchors() (*Anchors, error) { + if s == nil { + return nil, nil + } + anchors := &Anchors{ + Standard: make(map[text.Text]Anchor), + Dynamic: make(map[text.Text]Anchor), + } + if s.Anchor != "" { + anchors.Standard[s.Anchor] = Anchor{ + Location: s.Location.AppendLocation("$anchor"), + In: s, + Name: s.Anchor, + Type: AnchorTypeRegular, + } + } + if s.DynamicAnchor != "" { + anchors.Dynamic[s.DynamicAnchor] = Anchor{ + Location: s.Location.AppendLocation("$dynamicAnchor"), + In: s, + Name: s.DynamicAnchor, + Type: AnchorTypeDynamic, + } + } + if s.RecursiveAnchor != nil { + anchors.Recursive = &Anchor{ + Location: s.Location.AppendLocation("$recursiveAnchor"), + In: s, + Name: "", + Type: AnchorTypeRecursive, + } + } + var err error -// ResolveSchema resolves *SchemaObj by returning s -func (s *SchemaObj) ResolveSchema(SchemaResolver) (*SchemaObj, error) { - return s, nil -} + if anchors, err = anchors.merge(s.Ref.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Definitions.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.DynamicRef.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Not.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.AllOf.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.AnyOf.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.OneOf.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.If.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Then.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Else.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Properties.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.PropertyNames.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.PatternProperties.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.AdditionalProperties.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.DependentSchemas.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.UnevaluatedProperties.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Items.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.UnevaluatedItems.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.AdditionalItems.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.PrefixItems.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.Contains.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.RecursiveRef.Anchors()); err != nil { + return nil, err + } + if anchors, err = anchors.merge(s.XML.Anchors()); err != nil { + return nil, err + } -// UnmarshalJSON unmarshals JSON -func (s *SchemaObj) UnmarshalJSON(data []byte) error { - sv, err := unmarshalSchemaJSON(data) - *s = *sv - return err + return anchors, nil } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML -func (s SchemaObj) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(s) +// MarshalJSON marshals JSON +func (s Schema) MarshalJSON() ([]byte, error) { + type schema Schema + b := bytes.Buffer{} + data, err := json.Marshal(schema(s)) if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err -} + // trimming the last } + b.Write(data[:len(data)-1]) -// UnmarshalYAML unmarshals yaml into s -func (s *SchemaObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, s) + if len(s.Keywords) == 0 && len(s.Extensions) == 0 && b.Len() < 10 { + bs := b.String() + switch bs { + case "{": + return []byte("true"), nil + case `{"not":true`: + return []byte("false"), nil + } + } + if s.Keywords != nil { + for _, kv := range maps.SortByKeys(s.Keywords) { + if b.Len() > 2 { + b.WriteString(",") + } + jsonx.EncodeAndWriteString(&b, kv.Key) + b.WriteByte(':') + if kv.Value != nil { + bb, err := json.Marshal(kv.Value) + if err != nil { + return nil, err + } + b.Write(bb) + } + } + } + b.WriteByte('}') + return b.Bytes(), err } -// IsStrings returns false -func (s *SchemaObj) IsStrings() bool { - return false +// UnmarshalJSON unmarshals JSON +func (s *Schema) UnmarshalJSON(data []byte) error { + t := jsonx.TypeOf(data) + switch t { + case jsonx.TypeBool: + return s.unmarshalJSONBool(data) + case jsonx.TypeObject: + return s.unmarshalJSONObj(data) + default: + return &json.UnmarshalTypeError{Value: t.String(), Type: reflect.TypeOf(s)} + } } -// IsBool returns false -func (s *SchemaObj) IsBool() bool { - return false +func (s *Schema) unmarshalJSONBool(data []byte) error { + if jsonx.IsTrue(data) { + *s = Schema{} + return nil + } else { + *s = Schema{Not: &Schema{}} + return nil + } } -// IsRef returns true if s.Ref is set -func (s *SchemaObj) IsRef() bool { - return s.Ref != "" +func (s *Schema) unmarshalJSONObj(data []byte) error { + res := Schema{} + + d := map[Text]jsonx.RawMessage{} + err := json.Unmarshal(data, &d) + if err != nil { + return err + } + fields := res.fields() + for k, v := range d { + if f, ok := fields[k.String()]; ok { + err = json.Unmarshal(v, f) + if err != nil { + return err + } + } else if strings.HasPrefix(k.String(), "x-") { + if res.Extensions == nil { + res.Extensions = Extensions{} + } + res.Extensions[k] = v + } else { + if res.Keywords == nil { + res.Keywords = make(map[Text]jsonx.RawMessage) + } + res.Keywords[k] = v + } + } + if err != nil { + return err + } + if res.Ref != nil { + res.Ref.SchemaRefKind = SchemaRefTypeRef + } + if res.DynamicRef != nil { + res.DynamicRef.SchemaRefKind = SchemaRefTypeDynamic + } + if res.RecursiveRef != nil { + res.RecursiveRef.SchemaRefKind = SchemaRefTypeRecursive + } + *s = res + + return nil } -// SetKeyword encodes and sets the keyword key to the encoded value -func (s *SchemaObj) SetKeyword(key string, value interface{}) error { +// SetKeyword marshals value and sets the encoded json to key in Keywords +// +// If setting the value as []byte, it should be in the form of json.RawMessage +// or jsonx.RawMessage as both types implement json.Marshaler +func (s *Schema) SetKeyword(key Text, value interface{}) error { b, err := json.Marshal(value) if err != nil { return err } - return s.SetEncodedKeyword(key, b) + return s.setEncodedKeyword(key, b) } // SetEncodedKeyword sets the keyword key to value -func (s *SchemaObj) SetEncodedKeyword(key string, value []byte) error { - if strings.HasPrefix(key, "x-") { +func (s *Schema) setEncodedKeyword(key Text, value []byte) error { + if key.HasPrefix("x-") { return errors.New("keyword keys may not start with \"x-\"") } - s.Keywords[key] = value + s.Keywords[Text(key)] = value return nil } // DecodeKeyword unmarshals the keyword's raw data into dst -func (s *SchemaObj) DecodeKeyword(key string, dst interface{}) error { +func (s *Schema) DecodeKeyword(key Text, dst interface{}) error { return json.Unmarshal(s.Keywords[key], dst) } // DecodeKeywords unmarshals all keywords raw data into dst -func (s *SchemaObj) DecodeKeywords(dst interface{}) error { +func (s *Schema) DecodeKeywords(dst interface{}) error { data, err := json.Marshal(s.Keywords) if err != nil { return err @@ -370,225 +638,513 @@ func (s *SchemaObj) DecodeKeywords(dst interface{}) error { return json.Unmarshal(data, dst) } -// SchemaSet is a slice of **SchemaObj -type SchemaSet []*SchemaObj +func (s *Schema) fields() map[string]interface{} { + return map[string]interface{}{ + "$schema": &s.Schema, + "$id": &s.ID, + "type": &s.Type, + "$ref": &s.Ref, + "$defs": &s.Definitions, + "format": &s.Format, + "$dynamicAnchor": &s.DynamicAnchor, + "$dynamicRef": &s.DynamicRef, + "$anchor": &s.Anchor, + "const": &s.Const, + "enum": &s.Enum, + "$comment": &s.Comments, + "not": &s.Not, + "allOf": &s.AllOf, + "anyOf": &s.AnyOf, + "oneOf": &s.OneOf, + "if": &s.If, + "then": &s.Then, + "else": &s.Else, + "minProperties": &s.MinProperties, + "maxProperties": &s.MaxProperties, + "required": &s.Required, + "properties": &s.Properties, + "propertyNames": &s.PropertyNames, + "regexProperties": &s.RegexProperties, + "patternProperties": &s.PatternProperties, + "additionalProperties": &s.AdditionalProperties, + "dependentRequired": &s.DependentRequired, + "dependentSchemas": &s.DependentSchemas, + "unevaluatedProperties": &s.UnevaluatedProperties, + "uniqueItems": &s.UniqueItems, + "items": &s.Items, + "unevaluatedItems": &s.UnevaluatedItems, + "additionalItems": &s.AdditionalItems, + "prefixItems": &s.PrefixItems, + "contains": &s.Contains, + "minContains": &s.MinContains, + "maxContains": &s.MaxContains, + "minLength": &s.MinLength, + "maxLength": &s.MaxLength, + "pattern": &s.Pattern, + "contentEncoding": &s.ContentEncoding, + "contentMediaType": &s.ContentMediaType, + "minimum": &s.Minimum, + "exclusiveMinimum": &s.ExclusiveMinimum, + "maximum": &s.Maximum, + "exclusiveMaximum": &s.ExclusiveMaximum, + "multipleOf": &s.MultipleOf, + "title": &s.Title, + "description": &s.Description, + "default": &s.Default, + "readOnly": &s.ReadOnly, + "writeOnly": &s.WriteOnly, + "examples": &s.Examples, + "example": &s.Example, + "deprecated": &s.Deprecated, + "externalDocs": &s.ExternalDocs, + "$recursiveAnchor": &s.RecursiveAnchor, + "$recursiveRef": &s.RecursiveRef, + "discriminator": &s.Discriminator, + "xml": &s.XML, + } +} + +func (*Schema) Kind() Kind { return KindSchema } +func (*Schema) mapKind() Kind { return KindSchemaMap } +func (*Schema) sliceKind() Kind { return KindSchemaSlice } -// UnmarshalJSON unmarshals JSON -func (s *SchemaSet) UnmarshalJSON(data []byte) error { - var j []dynamic.JSON - if err := json.Unmarshal(data, &j); err != nil { +func (s *Schema) setLocation(loc Location) error { + if s == nil { + return nil + } + s.Location = loc + + if err := s.Ref.setLocation(loc.AppendLocation("$ref")); err != nil { return err } - res := make(SchemaSet, len(j)) - for i, d := range j { - v, err := unmarshalSchemaJSON(d) - if err != nil { - return err - } - res[i] = v + if err := s.Definitions.setLocation(loc.AppendLocation("$defs")); err != nil { + return err + } + if err := s.DynamicRef.setLocation(loc.AppendLocation("$dynamicRef")); err != nil { + return err + } + if err := s.Not.setLocation(loc.AppendLocation("not")); err != nil { + return err + } + if err := s.AllOf.setLocation(loc.AppendLocation("allOf")); err != nil { + return err + } + if err := s.AnyOf.setLocation(loc.AppendLocation("anyOf")); err != nil { + return err + } + if err := s.OneOf.setLocation(loc.AppendLocation("oneOf")); err != nil { + return err + } + if err := s.If.setLocation(loc.AppendLocation("if")); err != nil { + return err + } + if err := s.Then.setLocation(loc.AppendLocation("then")); err != nil { + return err + } + if err := s.Else.setLocation(loc.AppendLocation("else")); err != nil { + return err + } + if err := s.Properties.setLocation(loc.AppendLocation("properties")); err != nil { + return err + } + if err := s.PropertyNames.setLocation(loc.AppendLocation("propertyNames")); err != nil { + return err + } + if err := s.PatternProperties.setLocation(loc.AppendLocation("patternProperties")); err != nil { + return err + } + if err := s.AdditionalProperties.setLocation(loc.AppendLocation("additionalProperties")); err != nil { + return err + } + if err := s.DependentSchemas.setLocation(loc.AppendLocation("dependentSchemas")); err != nil { + return err + } + + if err := s.UnevaluatedProperties.setLocation(loc.AppendLocation("unevaluatedProperties")); err != nil { + return err + } + if err := s.Items.setLocation(loc.AppendLocation("items")); err != nil { + return err + } + if err := s.UnevaluatedItems.setLocation(loc.AppendLocation("unevaluatedItems")); err != nil { + return err + } + if err := s.AdditionalItems.setLocation(loc.AppendLocation("additionalItems")); err != nil { + return err + } + if err := s.PrefixItems.setLocation(loc.AppendLocation("prefixItems")); err != nil { + return err + } + if err := s.Contains.setLocation(loc.AppendLocation("contains")); err != nil { + return err + } + if err := s.RecursiveRef.setLocation(loc.AppendLocation("$recursiveRef")); err != nil { + return err + } + if err := s.Discriminator.setLocation(loc.AppendLocation("discriminator")); err != nil { + return err + } + if err := s.XML.setLocation(loc.AppendLocation("xml")); err != nil { + return err } - *s = res return nil } -func unmarshalSchemaJSON(data []byte) (*SchemaObj, error) { - var str string - l := len(data) - if l >= 4 && l <= 5 { - str = string(data) - } - switch { - case str == "true": - t := true - return &SchemaObj{Always: &t}, nil - case str == "false": - f := false - return &SchemaObj{Always: &f}, nil - default: - return unmarshalSchemaObjJSON(data) +// Clone returns a deep copy of Schema. This is to avoid overriding the initial +// Schema when dealing with $dynamicRef and $recursiveRef. +func (s *Schema) Clone() *Schema { + if s == nil { + return nil + } + var recAnc *bool + if s.RecursiveAnchor != nil { + *recAnc = *s.RecursiveAnchor + } + var cnst jsonx.RawMessage + if s.Const != nil { + cnst = make(jsonx.RawMessage, len(s.Const)) + copy(cnst, s.Const) } -} -func unmarshalSchemaObjJSON(data []byte) (*SchemaObj, error) { - var err error - exts := Extensions{} - kw := make(map[string]json.RawMessage) - var dst partialschema - if err = json.Unmarshal(data, &dst); err != nil { - return nil, err + var required text.Texts + if s.Required != nil { + required = make(text.Texts, len(s.Required)) + copy(required, s.Required) } - var jm map[string]json.RawMessage - if err = json.Unmarshal(data, &jm); err != nil { - return nil, err + var example jsonx.RawMessage + if s.Example != nil { + example = make(jsonx.RawMessage, len(s.Example)) + copy(example, s.Example) + } + var examples []jsonx.RawMessage + if s.Examples != nil { + examples = make([]jsonx.RawMessage, len(s.Examples)) + copy(examples, s.Examples) + } + var enum text.Texts + if s.Enum != nil { + enum = make(text.Texts, len(s.Enum)) + copy(enum, s.Enum) + } + var minprops *jsonx.Number + if s.MinProperties != nil { + v := *s.MinProperties + minprops = &v + } + var maxprops *jsonx.Number + if s.MaxProperties != nil { + v := *s.MaxProperties + maxprops = &v + } + var regexpProps *bool + if s.RegexProperties != nil { + v := *s.RegexProperties + regexpProps = &v + } + var depReq *Map[Texts] + if s.DependentRequired != nil { + i := make([]KeyValue[Texts], len(s.DependentRequired.Items)) + copy(i, s.DependentRequired.Items) + depReq = &Map[Texts]{Items: i} + } + var uniqItems *bool + if s.UniqueItems != nil { + v := *s.UniqueItems + uniqItems = &v + } + var minContains *jsonx.Number + if s.MinContains != nil { + v := *s.MinContains + minContains = &v + } + var maxContains *jsonx.Number + if s.MaxContains != nil { + v := *s.MaxContains + maxContains = &v + } + var minLen *jsonx.Number + if s.MinLength != nil { + v := *s.MinLength + minLen = &v + } + var maxLen *jsonx.Number + if s.MaxLength != nil { + v := *s.MaxLength + maxLen = &v + } + var min *jsonx.Number + if s.Minimum != nil { + v := *s.Minimum + min = &v + } + var max *jsonx.Number + if s.Maximum != nil { + v := *s.Maximum + max = &v } - for key, d := range jm { - if strings.HasPrefix(key, "x-") { - exts[key] = d - } else if set, isSchema := schemaFieldSetters[key]; isSchema { - var v *SchemaObj - v, err = unmarshalSchemaJSON(d) - if err != nil { - return nil, err - } - set(&dst, v) - } else if _, isfield := jsfields[key]; !isfield { - kw[key] = d + var exclMin *jsonx.Number + if s.ExclusiveMinimum != nil { + v := *s.ExclusiveMinimum + exclMin = &v + } + var exclMax *jsonx.Number + if s.ExclusiveMaximum != nil { + v := *s.ExclusiveMaximum + exclMax = &v + } + var multipleOf *jsonx.Number + if s.MultipleOf != nil { + v := *s.MultipleOf + multipleOf = &v + } + var readonly *bool + if s.ReadOnly != nil { + v := *s.ReadOnly + readonly = &v + } + var writeOnly *bool + if s.WriteOnly != nil { + v := *s.WriteOnly + writeOnly = &v + } + var deprecated *bool + if s.Deprecated != nil { + v := *s.Deprecated + deprecated = &v + } + var k map[Text]jsonx.RawMessage + if s.Keywords != nil { + k = make(map[Text]jsonx.RawMessage, len(s.Keywords)) + for key, value := range s.Keywords { + k[key] = value } } - res := SchemaObj(dst) - res.Keywords = kw - res.Extensions = exts - return &res, err + var id *uri.URI + if s.ID != nil { + id = s.ID.Clone() + } + var pattern *Regexp + if s.Pattern != nil { + pattern = &Regexp{s.Pattern.Copy()} + } + cloned := &Schema{ + RecursiveAnchor: recAnc, + Const: cnst, + Required: required, + Enum: enum, + Example: example, + Examples: examples, + MinProperties: minprops, + MaxProperties: maxprops, + RegexProperties: regexpProps, + DependentRequired: depReq, + UniqueItems: uniqItems, + MinContains: minContains, + MaxContains: maxContains, + MinLength: minLen, + MaxLength: maxLen, + Minimum: min, + Maximum: max, + ExclusiveMinimum: exclMin, + ExclusiveMaximum: exclMax, + MultipleOf: multipleOf, + ReadOnly: readonly, + WriteOnly: writeOnly, + Deprecated: deprecated, + Keywords: k, + Schema: s.Schema, + ID: id, + Title: s.Title, + Description: s.Description, + Default: s.Default, + ExternalDocs: s.ExternalDocs, + Format: s.Format, + ContentMediaType: s.ContentMediaType, + Discriminator: s.Discriminator.Clone(), + XML: s.XML.Clone(), + Definitions: s.Definitions.Clone(), + Anchor: s.Anchor, + DynamicAnchor: s.DynamicAnchor, + Ref: s.Ref.Clone(), + Type: s.Type.Clone(), + DynamicRef: s.DynamicRef.Clone(), + Not: s.Not.Clone(), + AllOf: s.AllOf.Clone(), + AnyOf: s.AnyOf.Clone(), + RecursiveRef: s.RecursiveRef.Clone(), + OneOf: s.OneOf.Clone(), + Properties: s.Properties.Clone(), + Comments: s.Comments, + PropertyNames: s.PropertyNames.Clone(), + PatternProperties: s.PatternProperties.Clone(), + If: s.If.Clone(), + Then: s.Then.Clone(), + Else: s.Else.Clone(), + AdditionalProperties: s.AdditionalProperties.Clone(), + DependentSchemas: s.DependentSchemas.Clone(), + UnevaluatedProperties: s.UnevaluatedProperties.Clone(), + Items: s.Items.Clone(), + UnevaluatedItems: s.UnevaluatedItems.Clone(), + AdditionalItems: s.AdditionalItems.Clone(), + PrefixItems: s.PrefixItems.Clone(), + Contains: s.Contains.Clone(), + Pattern: pattern, + ContentEncoding: s.ContentEncoding, + Extensions: cloneExtensions(s.Extensions), + Location: s.Location, + } + return cloned } -var schemaFieldSetters = map[string]func(s *partialschema, v *SchemaObj){ - "not": func(s *partialschema, v *SchemaObj) { s.Not = v }, - "if": func(s *partialschema, v *SchemaObj) { s.If = v }, - "then": func(s *partialschema, v *SchemaObj) { s.Then = v }, - "else": func(s *partialschema, v *SchemaObj) { s.Else = v }, - "propertyNames": func(s *partialschema, v *SchemaObj) { s.PropertyNames = v }, - "additionalProperties": func(s *partialschema, v *SchemaObj) { s.AdditionalProperties = v }, - "unevaluatedProperties": func(s *partialschema, v *SchemaObj) { s.UnevaluatedProperties = v }, - "items": func(s *partialschema, v *SchemaObj) { s.Items = v }, - "contains": func(s *partialschema, v *SchemaObj) { s.Contains = v }, - "unevaluatedObjs": func(s *partialschema, v *SchemaObj) { s.UnevaluatedObjs = v }, - "additionalObjs": func(s *partialschema, v *SchemaObj) { s.AdditionalObjs = v }, +func cloneExtensions(e Extensions) Extensions { + if e == nil { + return nil + } + a := make(Extensions, len(e)) + for k, v := range e { + a[k] = v + } + return a } -var jsfields = map[string]struct{}{ - "$schema": {}, - "$id": {}, - "type": {}, - "$ref": {}, - "$defs": {}, - "format": {}, - "$dynamicAnchor": {}, - "$dynamicRef": {}, - "$anchor": {}, - "const": {}, - "enum": {}, - "$comment": {}, - "not": {}, - "allOf": {}, - "anyOf": {}, - "oneOf": {}, - "if": {}, - "then": {}, - "else": {}, - "minProperties": {}, - "maxProperties": {}, - "required": {}, - "properties": {}, - "propertyNames": {}, - "regexProperties": {}, - "patternProperties": {}, - "additionalProperties": {}, - "dependentRequired": {}, - "dependentSchemas": {}, - "unevaluatedProperties": {}, - "uniqueObjs": {}, - "items": {}, - "unevaluatedObjs": {}, - "additionalObjs": {}, - "prefixObjs": {}, - "contains": {}, - "minContains": {}, - "maxContains": {}, - "minLength": {}, - "maxLength": {}, - "pattern": {}, - "contentEncoding": {}, - "contentMediaType": {}, - "minimum": {}, - "exclusiveMinimum": {}, - "maximum": {}, - "exclusiveMaximum": {}, - "multipleOf": {}, - "title": {}, - "description": {}, - "default": {}, - "readOnly": {}, - "writeOnly": {}, - "examples": {}, - "deprecated": {}, - "externalDocs": {}, - "$recursiveAnchor": {}, - "$recursiveRef": {}, - "discriminator": {}, - "xml": {}, -} +func (s *Schema) isNil() bool { return s == nil } -type partialschema struct { - Always *bool `json:"-"` - Schema string `json:"$schema,omitempty"` - ID string `json:"$id,omitempty"` - Type Types `json:"type,omitempty"` - Ref string `json:"$ref,omitempty"` - Definitions Schemas `json:"$defs,omitempty"` - Format string `json:"format,omitempty"` - DynamicAnchor string `json:"$dynamicAnchor,omitempty"` - DynamicRef string `json:"$dynamicRef,omitempty"` - Anchor string `json:"$anchor,omitempty"` - Const json.RawMessage `json:"const,omitempty"` - Enum []string `json:"enum,omitempty"` - Comments string `json:"$comment,omitempty"` - Not *SchemaObj `json:"-"` - AllOf SchemaSet `json:"allOf,omitempty"` - AnyOf SchemaSet `json:"anyOf,omitempty"` - OneOf SchemaSet `json:"oneOf,omitempty"` - If *SchemaObj `json:"-"` - Then *SchemaObj `json:"-"` - Else *SchemaObj `json:"-"` - MinProperties *int `json:"minProperties,omitempty"` - MaxProperties *int `json:"maxProperties,omitempty"` - Required []string `json:"required,omitempty"` - Properties Schemas `json:"properties,omitempty"` - PropertyNames *SchemaObj `json:"-"` - RegexProperties *bool `json:"regexProperties,omitempty"` - PatternProperties Schemas `json:"patternProperties,omitempty"` - AdditionalProperties *SchemaObj `json:"-"` - DependentRequired map[string][]string `json:"dependentRequired,omitempty"` - DependentSchemas Schemas `json:"dependentSchemas,omitempty"` - UnevaluatedProperties *SchemaObj `json:"-"` - UniqueObjs *bool `json:"uniqueObjs,omitempty"` - Items *SchemaObj `json:"-"` - UnevaluatedObjs *SchemaObj `json:"-"` - AdditionalObjs *SchemaObj `json:"-"` - PrefixObjs SchemaSet `json:"prefixObjs,omitempty"` - Contains *SchemaObj `json:"-"` - MinContains *Number `json:"minContains,omitempty"` - MaxContains *Number `json:"maxContains,omitempty"` - MinLength *Number `json:"minLength,omitempty"` - MaxLength *Number `json:"maxLength,omitempty"` - Pattern *Regexp `json:"pattern,omitempty"` - ContentEncoding string `json:"contentEncoding,omitempty"` - ContentMediaType string `json:"contentMediaType,omitempty"` - Minimum *Number `json:"minimum,omitempty"` - ExclusiveMinimum *Number `json:"exclusiveMinimum,omitempty"` - Maximum *Number `json:"maximum,omitempty"` - ExclusiveMaximum *Number `json:"exclusiveMaximum,omitempty"` - MultipleOf *Number `json:"multipleOf,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Default json.RawMessage `json:"default,omitempty"` - ReadOnly *bool `json:"readOnly,omitempty"` - WriteOnly *bool `json:"writeOnly,omitempty"` - Examples []json.RawMessage `json:"examples,omitempty"` - Example json.RawMessage `json:"example,omitempty"` - Deprecated *bool `json:"deprecated,omitempty"` - ExternalDocs string `json:"externalDocs,omitempty"` - RecursiveAnchor *bool `json:"$recursiveAnchor,omitempty"` - RecursiveRef string `json:"$recursiveRef,omitempty"` - Discriminator *Discriminator `json:"discriminator,omitempty"` - XML *XML `json:"xml,omitempty"` - Extensions `json:"-"` - Keywords map[string]json.RawMessage `json:"-"` -} +var _ node = (*Schema)(nil) -var ( - _ json.Marshaler = (*SchemaObj)(nil) - _ json.Unmarshaler = (*SchemaObj)(nil) - _ yaml.Unmarshaler = (*SchemaObj)(nil) - _ yaml.Marshaler = (*SchemaObj)(nil) -) +// func (s *Schema) ResolveByAnchor(anchor Text) (*Schema, error) { +// } + +// func (s *Schema) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return s.resolveNodeByPointer(ptr) +// } + +// func (s *Schema) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return s, nil +// } +// nxt, tok, _ := ptr.Next() + +// switch tok { +// case "ref": +// if s.Ref == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Ref.resolveNodeByPointer(nxt) +// case "definitions": +// if s.Definitions == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Definitions.resolveNodeByPointer(nxt) +// case "dynamicRef": +// if s.DynamicRef == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.DynamicRef.resolveNodeByPointer(nxt) +// case "not": +// if s.Not == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Not.resolveNodeByPointer(nxt) +// case "allOf": +// if s.AllOf == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.AllOf.resolveNodeByPointer(nxt) +// case "anyOf": +// if s.AnyOf == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.AnyOf.resolveNodeByPointer(nxt) +// case "oneOf": +// if s.OneOf == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.OneOf.resolveNodeByPointer(nxt) +// case "if": +// if s.If == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.If.resolveNodeByPointer(nxt) +// case "then": +// if s.Then == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Then.resolveNodeByPointer(nxt) +// case "else": +// if s.Else == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Else.resolveNodeByPointer(nxt) +// case "properties": +// if s.Properties == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Properties.resolveNodeByPointer(nxt) +// case "propertyNames": +// if s.PropertyNames == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.PropertyNames.resolveNodeByPointer(nxt) +// case "patternProperties": +// if s.PatternProperties == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.PatternProperties.resolveNodeByPointer(nxt) +// case "additionalProperties": +// if s.AdditionalProperties == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.AdditionalProperties.resolveNodeByPointer(nxt) +// case "dependentSchemas": +// if s.DependentSchemas == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.DependentSchemas.resolveNodeByPointer(nxt) +// case "unevaluatedProperties": +// if s.UnevaluatedProperties == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.UnevaluatedProperties.resolveNodeByPointer(nxt) +// case "items": +// if s.Items == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Items.resolveNodeByPointer(nxt) +// case "unevaluatedItems": +// if s.UnevaluatedItems == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.UnevaluatedItems.resolveNodeByPointer(nxt) +// case "additionalItems": +// if s.AdditionalItems == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.AdditionalItems.resolveNodeByPointer(nxt) +// case "prefixItems": +// if s.PrefixItems == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.PrefixItems.resolveNodeByPointer(nxt) +// case "contains": +// if s.Contains == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Contains.resolveNodeByPointer(nxt) +// case "recursiveRef": +// if s.RecursiveRef == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.RecursiveRef.resolveNodeByPointer(nxt) +// case "xml": +// if s.XML == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.XML.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(s.Location.AbsoluteLocation(), tok) +// } +// } diff --git a/schema/3.1.0/dialect/base.schema.json b/schema/3.1.0/dialect/base.schema.json deleted file mode 100644 index b553ef4..0000000 --- a/schema/3.1.0/dialect/base.schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$id": "https://spec.openapis.org/oas/3.1/dialect/base", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$vocabulary": { - "https://json-schema.org/draft/2020-12/vocab/core": true, - "https://json-schema.org/draft/2020-12/vocab/applicator": true, - "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, - "https://json-schema.org/draft/2020-12/vocab/validation": true, - "https://json-schema.org/draft/2020-12/vocab/meta-data": true, - "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, - "https://json-schema.org/draft/2020-12/vocab/content": true, - "https://spec.openapis.org/oas/3.1/vocab/base": false - }, - "$dynamicAnchor": "meta", - - "title": "OpenAPI 3.1 Schema Object Dialect", - "allOf": [ - { "$ref": "https://json-schema.org/draft/2020-12/schema" }, - { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } - ] -} diff --git a/schema/3.1.0/meta/base.schema.json b/schema/3.1.0/meta/base.schema.json deleted file mode 100644 index b6c02bd..0000000 --- a/schema/3.1.0/meta/base.schema.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "$id": "https://spec.openapis.org/oas/3.1/meta/base", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$vocabulary": { - "https://spec.openapis.org/oas/3.1/vocab/base": true - }, - "$dynamicAnchor": "meta", - "title": "OAS Base vocabulary", - - "type": ["object", "boolean"], - "properties": { - "example": true, - "discriminator": { "$ref": "#/$defs/discriminator" }, - "externalDocs": { "$ref": "#/$defs/external-docs" }, - "xml": { "$ref": "#/$defs/xml" } - }, - "$defs": { - "extensible": { - "patternProperties": { - "^x-": true - } - }, - "discriminator": { - "$ref": "#/$defs/extensible", - "type": "object", - "properties": { - "propertyName": { - "type": "string" - }, - "mapping": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["propertyName"], - "unevaluatedProperties": false - }, - "external-docs": { - "$ref": "#/$defs/extensible", - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri-reference" - }, - "description": { - "type": "string" - } - }, - "required": ["url"], - "unevaluatedProperties": false - }, - "xml": { - "$ref": "#/$defs/extensible", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string", - "format": "uri" - }, - "prefix": { - "type": "string" - }, - "attribute": { - "type": "boolean" - }, - "wrapped": { - "type": "boolean" - } - }, - "unevaluatedProperties": false - } - } -} diff --git a/schema/3.1.0/schema-base.json b/schema/3.1.0/schema-base.json deleted file mode 100644 index 624b343..0000000 --- a/schema/3.1.0/schema-base.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$id": "https://spec.openapis.org/oas/3.1/schema-base/2021-09-28", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://spec.openapis.org/oas/3.1/schema/2021-09-28", - "properties": { - "jsonSchemaDialect": { - "$ref": "#/$defs/dialect" - } - }, - "$defs": { - "dialect": { - "const": "https://spec.openapis.org/oas/3.1/dialect/base" - }, - "schema": { - "$dynamicAnchor": "meta", - "$ref": "https://spec.openapis.org/oas/3.1/dialect/base", - "properties": { - "$schema": { - "$ref": "#/$defs/dialect" - } - } - } - } -} diff --git a/schema/3.1.0/schema.json b/schema/3.1.0/schema.json deleted file mode 100644 index eb18a49..0000000 --- a/schema/3.1.0/schema.json +++ /dev/null @@ -1,1244 +0,0 @@ -{ - "$id": "https://spec.openapis.org/oas/3.1/schema/2021-09-28", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "openapi": { - "type": "string", - "pattern": "^3\\.1\\.\\d+(-.+)?$" - }, - "info": { - "$ref": "#/$defs/info" - }, - "jsonSchemaDialect": { - "type": "string", - "format": "uri", - "default": "https://spec.openapis.org/oas/3.1/dialect/base" - }, - "servers": { - "type": "array", - "items": { - "$ref": "#/$defs/server" - } - }, - "paths": { - "$ref": "#/$defs/paths" - }, - "webhooks": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/path-item-or-reference" - } - }, - "components": { - "$ref": "#/$defs/components" - }, - "security": { - "type": "array", - "items": { - "$ref": "#/$defs/security-requirement" - } - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/$defs/tag" - } - }, - "externalDocs": { - "$ref": "#/$defs/external-documentation" - } - }, - "required": ["openapi", "info"], - "anyOf": [ - { - "required": ["paths"] - }, - { - "required": ["components"] - }, - { - "required": ["webhooks"] - } - ], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false, - "$defs": { - "info": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "termsOfService": { - "type": "string" - }, - "contact": { - "$ref": "#/$defs/contact" - }, - "license": { - "$ref": "#/$defs/license" - }, - "version": { - "type": "string" - } - }, - "required": ["title", "version"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "contact": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "url": { - "type": "string" - }, - "email": { - "type": "string" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "license": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "identifier": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "required": ["name"], - "oneOf": [ - { - "required": ["identifier"] - }, - { - "required": ["url"] - } - ], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "server": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri-reference" - }, - "description": { - "type": "string" - }, - "variables": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/server-variable" - } - } - }, - "required": ["url"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "server-variable": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", - "type": "object", - "properties": { - "enum": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "default": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["default"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "components": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", - "type": "object", - "properties": { - "schemas": { - "type": "object", - "additionalProperties": { - "$dynamicRef": "#meta" - } - }, - "responses": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/response-or-reference" - } - }, - "parameters": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/parameter-or-reference" - } - }, - "examples": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/example-or-reference" - } - }, - "requestBodies": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/request-body-or-reference" - } - }, - "headers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/header-or-reference" - } - }, - "securitySchemes": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/security-scheme-or-reference" - } - }, - "links": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/link-or-reference" - } - }, - "callbacks": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/callbacks-or-reference" - } - }, - "pathItems": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/path-item-or-reference" - } - } - }, - "patternProperties": { - "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { - "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", - "propertyNames": { - "pattern": "^[a-zA-Z0-9._-]+$" - } - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "paths": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", - "type": "object", - "patternProperties": { - "^/": { - "$ref": "#/$defs/path-item" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "path-item": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", - "type": "object", - "properties": { - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "servers": { - "type": "array", - "items": { - "$ref": "#/$defs/server" - } - }, - "parameters": { - "type": "array", - "items": { - "$ref": "#/$defs/parameter-or-reference" - } - } - }, - "patternProperties": { - "^(get|put|post|delete|options|head|patch|trace)$": { - "$ref": "#/$defs/operation" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "path-item-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/path-item" - } - }, - "operation": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/$defs/external-documentation" - }, - "operationId": { - "type": "string" - }, - "parameters": { - "type": "array", - "items": { - "$ref": "#/$defs/parameter-or-reference" - } - }, - "requestBody": { - "$ref": "#/$defs/request-body-or-reference" - }, - "responses": { - "$ref": "#/$defs/responses" - }, - "callbacks": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/callbacks-or-reference" - } - }, - "deprecated": { - "default": false, - "type": "boolean" - }, - "security": { - "type": "array", - "items": { - "$ref": "#/$defs/security-requirement" - } - }, - "servers": { - "type": "array", - "items": { - "$ref": "#/$defs/server" - } - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "external-documentation": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "required": ["url"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "parameter": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "in": { - "enum": ["query", "header", "path", "cookie"] - }, - "description": { - "type": "string" - }, - "required": { - "default": false, - "type": "boolean" - }, - "deprecated": { - "default": false, - "type": "boolean" - }, - "allowEmptyValue": { - "default": false, - "type": "boolean" - }, - "schema": { - "$dynamicRef": "#meta" - }, - "content": { - "$ref": "#/$defs/content", - "minProperties": 1, - "maxProperties": 1 - } - }, - "required": ["name", "in"], - "oneOf": [ - { - "required": ["schema"] - }, - { - "required": ["content"] - } - ], - "dependentSchemas": { - "schema": { - "properties": { - "style": { - "type": "string" - }, - "explode": { - "type": "boolean" - }, - "allowReserved": { - "default": false, - "type": "boolean" - } - }, - "allOf": [ - { - "$ref": "#/$defs/examples" - }, - { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" - }, - { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" - }, - { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" - }, - { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" - }, - { - "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" - } - ], - "$defs": { - "styles-for-path": { - "if": { - "properties": { - "in": { - "const": "path" - } - }, - "required": ["in"] - }, - "then": { - "properties": { - "name": { - "pattern": "[^/#?]+$" - }, - "style": { - "default": "simple", - "enum": ["matrix", "label", "simple"] - }, - "required": { - "const": true - } - }, - "required": ["required"] - } - }, - "styles-for-header": { - "if": { - "properties": { - "in": { - "const": "header" - } - }, - "required": ["in"] - }, - "then": { - "properties": { - "style": { - "default": "simple", - "const": "simple" - } - } - } - }, - "styles-for-query": { - "if": { - "properties": { - "in": { - "const": "query" - } - }, - "required": ["in"] - }, - "then": { - "properties": { - "style": { - "default": "form", - "enum": [ - "form", - "spaceDelimited", - "pipeDelimited", - "deepObject" - ] - } - } - } - }, - "styles-for-cookie": { - "if": { - "properties": { - "in": { - "const": "cookie" - } - }, - "required": ["in"] - }, - "then": { - "properties": { - "style": { - "default": "form", - "const": "form" - } - } - } - }, - "styles-for-form": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } - } - } - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "parameter-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/parameter" - } - }, - "request-body": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "content": { - "$ref": "#/$defs/content" - }, - "required": { - "default": false, - "type": "boolean" - } - }, - "required": ["content"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "request-body-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/request-body" - } - }, - "content": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/media-type" - }, - "propertyNames": { - "format": "media-range" - } - }, - "media-type": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", - "type": "object", - "properties": { - "schema": { - "$dynamicRef": "#meta" - }, - "encoding": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/encoding" - } - } - }, - "allOf": [ - { - "$ref": "#/$defs/specification-extensions" - }, - { - "$ref": "#/$defs/examples" - } - ], - "unevaluatedProperties": false - }, - "encoding": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", - "type": "object", - "properties": { - "contentType": { - "type": "string", - "format": "media-range" - }, - "headers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/header-or-reference" - } - }, - "style": { - "default": "form", - "enum": ["form", "spaceDelimited", "pipeDelimited", "deepObject"] - }, - "explode": { - "type": "boolean" - }, - "allowReserved": { - "default": false, - "type": "boolean" - } - }, - "allOf": [ - { - "$ref": "#/$defs/specification-extensions" - }, - { - "$ref": "#/$defs/encoding/$defs/explode-default" - } - ], - "unevaluatedProperties": false, - "$defs": { - "explode-default": { - "if": { - "properties": { - "style": { - "const": "form" - } - }, - "required": ["style"] - }, - "then": { - "properties": { - "explode": { - "default": true - } - } - }, - "else": { - "properties": { - "explode": { - "default": false - } - } - } - } - } - }, - "responses": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", - "type": "object", - "properties": { - "default": { - "$ref": "#/$defs/response-or-reference" - } - }, - "patternProperties": { - "^[1-5](?:[0-9]{2}|XX)$": { - "$ref": "#/$defs/response-or-reference" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "response": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "headers": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/header-or-reference" - } - }, - "content": { - "$ref": "#/$defs/content" - }, - "links": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/link-or-reference" - } - } - }, - "required": ["description"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "response-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/response" - } - }, - "callbacks": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", - "type": "object", - "$ref": "#/$defs/specification-extensions", - "additionalProperties": { - "$ref": "#/$defs/path-item-or-reference" - } - }, - "callbacks-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/callbacks" - } - }, - "example": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", - "type": "object", - "properties": { - "summary": { - "type": "string" - }, - "description": { - "type": "string" - }, - "value": true, - "externalValue": { - "type": "string", - "format": "uri" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "example-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/example" - } - }, - "link": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", - "type": "object", - "properties": { - "operationRef": { - "type": "string", - "format": "uri-reference" - }, - "operationId": true, - "parameters": { - "$ref": "#/$defs/map-of-strings" - }, - "requestBody": true, - "description": { - "type": "string" - }, - "body": { - "$ref": "#/$defs/server" - } - }, - "oneOf": [ - { - "required": ["operationRef"] - }, - { - "required": ["operationId"] - } - ], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "link-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/link" - } - }, - "header": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "required": { - "default": false, - "type": "boolean" - }, - "deprecated": { - "default": false, - "type": "boolean" - }, - "schema": { - "$dynamicRef": "#meta" - }, - "content": { - "$ref": "#/$defs/content", - "minProperties": 1, - "maxProperties": 1 - } - }, - "oneOf": [ - { - "required": ["schema"] - }, - { - "required": ["content"] - } - ], - "dependentSchemas": { - "schema": { - "properties": { - "style": { - "default": "simple", - "const": "simple" - }, - "explode": { - "default": false, - "type": "boolean" - } - }, - "$ref": "#/$defs/examples" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "header-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/header" - } - }, - "tag": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "externalDocs": { - "$ref": "#/$defs/external-documentation" - } - }, - "required": ["name"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "reference": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", - "type": "object", - "properties": { - "$ref": { - "type": "string", - "format": "uri-reference" - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "unevaluatedProperties": false - }, - "schema": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", - "$dynamicAnchor": "meta", - "type": ["object", "boolean"] - }, - "security-scheme": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", - "type": "object", - "properties": { - "type": { - "enum": ["apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"] - }, - "description": { - "type": "string" - } - }, - "required": ["type"], - "allOf": [ - { - "$ref": "#/$defs/specification-extensions" - }, - { - "$ref": "#/$defs/security-scheme/$defs/type-apikey" - }, - { - "$ref": "#/$defs/security-scheme/$defs/type-http" - }, - { - "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" - }, - { - "$ref": "#/$defs/security-scheme/$defs/type-oauth2" - }, - { - "$ref": "#/$defs/security-scheme/$defs/type-oidc" - } - ], - "unevaluatedProperties": false, - "$defs": { - "type-apikey": { - "if": { - "properties": { - "type": { - "const": "apiKey" - } - }, - "required": ["type"] - }, - "then": { - "properties": { - "name": { - "type": "string" - }, - "in": { - "enum": ["query", "header", "cookie"] - } - }, - "required": ["name", "in"] - } - }, - "type-http": { - "if": { - "properties": { - "type": { - "const": "http" - } - }, - "required": ["type"] - }, - "then": { - "properties": { - "scheme": { - "type": "string" - } - }, - "required": ["scheme"] - } - }, - "type-http-bearer": { - "if": { - "properties": { - "type": { - "const": "http" - }, - "scheme": { - "type": "string", - "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" - } - }, - "required": ["type", "scheme"] - }, - "then": { - "properties": { - "bearerFormat": { - "type": "string" - } - } - } - }, - "type-oauth2": { - "if": { - "properties": { - "type": { - "const": "oauth2" - } - }, - "required": ["type"] - }, - "then": { - "properties": { - "flows": { - "$ref": "#/$defs/oauth-flows" - } - }, - "required": ["flows"] - } - }, - "type-oidc": { - "if": { - "properties": { - "type": { - "const": "openIdConnect" - } - }, - "required": ["type"] - }, - "then": { - "properties": { - "openIdConnectUrl": { - "type": "string", - "format": "uri" - } - }, - "required": ["openIdConnectUrl"] - } - } - } - }, - "security-scheme-or-reference": { - "if": { - "type": "object", - "required": ["$ref"] - }, - "then": { - "$ref": "#/$defs/reference" - }, - "else": { - "$ref": "#/$defs/security-scheme" - } - }, - "oauth-flows": { - "type": "object", - "properties": { - "implicit": { - "$ref": "#/$defs/oauth-flows/$defs/implicit" - }, - "password": { - "$ref": "#/$defs/oauth-flows/$defs/password" - }, - "clientCredentials": { - "$ref": "#/$defs/oauth-flows/$defs/client-credentials" - }, - "authorizationCode": { - "$ref": "#/$defs/oauth-flows/$defs/authorization-code" - } - }, - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false, - "$defs": { - "implicit": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string" - }, - "refreshUrl": { - "type": "string" - }, - "scopes": { - "$ref": "#/$defs/map-of-strings" - } - }, - "required": ["authorizationUrl", "scopes"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "password": { - "type": "object", - "properties": { - "tokenUrl": { - "type": "string" - }, - "refreshUrl": { - "type": "string" - }, - "scopes": { - "$ref": "#/$defs/map-of-strings" - } - }, - "required": ["tokenUrl", "scopes"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "client-credentials": { - "type": "object", - "properties": { - "tokenUrl": { - "type": "string" - }, - "refreshUrl": { - "type": "string" - }, - "scopes": { - "$ref": "#/$defs/map-of-strings" - } - }, - "required": ["tokenUrl", "scopes"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - }, - "authorization-code": { - "type": "object", - "properties": { - "authorizationUrl": { - "type": "string" - }, - "tokenUrl": { - "type": "string" - }, - "refreshUrl": { - "type": "string" - }, - "scopes": { - "$ref": "#/$defs/map-of-strings" - } - }, - "required": ["authorizationUrl", "tokenUrl", "scopes"], - "$ref": "#/$defs/specification-extensions", - "unevaluatedProperties": false - } - } - }, - "security-requirement": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "specification-extensions": { - "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", - "patternProperties": { - "^x-": true - } - }, - "examples": { - "properties": { - "example": true, - "examples": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/example-or-reference" - } - } - } - }, - "map-of-strings": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } -} diff --git a/schema/jsonschema/2019-09/meta/applicator.json b/schema/jsonschema/2019-09/meta/applicator.json new file mode 100644 index 0000000..24a1cc4 --- /dev/null +++ b/schema/jsonschema/2019-09/meta/applicator.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/applicator": true + }, + "$recursiveAnchor": true, + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "additionalItems": { "$recursiveRef": "#" }, + "unevaluatedItems": { "$recursiveRef": "#" }, + "items": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "#/$defs/schemaArray" } + ] + }, + "contains": { "$recursiveRef": "#" }, + "additionalProperties": { "$recursiveRef": "#" }, + "unevaluatedProperties": { "$recursiveRef": "#" }, + "properties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$recursiveRef": "#" + } + }, + "propertyNames": { "$recursiveRef": "#" }, + "if": { "$recursiveRef": "#" }, + "then": { "$recursiveRef": "#" }, + "else": { "$recursiveRef": "#" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$recursiveRef": "#" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$recursiveRef": "#" } + } + } +} diff --git a/schema/jsonschema/2019-09/meta/content.json b/schema/jsonschema/2019-09/meta/content.json new file mode 100644 index 0000000..f6752a8 --- /dev/null +++ b/schema/jsonschema/2019-09/meta/content.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentSchema": { "$recursiveRef": "#" } + } +} diff --git a/schema/jsonschema/2019-09/meta/core.json b/schema/jsonschema/2019-09/meta/core.json new file mode 100644 index 0000000..eb708a5 --- /dev/null +++ b/schema/jsonschema/2019-09/meta/core.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true + }, + "$recursiveAnchor": true, + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveRef": { + "type": "string", + "format": "uri-reference" + }, + "$recursiveAnchor": { + "type": "boolean", + "default": false + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + } + } +} diff --git a/schema/jsonschema/2019-09/meta/format.json b/schema/jsonschema/2019-09/meta/format.json new file mode 100644 index 0000000..09bbfdd --- /dev/null +++ b/schema/jsonschema/2019-09/meta/format.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/format", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/format": true + }, + "$recursiveAnchor": true, + + "title": "Format vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/schema/jsonschema/2019-09/meta/hyper-schema.json b/schema/jsonschema/2019-09/meta/hyper-schema.json new file mode 100644 index 0000000..3d23058 --- /dev/null +++ b/schema/jsonschema/2019-09/meta/hyper-schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/hyper-schema", + "$id": "https://json-schema.org/draft/2019-09/meta/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$recursiveAnchor": true, + + "title": "JSON Hyper-Schema Vocabulary Schema", + "type": ["object", "boolean"], + "properties": { + "base": { + "type": "string", + "format": "uri-template" + }, + "links": { + "type": "array", + "items": { + "$ref": "https://json-schema.org/draft/2019-09/links" + } + } + }, + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] +} diff --git a/schema/jsonschema/2019-09/meta/meta-data.json b/schema/jsonschema/2019-09/meta/meta-data.json new file mode 100644 index 0000000..da04cff --- /dev/null +++ b/schema/jsonschema/2019-09/meta/meta-data.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/meta-data": true + }, + "$recursiveAnchor": true, + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/schema/jsonschema/2019-09/meta/validation.json b/schema/jsonschema/2019-09/meta/validation.json new file mode 100644 index 0000000..9f59677 --- /dev/null +++ b/schema/jsonschema/2019-09/meta/validation.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/validation": true + }, + "$recursiveAnchor": true, + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/schema/jsonschema/2019-09/schema.json b/schema/jsonschema/2019-09/schema.json new file mode 100644 index 0000000..2248a0c --- /dev/null +++ b/schema/jsonschema/2019-09/schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/core": true, + "https://json-schema.org/draft/2019-09/vocab/applicator": true, + "https://json-schema.org/draft/2019-09/vocab/validation": true, + "https://json-schema.org/draft/2019-09/vocab/meta-data": true, + "https://json-schema.org/draft/2019-09/vocab/format": false, + "https://json-schema.org/draft/2019-09/vocab/content": true + }, + "$recursiveAnchor": true, + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": { "$recursiveRef": "#" }, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$recursiveRef": "#" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + } + } + } +} diff --git a/schema/jsonschema/2020-12/hyper-schema.json b/schema/jsonschema/2020-12/hyper-schema.json new file mode 100644 index 0000000..eb4f0b3 --- /dev/null +++ b/schema/jsonschema/2020-12/hyper-schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/hyper-schema", + "$id": "https://json-schema.org/draft/2020-12/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$dynamicAnchor": "meta", + + "title": "JSON Hyper-Schema", + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/hyper-schema" } + ], + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] +} diff --git a/schema/jsonschema/2020-12/links.json b/schema/jsonschema/2020-12/links.json new file mode 100644 index 0000000..2b1bd0e --- /dev/null +++ b/schema/jsonschema/2020-12/links.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/links", + "title": "Link Description Object", + + "type": "object", + "properties": { + "anchor": { + "type": "string", + "format": "uri-template" + }, + "anchorPointer": { + "type": "string", + "anyOf": [ + { "format": "json-pointer" }, + { "format": "relative-json-pointer" } + ] + }, + "rel": { + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + } + ] + }, + "href": { + "type": "string", + "format": "uri-template" + }, + "hrefSchema": { + "$dynamicRef": "https://json-schema.org/draft/2020-12/hyper-schema#meta", + "default": false + }, + "templatePointers": { + "type": "object", + "additionalProperties": { + "type": "string", + "anyOf": [ + { "format": "json-pointer" }, + { "format": "relative-json-pointer" } + ] + } + }, + "templateRequired": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "targetSchema": { + "$dynamicRef": "https://json-schema.org/draft/2020-12/hyper-schema#meta", + "default": true + }, + "targetMediaType": { + "type": "string" + }, + "targetHints": {}, + "headerSchema": { + "$dynamicRef": "https://json-schema.org/draft/2020-12/hyper-schema#meta", + "default": true + }, + "submissionMediaType": { + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "$dynamicRef": "https://json-schema.org/draft/2020-12/hyper-schema#meta", + "default": true + }, + "$comment": { + "type": "string" + } + }, + "required": ["rel", "href"] +} diff --git a/schema/jsonschema/2020-12/meta/applicator.json b/schema/jsonschema/2020-12/meta/applicator.json new file mode 100644 index 0000000..ca69923 --- /dev/null +++ b/schema/jsonschema/2020-12/meta/applicator.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/applicator", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/applicator": true + }, + "$dynamicAnchor": "meta", + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "#meta" }, + "contains": { "$dynamicRef": "#meta" }, + "additionalProperties": { "$dynamicRef": "#meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "#meta" }, + "if": { "$dynamicRef": "#meta" }, + "then": { "$dynamicRef": "#meta" }, + "else": { "$dynamicRef": "#meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "#meta" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "#meta" } + } + } +} diff --git a/schema/jsonschema/2020-12/meta/content.json b/schema/jsonschema/2020-12/meta/content.json new file mode 100644 index 0000000..2f6e056 --- /dev/null +++ b/schema/jsonschema/2020-12/meta/content.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/content", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "#meta" } + } +} diff --git a/schema/jsonschema/2020-12/meta/core.json b/schema/jsonschema/2020-12/meta/core.json new file mode 100644 index 0000000..dfc092d --- /dev/null +++ b/schema/jsonschema/2020-12/meta/core.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/core", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true + }, + "$dynamicAnchor": "meta", + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { "$ref": "#/$defs/uriString" }, + "$ref": { "$ref": "#/$defs/uriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$vocabulary": { + "type": "object", + "propertyNames": { "$ref": "#/$defs/uriString" }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" } + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "uriString": { + "type": "string", + "format": "uri" + }, + "uriReferenceString": { + "type": "string", + "format": "uri-reference" + } + } +} diff --git a/schema/jsonschema/2020-12/meta/format-annotation.json b/schema/jsonschema/2020-12/meta/format-annotation.json new file mode 100644 index 0000000..51ef7ea --- /dev/null +++ b/schema/jsonschema/2020-12/meta/format-annotation.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true + }, + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for annotation results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/schema/jsonschema/2020-12/meta/format-assertion.json b/schema/jsonschema/2020-12/meta/format-assertion.json new file mode 100644 index 0000000..5e73fd7 --- /dev/null +++ b/schema/jsonschema/2020-12/meta/format-assertion.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/format-assertion": true + }, + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for assertion results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} diff --git a/schema/jsonschema/2020-12/meta/hyper-schema.json b/schema/jsonschema/2020-12/meta/hyper-schema.json new file mode 100644 index 0000000..62a3136 --- /dev/null +++ b/schema/jsonschema/2020-12/meta/hyper-schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/hyper-schema", + "$id": "https://json-schema.org/draft/2020-12/meta/hyper-schema", + "$vocabulary": { + "https://json-schema.org/draft/2019-09/vocab/hyper-schema": true + }, + "$dynamicAnchor": "meta", + + "title": "JSON Hyper-Schema Vocabulary Schema", + "type": ["object", "boolean"], + "properties": { + "base": { + "type": "string", + "format": "uri-template" + }, + "links": { + "type": "array", + "items": { + "$ref": "https://json-schema.org/draft/2020-12/links" + } + } + }, + "links": [ + { + "rel": "self", + "href": "{+%24id}" + } + ] +} diff --git a/schema/jsonschema/2020-12/meta/meta-data.json b/schema/jsonschema/2020-12/meta/meta-data.json new file mode 100644 index 0000000..05cbc22 --- /dev/null +++ b/schema/jsonschema/2020-12/meta/meta-data.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/meta-data": true + }, + "$dynamicAnchor": "meta", + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} diff --git a/schema/jsonschema/2020-12/meta/unevaluated.json b/schema/jsonschema/2020-12/meta/unevaluated.json new file mode 100644 index 0000000..5f62a3f --- /dev/null +++ b/schema/jsonschema/2020-12/meta/unevaluated.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true + }, + "$dynamicAnchor": "meta", + + "title": "Unevaluated applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "unevaluatedItems": { "$dynamicRef": "#meta" }, + "unevaluatedProperties": { "$dynamicRef": "#meta" } + } +} diff --git a/schema/jsonschema/2020-12/meta/validation.json b/schema/jsonschema/2020-12/meta/validation.json new file mode 100644 index 0000000..606b87b --- /dev/null +++ b/schema/jsonschema/2020-12/meta/validation.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/validation", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/validation": true + }, + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/schema/jsonschema/2020-12/schema.json b/schema/jsonschema/2020-12/schema.json new file mode 100644 index 0000000..d5e2d31 --- /dev/null +++ b/schema/jsonschema/2020-12/schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/unevaluated"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format-annotation"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} diff --git a/schema/openapi/3.1/3.0/schema.json b/schema/openapi/3.1/3.0/schema.json new file mode 100644 index 0000000..2513bd0 --- /dev/null +++ b/schema/openapi/3.1/3.0/schema.json @@ -0,0 +1,1506 @@ +{ + "id": "https://spec.openapis.org/oas/3.0/schema/2021-09-28", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "The description of OpenAPI v3.0.x documents, as defined by https://spec.openapis.org/oas/v3.0.3", + "type": "object", + "required": ["openapi", "info", "paths"], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": ["$ref"], + "patternProperties": { + "^\\$ref$": { + "type": "string", + "format": "uri-reference" + } + } + }, + "Info": { + "type": "object", + "required": ["title", "version"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri-reference" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": ["default"], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": {}, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": {}, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": {}, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": ["propertyName"], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": ["description"], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": {}, + "externalValue": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": ["simple"], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": {} + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/definitions/Operation" + }, + "^x-": {} + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": ["responses"], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": {} + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": ["url"], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri-reference" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": ["example", "examples"] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": ["schema", "content"] + }, + "oneOf": [ + { + "required": ["schema"] + }, + { + "required": ["content"], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": ["style"] + } + }, + { + "not": { + "required": ["explode"] + } + }, + { + "not": { + "required": ["allowReserved"] + } + }, + { + "not": { + "required": ["example"] + } + }, + { + "not": { + "required": ["examples"] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": {}, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "required": ["name", "in"], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + }, + { + "$ref": "#/definitions/ParameterLocation" + } + ] + }, + "ParameterLocation": { + "description": "Parameter location", + "oneOf": [ + { + "description": "Parameter in path", + "required": ["required"], + "properties": { + "in": { + "enum": ["path"] + }, + "style": { + "enum": ["matrix", "label", "simple"], + "default": "simple" + }, + "required": { + "enum": [true] + } + } + }, + { + "description": "Parameter in query", + "properties": { + "in": { + "enum": ["query"] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + { + "description": "Parameter in header", + "properties": { + "in": { + "enum": ["header"] + }, + "style": { + "enum": ["simple"], + "default": "simple" + } + } + }, + { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": ["cookie"] + }, + "style": { + "enum": ["form"], + "default": "form" + } + } + } + ] + }, + "RequestBody": { + "type": "object", + "required": ["content"], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": ["type", "name", "in"], + "properties": { + "type": { + "type": "string", + "enum": ["apiKey"] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": ["header", "query", "cookie"] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": ["scheme", "type"], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["http"] + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": ["bearerFormat"] + }, + "properties": { + "scheme": { + "not": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": ["type", "flows"], + "properties": { + "type": { + "type": "string", + "enum": ["oauth2"] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": ["type", "openIdConnectUrl"], + "properties": { + "type": { + "type": "string", + "enum": ["openIdConnect"] + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": ["authorizationUrl", "scopes"], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": ["tokenUrl", "scopes"], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": ["tokenUrl", "scopes"], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": ["authorizationUrl", "tokenUrl", "scopes"], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": {} + }, + "requestBody": {}, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": {} + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": ["operationId", "operationRef"] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": {} + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + } +} diff --git a/schema/openapi/3.1/dialect/base.schema.json b/schema/openapi/3.1/dialect/base.schema.json new file mode 100644 index 0000000..eae8386 --- /dev/null +++ b/schema/openapi/3.1/dialect/base.schema.json @@ -0,0 +1,25 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/dialect/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OpenAPI 3.1 Schema Object Dialect", + "description": "A JSON Schema dialect describing schemas found in OpenAPI documents", + + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true, + "https://spec.openapis.org/oas/3.1/vocab/base": false + }, + + "$dynamicAnchor": "meta", + + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/schema" }, + { "$ref": "https://spec.openapis.org/oas/3.1/meta/base" } + ] +} diff --git a/schema/openapi/3.1/meta/base.schema.json b/schema/openapi/3.1/meta/base.schema.json new file mode 100644 index 0000000..a7a59f1 --- /dev/null +++ b/schema/openapi/3.1/meta/base.schema.json @@ -0,0 +1,87 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/meta/base", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "title": "OAS Base vocabulary", + "description": "A JSON Schema Vocabulary used in the OpenAPI Schema Dialect", + + "$vocabulary": { + "https://spec.openapis.org/oas/3.1/vocab/base": true + }, + + "$dynamicAnchor": "meta", + + "type": ["object", "boolean"], + "properties": { + "example": true, + "discriminator": { "$ref": "#/$defs/discriminator" }, + "externalDocs": { "$ref": "#/$defs/external-docs" }, + "xml": { "$ref": "#/$defs/xml" } + }, + + "$defs": { + "extensible": { + "patternProperties": { + "^x-": true + } + }, + + "discriminator": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["propertyName"], + "unevaluatedProperties": false + }, + + "external-docs": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + } + }, + "required": ["url"], + "unevaluatedProperties": false + }, + + "xml": { + "$ref": "#/$defs/extensible", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean" + }, + "wrapped": { + "type": "boolean" + } + }, + "unevaluatedProperties": false + } + } +} diff --git a/schema/openapi/3.1/schema-base.json b/schema/openapi/3.1/schema-base.json new file mode 100644 index 0000000..04c9f60 --- /dev/null +++ b/schema/openapi/3.1/schema-base.json @@ -0,0 +1,23 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/schema-base/2022-02-27", + "$schema": "https://json-schema.org/draft/2020-12/schema", + + "description": "The description of OpenAPI v3.1.x documents using the OpenAPI JSON Schema dialect, as defined by https://spec.openapis.org/oas/v3.1.0", + + "$ref": "https://spec.openapis.org/oas/3.1/schema/2022-02-27", + "properties": { + "jsonSchemaDialect": { "$ref": "#/$defs/dialect" } + }, + + "$defs": { + "dialect": { "const": "https://spec.openapis.org/oas/3.1/dialect/base" }, + + "schema": { + "$dynamicAnchor": "meta", + "$ref": "https://spec.openapis.org/oas/3.1/dialect/base", + "properties": { + "$schema": { "$ref": "#/$defs/dialect" } + } + } + } +} diff --git a/schema/openapi/3.1/schema.json b/schema/openapi/3.1/schema.json new file mode 100644 index 0000000..ed0fd49 --- /dev/null +++ b/schema/openapi/3.1/schema.json @@ -0,0 +1,1420 @@ +{ + "$id": "https://spec.openapis.org/oas/3.1/schema/2022-02-27", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "The description of OpenAPI v3.1.x documents without schema validation, as defined by https://spec.openapis.org/oas/v3.1.0", + "type": "object", + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.1\\.\\d+(-.+)?$" + }, + "info": { + "$ref": "#/$defs/info" + }, + "jsonSchemaDialect": { + "type": "string", + "format": "uri", + "default": "https://spec.openapis.org/oas/3.1/dialect/base" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + }, + "default": [ + { "url": "/" } + ] + }, + "paths": { + "$ref": "#/$defs/paths" + }, + "webhooks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "components": { + "$ref": "#/$defs/components" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/$defs/tag" + } + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "openapi", + "info" + ], + "anyOf": [ + { + "required": [ + "paths" + ] + }, + { + "required": [ + "components" + ] + }, + { + "required": [ + "webhooks" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "info": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#info-object", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string", + "format": "uri" + }, + "contact": { + "$ref": "#/$defs/contact" + }, + "license": { + "$ref": "#/$defs/license" + }, + "version": { + "type": "string" + } + }, + "required": [ + "title", + "version" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "contact": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#contact-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "license": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#license-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "identifier" + ] + }, + { + "required": [ + "url" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri-reference" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server-variable" + } + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "server-variable": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#server-variable-object", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "default" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "components": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#components-object", + "type": "object", + "properties": { + "schemas": { + "type": "object", + "additionalProperties": { + "$dynamicRef": "#meta" + } + }, + "responses": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/response-or-reference" + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + }, + "requestBodies": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/request-body-or-reference" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/security-scheme-or-reference" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "pathItems": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + } + }, + "patternProperties": { + "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": { + "$comment": "Enumerating all of the property names in the regex above is necessary for unevaluatedProperties to work as expected", + "propertyNames": { + "pattern": "^[a-zA-Z0-9._-]+$" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "paths": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#paths-object", + "type": "object", + "patternProperties": { + "^/": { + "$ref": "#/$defs/path-item" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/$defs/operation" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "path-item-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/path-item" + } + }, + "operation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object", + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter-or-reference" + } + }, + "requestBody": { + "$ref": "#/$defs/request-body-or-reference" + }, + "responses": { + "$ref": "#/$defs/responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/callbacks-or-reference" + } + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/$defs/security-requirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/$defs/server" + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "external-documentation": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#external-documentation-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#parameter-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "path", + "cookie" + ] + }, + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "required": [ + "name", + "in" + ], + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "allowEmptyValue": { + "default": false, + "type": "boolean" + } + } + }, + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/examples" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-path" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-header" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-query" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-cookie" + }, + { + "$ref": "#/$defs/parameter/dependentSchemas/schema/$defs/styles-for-form" + } + ], + "$defs": { + "styles-for-path": { + "if": { + "properties": { + "in": { + "const": "path" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "name": { + "pattern": "[^/#?]+$" + }, + "style": { + "default": "simple", + "enum": [ + "matrix", + "label", + "simple" + ] + }, + "required": { + "const": true + } + }, + "required": [ + "required" + ] + } + }, + "styles-for-header": { + "if": { + "properties": { + "in": { + "const": "header" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + } + } + } + }, + "styles-for-query": { + "if": { + "properties": { + "in": { + "const": "query" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + } + } + }, + "styles-for-cookie": { + "if": { + "properties": { + "in": { + "const": "cookie" + } + }, + "required": [ + "in" + ] + }, + "then": { + "properties": { + "style": { + "default": "form", + "const": "form" + } + } + } + }, + "styles-for-form": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "parameter-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/parameter" + } + }, + "request-body": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#request-body-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "content": { + "$ref": "#/$defs/content" + }, + "required": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "content" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "request-body-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/request-body" + } + }, + "content": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#fixed-fields-10", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/media-type" + }, + "propertyNames": { + "format": "media-range" + } + }, + "media-type": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#media-type-object", + "type": "object", + "properties": { + "schema": { + "$dynamicRef": "#meta" + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/encoding" + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/examples" + } + ], + "unevaluatedProperties": false + }, + "encoding": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#encoding-object", + "type": "object", + "properties": { + "contentType": { + "type": "string", + "format": "media-range" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "style": { + "default": "form", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "default": false, + "type": "boolean" + } + }, + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/encoding/$defs/explode-default" + } + ], + "unevaluatedProperties": false, + "$defs": { + "explode-default": { + "if": { + "properties": { + "style": { + "const": "form" + } + }, + "required": [ + "style" + ] + }, + "then": { + "properties": { + "explode": { + "default": true + } + } + }, + "else": { + "properties": { + "explode": { + "default": false + } + } + } + } + } + }, + "responses": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#responses-object", + "type": "object", + "properties": { + "default": { + "$ref": "#/$defs/response-or-reference" + } + }, + "patternProperties": { + "^[1-5](?:[0-9]{2}|XX)$": { + "$ref": "#/$defs/response-or-reference" + } + }, + "minProperties": 1, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#response-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/header-or-reference" + } + }, + "content": { + "$ref": "#/$defs/content" + }, + "links": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/link-or-reference" + } + } + }, + "required": [ + "description" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "response-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/response" + } + }, + "callbacks": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object", + "type": "object", + "$ref": "#/$defs/specification-extensions", + "additionalProperties": { + "$ref": "#/$defs/path-item-or-reference" + } + }, + "callbacks-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/callbacks" + } + }, + "example": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#example-object", + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": true, + "externalValue": { + "type": "string", + "format": "uri" + } + }, + "not": { + "required": [ + "value", + "externalValue" + ] + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "example-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/example" + } + }, + "link": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#link-object", + "type": "object", + "properties": { + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "operationId": true, + "parameters": { + "$ref": "#/$defs/map-of-strings" + }, + "requestBody": true, + "description": { + "type": "string" + }, + "body": { + "$ref": "#/$defs/server" + } + }, + "oneOf": [ + { + "required": [ + "operationRef" + ] + }, + { + "required": [ + "operationId" + ] + } + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "link-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/link" + } + }, + "header": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#header-object", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "default": false, + "type": "boolean" + }, + "deprecated": { + "default": false, + "type": "boolean" + }, + "schema": { + "$dynamicRef": "#meta" + }, + "content": { + "$ref": "#/$defs/content", + "minProperties": 1, + "maxProperties": 1 + } + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ] + } + ], + "dependentSchemas": { + "schema": { + "properties": { + "style": { + "default": "simple", + "const": "simple" + }, + "explode": { + "default": false, + "type": "boolean" + } + }, + "$ref": "#/$defs/examples" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "header-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/header" + } + }, + "tag": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#tag-object", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/$defs/external-documentation" + } + }, + "required": [ + "name" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "reference": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#reference-object", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "unevaluatedProperties": false + }, + "schema": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object", + "$dynamicAnchor": "meta", + "type": [ + "object", + "boolean" + ] + }, + "security-scheme": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-scheme-object", + "type": "object", + "properties": { + "type": { + "enum": [ + "apiKey", + "http", + "mutualTLS", + "oauth2", + "openIdConnect" + ] + }, + "description": { + "type": "string" + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/specification-extensions" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-apikey" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-http-bearer" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oauth2" + }, + { + "$ref": "#/$defs/security-scheme/$defs/type-oidc" + } + ], + "unevaluatedProperties": false, + "$defs": { + "type-apikey": { + "if": { + "properties": { + "type": { + "const": "apiKey" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "name": { + "type": "string" + }, + "in": { + "enum": [ + "query", + "header", + "cookie" + ] + } + }, + "required": [ + "name", + "in" + ] + } + }, + "type-http": { + "if": { + "properties": { + "type": { + "const": "http" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "scheme": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + } + }, + "type-http-bearer": { + "if": { + "properties": { + "type": { + "const": "http" + }, + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + }, + "required": [ + "type", + "scheme" + ] + }, + "then": { + "properties": { + "bearerFormat": { + "type": "string" + } + } + } + }, + "type-oauth2": { + "if": { + "properties": { + "type": { + "const": "oauth2" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "flows": { + "$ref": "#/$defs/oauth-flows" + } + }, + "required": [ + "flows" + ] + } + }, + "type-oidc": { + "if": { + "properties": { + "type": { + "const": "openIdConnect" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "openIdConnectUrl": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "openIdConnectUrl" + ] + } + } + } + }, + "security-scheme-or-reference": { + "if": { + "type": "object", + "required": [ + "$ref" + ] + }, + "then": { + "$ref": "#/$defs/reference" + }, + "else": { + "$ref": "#/$defs/security-scheme" + } + }, + "oauth-flows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/$defs/oauth-flows/$defs/implicit" + }, + "password": { + "$ref": "#/$defs/oauth-flows/$defs/password" + }, + "clientCredentials": { + "$ref": "#/$defs/oauth-flows/$defs/client-credentials" + }, + "authorizationCode": { + "$ref": "#/$defs/oauth-flows/$defs/authorization-code" + } + }, + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false, + "$defs": { + "implicit": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "password": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "client-credentials": { + "type": "object", + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + }, + "authorization-code": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/$defs/map-of-strings" + } + }, + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "$ref": "#/$defs/specification-extensions", + "unevaluatedProperties": false + } + } + }, + "security-requirement": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#security-requirement-object", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "specification-extensions": { + "$comment": "https://spec.openapis.org/oas/v3.1.0#specification-extensions", + "patternProperties": { + "^x-": true + } + }, + "examples": { + "properties": { + "example": true, + "examples": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/example-or-reference" + } + } + } + }, + "map-of-strings": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/schema_map.go b/schema_map.go new file mode 100644 index 0000000..f7b51a4 --- /dev/null +++ b/schema_map.go @@ -0,0 +1,213 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +type SchemaItem struct { + Key Text + Schema *Schema +} + +func (si *SchemaItem) Clone() SchemaItem { + if si == nil { + return SchemaItem{} + } + return SchemaItem{ + Key: si.Key, + Schema: si.Schema.Clone(), + } +} + +// SchemaMap is a psuedo, ordered map ofASew3 Schemas +// +// Under the hood, SchemaMap is a slice of SchemaEntry +type SchemaMap struct { + Location + Items []SchemaItem +} + +func (sm *SchemaMap) Nodes() []Node { + if sm == nil { + return nil + } + return downcastNodes(sm.nodes()) +} + +func (sm *SchemaMap) nodes() []node { + if sm == nil { + return nil + } + var edges []node + for _, e := range sm.Items { + edges = append(edges, e.Schema) + } + return edges +} + +func (sm *SchemaMap) Refs() []Ref { + if sm == nil { + return nil + } + var refs []Ref + for _, e := range sm.Items { + refs = append(refs, e.Schema.Refs()...) + } + return refs +} + +func (*SchemaMap) Kind() Kind { return KindSchemaMap } +func (*SchemaMap) sliceKind() Kind { return KindUndefined } +func (*SchemaMap) mapKind() Kind { return KindUndefined } +func (sm *SchemaMap) isNil() bool { return sm == nil } +func (sm *SchemaMap) Anchors() (*Anchors, error) { + if sm == nil { + return nil, nil + } + var anchors *Anchors + var err error + for _, e := range sm.Items { + if anchors, err = e.Schema.Anchors(); err != nil { + return nil, err + } + } + return anchors, nil +} + +// func (sm *SchemaMap) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return sm.resolveNodeByPointer(ptr) +// } + +// func (sm *SchemaMap) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return sm, nil +// } +// tok, _ := ptr.NextToken() +// v := sm.Get(Text(tok)) +// if v == nil { +// return nil, newErrNotFound(sm.AbsoluteLocation(), tok) +// } +// return v.resolveNodeByPointer(ptr) +// } + +func (sm *SchemaMap) Set(key Text, s *Schema) { + se := SchemaItem{ + Key: key, + Schema: s, + } + for i, v := range sm.Items { + if v.Key == key { + sm.Items[i] = se + return + } + } + sm.Items = append(sm.Items, se) +} + +func (sm *SchemaMap) setLocation(loc Location) error { + if sm == nil { + return nil + } + sm.Location = loc + + for _, e := range sm.Items { + err := e.Schema.setLocation(loc.AppendLocation(e.Key.String())) + if err != nil { + return err + } + } + return nil +} + +func (sm SchemaMap) Get(key Text) *Schema { + for _, v := range sm.Items { + if v.Key == key { + return v.Schema + } + } + return nil +} + +func (sm SchemaMap) MarshalJSON() ([]byte, error) { + b := bytes.Buffer{} + b.WriteByte('{') + var err error + var s []byte + for _, v := range sm.Items { + if b.Len() > 1 { + b.WriteByte(',') + } + jsonx.EncodeAndWriteString(&b, v.Key.String()) + b.WriteByte(':') + s, err = v.Schema.MarshalJSON() + if err != nil { + return nil, err + } + b.Write(s) + } + b.WriteByte('}') + return b.Bytes(), nil +} + +func (sm *SchemaMap) UnmarshalJSON(data []byte) error { + t := jsonx.TypeOf(data) + if t != jsonx.TypeObject { + return &json.UnmarshalTypeError{Value: t.String(), Type: reflect.TypeOf(sm)} + } + *sm = SchemaMap{} + var err error + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + var s Schema + err = json.Unmarshal([]byte(value.Raw), &s) + sm.Items = append(sm.Items, SchemaItem{Key: Text(key.String()), Schema: &s}) + return err == nil + }) + return err +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (sm SchemaMap) MarshalYAML() (interface{}, error) { + j, err := sm.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (sm *SchemaMap) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, sm) +} + +func (sm *SchemaMap) Clone() *SchemaMap { + if sm == nil { + return nil + } + m := make([]SchemaItem, len(sm.Items)) + for i, v := range sm.Items { + m[i] = v.Clone() + } + return &SchemaMap{ + Location: Location{ + absolute: *sm.absolute.Clone(), + relative: sm.relative, + }, + Items: m, + } +} + +var _ node = (*SchemaMap)(nil) diff --git a/schema_map_test.go b/schema_map_test.go new file mode 100644 index 0000000..37a2903 --- /dev/null +++ b/schema_map_test.go @@ -0,0 +1,33 @@ +package openapi_test + +import ( + "encoding/json" + "io" + "testing" + + "github.com/chanced/openapi" +) + +func TestSchemaMapMarshaling(t *testing.T) { + f, err := testdata.Open("testdata/schemas/petstore-schema-map-test-1.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + fc, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + var sm openapi.SchemaMap + err = json.Unmarshal(fc, &sm) + if err != nil { + t.Fatal(err) + } + b, err := json.MarshalIndent(sm, "", " ") + if err != nil { + t.Error(err) + } + _ = b + // fmt.Println(string(b)) +} diff --git a/schema_ref.go b/schema_ref.go new file mode 100644 index 0000000..d826e50 --- /dev/null +++ b/schema_ref.go @@ -0,0 +1,194 @@ +package openapi + +import ( + "encoding/json" + "fmt" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/chanced/uri" + "gopkg.in/yaml.v3" +) + +type SchemaRefType uint8 + +const ( + SchamRefTypeUndefined SchemaRefType = iota + SchemaRefTypeRef + SchemaRefTypeDynamic + SchemaRefTypeRecursive +) + +type SchemaRef struct { + Location + Ref *uri.URI `json:"-"` + Resolved *Schema `json:"-"` + + SchemaRefKind SchemaRefType `json:"-"` +} + +func (sr *SchemaRef) Nodes() []Node { + if sr == nil { + return nil + } + return downcastNodes(sr.nodes()) +} + +func (sr *SchemaRef) RefType() RefType { + switch sr.SchemaRefKind { + case SchemaRefTypeRef: + return RefTypeSchema + case SchemaRefTypeDynamic: + return RefTypeSchemaDynamicRef + case SchemaRefTypeRecursive: + return RefTypeSchemaRecursiveRef + default: + return RefTypeUndefined + } +} + +func (sr *SchemaRef) RefKind() Kind { return KindSchema } + +func (sr *SchemaRef) nodes() []node { return []node{sr.Resolved} } + +func (*SchemaRef) Refs() []Ref { return nil } + +func (sr *SchemaRef) IsResolved() bool { + return sr.Resolved != nil +} + +func (sr *SchemaRef) URI() *uri.URI { return sr.Ref } + +func (*SchemaRef) Kind() Kind { return KindSchemaRef } +func (*SchemaRef) mapKind() Kind { return KindUndefined } +func (*SchemaRef) sliceKind() Kind { return KindUndefined } + +func (sr *SchemaRef) ResolvedNode() Node { + if sr == nil { + return nil + } + return sr.Resolved +} + +// func (sr *SchemaRef) Clone() *SchemaRef { +// if sr == nil { +// return nil +// } +// c := *sr +// return &c +// } + +func (sr *SchemaRef) resolve(n Node) error { + if n == nil { + return fmt.Errorf("node is nil") + } + + if s, ok := n.(*Schema); ok { + sr.Resolved = s + return nil + } + return NewResolutionError(sr, KindSchema, n.Kind()) +} + +func (*SchemaRef) Anchors() (*Anchors, error) { return nil, nil } + +func (sr *SchemaRef) setLocation(l Location) error { + if sr == nil { + return nil + } + sr.Location = l + // if sr.Schema != nil { + // if sr.Ref != nil { + // nl, err := NewLocation(*sr.Ref) + // if err != nil { + // return err + // } + // sr.Schema.setLocation(nl) + // return nil + // } + // return sr.Schema.setLocation(l) + // } + return nil +} + +func (sr *SchemaRef) UnmarshalJSON(data []byte) error { + if jsonx.IsString(data) { + var u uri.URI + if err := json.Unmarshal(data, &u); err != nil { + return err + } + sr.Ref = &u + return nil + } + + var s Schema + err := json.Unmarshal(data, &s) + sr.Resolved = &s + return err +} + +func (sr SchemaRef) MarshalJSON() ([]byte, error) { + return json.Marshal(sr.Ref) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (sr SchemaRef) MarshalYAML() (interface{}, error) { + j, err := sr.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (sr *SchemaRef) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, sr) +} + +func (sr *SchemaRef) isNil() bool { return sr == nil } + +func (sr *SchemaRef) Clone() *SchemaRef { + if sr == nil { + return nil + } + var ref *uri.URI + if sr.Ref != nil { + ref = sr.Ref.Clone() + } + return &SchemaRef{ + Ref: ref, + Location: Location{ + absolute: sr.Location.absolute, + relative: sr.Location.relative, + }, + Resolved: sr.Resolved.Clone(), // should this be cloned? + SchemaRefKind: sr.SchemaRefKind, + } +} + +var ( + _ node = (*SchemaRef)(nil) + + _ Ref = (*SchemaRef)(nil) +) + +// func (sr *SchemaRef) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return sr.resolveNodeByPointer(ptr) +// } + +// func (sr *SchemaRef) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// tok, _ := ptr.NextToken() +// if !ptr.IsRoot() { +// if sr.Ref != nil { +// return nil, newErrNotResolvable(sr.Location.AbsoluteLocation(), tok) +// } +// } +// return sr, nil +// } diff --git a/schema_slice.go b/schema_slice.go new file mode 100644 index 0000000..df29c6e --- /dev/null +++ b/schema_slice.go @@ -0,0 +1,144 @@ +package openapi + +import ( + "encoding/json" + "strconv" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +type SchemaSlice struct { + Location + Items []*Schema +} + +func (ss *SchemaSlice) Nodes() []Node { + if ss == nil { + return nil + } + return downcastNodes(ss.nodes()) +} + +func (ss *SchemaSlice) nodes() []node { + edges := make([]node, len(ss.Items)) + for i, s := range ss.Items { + edges[i] = s + } + return edges +} + +func (ss *SchemaSlice) Anchors() (*Anchors, error) { + if ss == nil { + return nil, nil + } + var anchors *Anchors + var err error + for _, s := range ss.Items { + if anchors, err = anchors.merge(s.Anchors()); err != nil { + return nil, err + } + } + return anchors, nil +} + +func (ss *SchemaSlice) Refs() []Ref { + if ss == nil { + return nil + } + var refs []Ref + for _, s := range ss.Items { + refs = append(refs, s.Refs()...) + } + return refs +} + +func (*SchemaSlice) Kind() Kind { return KindSchemaSlice } + +// func (ss *SchemaSlice) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } + +// return ss.resolveNodeByPointer(ptr) +// } + +// func (ss *SchemaSlice) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return ss, nil +// } +// nxt, tok, _ := ptr.Next() +// idx, err := tok.Int() +// if err != nil { +// return nil, newErrNotResolvable(ss.Location.AbsoluteLocation(), tok) +// } +// if idx < 0 { +// return nil, newErrNotFound(ss.Location.AbsoluteLocation(), tok) +// } +// if idx >= len(ss.Items) { +// return nil, newErrNotFound(ss.Location.AbsoluteLocation(), tok) +// } +// return ss.Items[idx].resolveNodeByPointer(nxt) +// } + +func (ss SchemaSlice) MarshalJSON() ([]byte, error) { + return json.Marshal(ss.Items) +} + +func (ss *SchemaSlice) UnmarshalJSON(data []byte) error { + var v []*Schema + if err := json.Unmarshal(data, &v); err != nil { + return err + } + *ss = SchemaSlice{Items: v} + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (ss SchemaSlice) MarshalYAML() (interface{}, error) { + j, err := ss.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (ss *SchemaSlice) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, ss) +} + +func (*SchemaSlice) mapKind() Kind { return KindUndefined } +func (*SchemaSlice) sliceKind() Kind { return KindUndefined } + +func (ss *SchemaSlice) setLocation(loc Location) error { + if ss == nil { + return nil + } + ss.Location = loc + for i, s := range ss.Items { + err := s.setLocation(loc.AppendLocation(strconv.Itoa(i))) + if err != nil { + return err + } + } + return nil +} + +func (ss *SchemaSlice) Clone() *SchemaSlice { + if ss == nil { + return nil + } + v := make([]*Schema, len(ss.Items)) + for i, s := range ss.Items { + v[i] = s.Clone() + } + return &SchemaSlice{Items: v} +} +func (ss *SchemaSlice) isNil() bool { return ss == nil } + +var _ node = (*SchemaSlice)(nil) diff --git a/schema_test.go b/schema_test.go index 55845a6..05ec983 100644 --- a/schema_test.go +++ b/schema_test.go @@ -2,355 +2,63 @@ package openapi_test import ( "encoding/json" - "fmt" "testing" - jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/stretchr/testify/require" - yaml "sigs.k8s.io/yaml" - - "github.com/chanced/cmpjson" "github.com/chanced/openapi" + "github.com/chanced/uri" ) func TestSchema(t *testing.T) { - assert := require.New(t) + bs := []byte(`true`) + + var s openapi.Schema + err := json.Unmarshal(bs, &s) + if err != nil { + t.Error(err) + } + sb, err := s.MarshalJSON() + if err != nil { + t.Error(err) + } + if string(sb) != "true" { + t.Errorf("expected %q, got %q", "true", string(sb)) + } + var s2 openapi.Schema + bs = []byte(`{"keyword": "value"}`) + err = json.Unmarshal(bs, &s2) + if err != nil { + t.Error(err) + } + if string(s2.Keywords["keyword"]) != `"value"` { + t.Errorf("expected %q, got %q", "value", s2.Keywords["keyword"]) + } + br, err := s2.MarshalJSON() + if err != nil { + t.Error(err) + } + _ = br + // fmt.Println(string(br)) +} - j := []string{`{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/tree", - "$dynamicAnchor": "node", - "type": "object", - "properties": { - "data": true, - "children": { - "type": "array", - "items": { "$dynamicRef": "#node" } - } - }, - "discriminator": { - "propertyName": "type", - "x-extension": true - } - }`, - `{ - "$id": "https://example.com/person.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } - }`, - `{ - "$ref": "#/$defs/enabledToggle", - "default": true - }`, - `{ - "title": "Feature A", - "properties": { - "enabled": { - "$ref": "#/$defs/enabledToggle", - "default": true - } - } - }`, - `{ - "title": "Feature B", - "properties": { - "enabled": { - "description": "If set to null, Feature B inherits the enabled value from Feature A", - "$ref": "#/$defs/enabledToggle" - } - } - }`, - `{ - "title": "Feature list", - "type": "array", - "prefixObjs": [ - { - "title": "Feature A", - "properties": { - "enabled": { - "$ref": "#/$defs/enabledToggle", - "default": true - } - } +func TestClone(t *testing.T) { + s := openapi.Schema{ + If: &openapi.Schema{ + Format: "format", + }, + Schema: uri.MustParse("https://json-schema.org/draft/2019-09/schema"), + ID: uri.MustParse("http://example.com/schema"), + Ref: &openapi.SchemaRef{ + Ref: uri.MustParse("http://example.com/schema2"), + Resolved: &openapi.Schema{ + ID: uri.MustParse("http://example.com/resolved"), }, - { - "title": "Feature B", - "properties": { - "enabled": { - "description": "If set to null, Feature B inherits the enabled value from Feature A", - "$ref": "#/$defs/enabledToggle" - } - } - } - ], - "$defs": { - "enabledToggle": { - "title": "Enabled", - "description": "Whether the feature is enabled (true), disabled (false), or under automatic control (null)", - "type": ["boolean", "null"], - "default": null - } - } - }`, - `{ - "$id": "https://example.com/geographical-location.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Longitude and Latitude Values", - "description": "A geographical coordinate.", - "required": [ "latitude", "longitude" ], - "type": "object", - "properties": { - "latitude": { - "type": "number", - "minimum": -90, - "maximum": 90 - }, - "longitude": { - "type": "number", - "minimum": -180, - "maximum": 180 - } - } - }`, - `{ - "$id": "https://example.com/card.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "A representation of a person, company, organization, or place", - "type": "object", - "required": [ "familyName", "givenName" ], - "properties": { - "fn": { - "description": "Formatted Name", - "type": "string" - }, - "familyName": { - "type": "string" - }, - "givenName": { - "type": "string" - }, - "additionalName": { - "type": "array", - "items": { - "type": "string" - } - }, - "honorificPrefix": { - "type": "array", - "items": { - "type": "string" - } - }, - "honorificSuffix": { - "type": "array", - "items": { - "type": "string" - } - }, - "nickname": { - "type": "string" - }, - "url": { - "type": "string" - }, - "email": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "tel": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "adr": { "$ref": "https://example.com/address.schema.json" }, - "geo": { "$ref": "https://example.com/geographical-location.schema.json" }, - "tz": { - "type": "string" - }, - "photo": { - "type": "string" - }, - "logo": { - "type": "string" - }, - "sound": { - "type": "string" - }, - "bday": { - "type": "string" - }, - "title": { - "type": "string" - }, - "role": { - "type": "string" - }, - "org": { - "type": "object", - "properties": { - "organizationName": { - "type": "string" - }, - "organizationUnit": { - "type": "string" - } - } - } - } - }`, - `{ - "$id": "https://example.com/calendar.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "A representation of an event", - "type": "object", - "required": [ "dtstart", "summary" ], - "properties": { - "dtstart": { - "type": "string", - "description": "Event starting time" - }, - "dtend": { - "type": "string", - "description": "Event ending time" - }, - "summary": { - "type": "string" - }, - "location": { - "type": "string" - }, - "url": { - "type": "string" - }, - "duration": { - "type": "string", - "description": "Event duration" - }, - "rdate": { - "type": "string", - "description": "Recurrence date" - }, - "rrule": { - "type": "string", - "description": "Recurrence rule" - }, - "category": { - "type": "string" - }, - "description": { - "type": "string" - }, - "geo": { - "$ref": "https://example.com/geographical-location.schema.json" - } - } - }`, - `{ - "$id": "https://example.com/address.schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "An address similar to http://microformats.org/wiki/h-card", - "type": "object", - "properties": { - "post-office-box": { - "type": "string" - }, - "extended-address": { - "type": "string" - }, - "street-address": { - "type": "string" - }, - "locality": { - "type": "string" - }, - "region": { - "type": "string" - }, - "postal-code": { - "type": "string" - }, - "country-name": { - "type": "string" - } }, - "required": [ "locality", "region", "country-name" ], - "dependentRequired": { - "post-office-box": [ "street-address" ], - "extended-address": [ "street-address" ] - } - }`, } - for _, d := range j { - var data = []byte(d) - var v *openapi.SchemaObj - err := json.Unmarshal(data, &v) - assert.NoError(err) - - err = json.Unmarshal(data, &v) - assert.NoError(err) - - b, err := json.MarshalIndent(v, "", " ") - assert.NoError(err) - - if !jsonpatch.Equal(data, b) { - fmt.Println(d) - fmt.Println(string(b)) - // litter.Dump(v) - } - assert.True(jsonpatch.Equal(data, b), cmpjson.Diff(data, b)) - assert.NoError(err) - b, err = yaml.Marshal(v) - assert.NoError(err) - - var s *openapi.SchemaObj - err = yaml.Unmarshal(b, &s) - assert.NoError(err) - b, err = json.MarshalIndent(s, "", " ") - assert.NoError(err) - assert.True(jsonpatch.Equal(b, data)) - - // testing yaml - - y, err := yaml.JSONToYAML(data) - assert.NoError(err) - var yo openapi.SchemaObj - err = yaml.Unmarshal(y, &yo) - assert.NoError(err) - yb, err := json.MarshalIndent(yo, "", " ") - assert.NoError(err) - if !jsonpatch.Equal(data, yb) { - fmt.Println(string(data), "\n------------------------\n", string(yb)) - } - assert.True(jsonpatch.Equal(data, yb), cmpjson.Diff(data, yb)) - + s2 := s.Clone() + if s2.If.Format != "format" { + t.Errorf("expected %q, got %q", "format", s2.If.Format) + } + if s2.Ref.Resolved == nil { + t.Error("expected resolved schema, got nil") } } diff --git a/scope.go b/scope.go new file mode 100644 index 0000000..296452c --- /dev/null +++ b/scope.go @@ -0,0 +1,316 @@ +package openapi + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v3" +) + +type Scope struct { + Location `json:"-"` + Key Text `json:"-"` + Value Text `json:"-"` +} + +func (*Scope) Anchors() (*Anchors, error) { return nil, nil } +func (*Scope) Kind() Kind { return KindScope } +func (*Scope) mapKind() Kind { return KindUndefined } +func (*Scope) sliceKind() Kind { return KindUndefined } +func (s *Scope) Refs() []Ref { return nil } + +// func (s *Scope) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return s.resolveNodeByPointer(ptr) +// } +// +// func (s *Scope) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return s, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(s.AbsoluteLocation(), tok) +// } +func (s *Scope) Nodes() []Node { + if s == nil { + return nil + } + return downcastNodes(s.nodes()) +} +func (s *Scope) nodes() []node { return nil } + +func (s Scope) MarshalJSON() ([]byte, error) { + return json.Marshal(s.Value) +} + +func (s *Scope) UnmarshalJSON(data []byte) error { + *s = Scope{} + if len(data) == 0 { + return nil + } + t := jsonx.TypeOf(data) + switch t { + case jsonx.TypeString: + var v Text + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + s.Value = v + return nil + default: + var v map[Text]Text + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + if len(v) > 1 { + return fmt.Errorf("can not unmarshal more than a single key/value pair into a Scope") + } + for k, v := range v { + s.Key = k + s.Value = v + break + } + return nil + } +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (s Scope) MarshalYAML() (interface{}, error) { + j, err := s.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (s *Scope) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, s) +} + +func (s *Scope) setLocation(loc Location) error { + if s == nil { + return nil + } + s.Location = loc + return nil +} + +func (s Scope) String() string { + return s.Value.String() +} + +func (s Scope) Text() Text { + return s.Value +} + +type Scopes struct { + Location `json:"-"` + + Items []*Scope `json:"-"` +} + +func (s *Scopes) Refs() []Ref { + if s == nil { + return nil + } + var refs []Ref + for _, v := range s.Items { + refs = append(refs, v.Refs()...) + } + return refs +} +func (*Scopes) Anchors() (*Anchors, error) { return nil, nil } +func (*Scopes) Kind() Kind { return KindScopes } +func (*Scopes) mapKind() Kind { return KindUndefined } +func (*Scopes) sliceKind() Kind { return KindUndefined } + +func (s *Scopes) isNil() bool { return s == nil } + +// func (s *Scopes) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return s.resolveNodeByPointer(ptr) +// } + +// func (s *Scopes) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return s, nil +// } +// tok, _ := ptr.NextToken() +// if tok == "" { +// return s, nil +// } +// tk := Text(tok) +// for _, v := range s.Items { +// if v.Key == tk { +// return v.ResolveNodeByPointer(ptr) +// } +// } +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } + +func (s *Scopes) Nodes() []Node { + if s == nil { + return nil + } + return downcastNodes(s.nodes()) +} + +func (s *Scopes) nodes() []node { + if s == nil { + return nil + } + nodes := make([]node, len(s.Items)) + for i, n := range s.Items { + nodes[i] = n + } + return nodes +} + +func (s *Scopes) setLocation(loc Location) error { + if s == nil { + return nil + } + s.Location = loc + for _, item := range s.Items { + item.Location = loc.AppendLocation(item.Key.String()) + } + return nil +} + +func (s Scopes) Get(key Text) *Scope { + for _, v := range s.Items { + if v.Key == key { + return v + } + } + return nil +} + +func (s Scopes) Has(key Text) bool { + for _, v := range s.Items { + if v.Key == key { + return true + } + } + return false +} + +func (s *Scopes) Set(key Text, value Text) { + if s == nil { + *s = Scopes{} + } + for _, v := range s.Items { + if v.Key == key { + v.Value = value + return + } + } + s.Items = append(s.Items, &Scope{ + Key: key, + Value: value, + }) +} + +func (s *Scopes) Map() map[Text]Text { + if s == nil || s.Items == nil { + return nil + } + m := make(map[Text]Text, len(s.Items)) + for _, v := range s.Items { + m[v.Key] = v.Value + } + return m +} + +func (s Scopes) MarshalJSON() ([]byte, error) { + b := strings.Builder{} + b.WriteByte('{') + for _, v := range s.Items { + if b.Len() > 1 { + b.WriteByte(',') + } + key, err := json.Marshal(v.Key) + if err != nil { + return nil, err + } + b.Write(key) + b.WriteByte(':') + value, err := json.Marshal(v.Value) + if err != nil { + return nil, err + } + b.Write(value) + } + b.WriteByte('}') + return []byte(b.String()), nil +} + +func (s *Scopes) UnmarshalJSON(data []byte) error { + *s = Scopes{} + if len(data) == 0 { + return nil + } + if !jsonx.IsObject(data) { + return &json.UnmarshalTypeError{ + Value: jsonx.TypeOf(data).String(), + Type: reflect.TypeOf(s), + Struct: "Scopes", + } + } + var err error + var v string + gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool { + err = json.Unmarshal([]byte(value.Raw), &v) + if err != nil { + return false + } + s.Items = append(s.Items, &Scope{ + Key: Text(key.String()), + Value: Text(v), + }) + return true + }) + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (s Scopes) MarshalYAML() (interface{}, error) { + j, err := s.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (s *Scopes) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, s) +} + +func (s *Scope) isNil() bool { return s == nil } + +var ( + _ node = (*Scope)(nil) + + _ node = (*Scopes)(nil) +) diff --git a/security.go b/security.go deleted file mode 100644 index d21c181..0000000 --- a/security.go +++ /dev/null @@ -1,187 +0,0 @@ -package openapi - -import ( - "encoding/json" - - "github.com/chanced/openapi/yamlutil" -) - -const ( - // SecuritySchemeTypeAPIKey = "apiKey" - SecuritySchemeTypeAPIKey SecuritySchemeType = "apiKey" - // SecuritySchemeTypeHTTP = "http" - SecuritySchemeTypeHTTP SecuritySchemeType = "http" - // SecuritySchemeTypeMutualTLS = mutualTLS - SecuritySchemeTypeMutualTLS SecuritySchemeType = "mutualTLS" - // SecuritySchemeTypeOAuth2 = oauth2 - SecuritySchemeTypeOAuth2 SecuritySchemeType = "oauth2" - // SecuritySchemeTypeOpenIDConnect = "openIdConnect" - SecuritySchemeTypeOpenIDConnect SecuritySchemeType = "openIdConnect" -) - -// SecurityRequirements is a list of SecurityRequirement -type SecurityRequirements []SecurityRequirement - -// SecurityRequirement lists the required security schemes to execute this -// operation. The name used for each property MUST correspond to a security -// scheme declared in the Security Schemes under the Components Object. -// -// Security Requirement Objects that contain multiple schemes require that all -// schemes MUST be satisfied for a request to be authorized. This enables -// support for scenarios where multiple query parameters or HTTP headers are -// required to convey security information. -// -// When a list of Security Requirement Objects is defined on the OpenAPI Object -// or Operation Object, only one of the Security Requirement Objects in the list -// needs to be satisfied to authorize the request. -// -// Each name MUST correspond to a security scheme which is declared in the -// Security Schemes under the Components Object. If the security scheme is of -// type "oauth2" or "openIdConnect", then the value is a list of scope names -// required for the execution, and the list MAY be empty if authorization does -// not require a specified scope. For other security scheme types, the array MAY -// contain a list of role names which are required for the execution, but are -// not otherwise defined or exchanged in-band. -type SecurityRequirement map[string][]string - -// SecuritySchemeType represents the type of the security scheme. -type SecuritySchemeType string - -func (ss SecuritySchemeType) String() string { - return string(ss) -} - -// SecuritySchemeKind is either a SecuritySchemeObj or Reference -type SecuritySchemeKind uint8 - -const ( - // SecuritySchemeKindObj = SecuritySchemeObj - SecuritySchemeKindObj SecuritySchemeKind = iota - // SecuritySchemeKindRef = Reference - SecuritySchemeKindRef -) - -// SecuritySchemes is a map of SecurityScheme -type SecuritySchemes map[string]SecurityScheme - -// UnmarshalJSON unmarshals json -func (ss *SecuritySchemes) UnmarshalJSON(data []byte) error { - var dm map[string]json.RawMessage - if err := json.Unmarshal(data, &dm); err != nil { - return err - } - res := make(SecuritySchemes, len(dm)) - for k, d := range dm { - if isRefJSON(d) { - v, err := unmarshalReferenceJSON(d) - if err != nil { - return err - } - res[k] = v - continue - } - var v SecuritySchemeObj - if err := json.Unmarshal(d, &v); err != nil { - return err - } - res[k] = &v - } - *ss = res - return nil -} - -// MarshalYAML marshals YAML -func (ss SecuritySchemes) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(ss) -} - -// UnmarshalYAML unmarshals YAML -func (ss *SecuritySchemes) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, ss) -} - -// SecuritySchemeObj defines a security scheme that can be used by the operations. -type SecuritySchemeObj struct { - // The type of the security scheme. - // - // *required - Type SecuritySchemeType `json:"type,omitempty"` - // Any description for security scheme. CommonMark syntax MAY be used for - // rich text representation. - Description string `json:"description,omitempty"` - // The name of the header, query or cookie parameter to be used. - // - // Applies to: API Key - // - // *required* - Name string `json:"name,omitempty"` - // The location of the API key. Valid values are "query", "header" or "cookie". - // - // Applies to: APIKey - // - // *required* - In In `json:"in,omitempty"` - // The name of the HTTP Authorization scheme to be used in the Authorization - // header as defined in RFC7235. The values used SHOULD be registered in the - // IANA Authentication Scheme registry. - // - // *required* - Scheme string `json:"scheme,omitempty"` - - // http ("bearer") A hint to the client to identify how the bearer token is - // formatted. Bearer tokens are usually generated by an authorization - // server, so this information is primarily for documentation purposes. - BearerFormat string `json:"bearerFormat,omitempty"` - - // An object containing configuration information for the flow types supported. - // - // *required* - Flows *OAuthFlows `json:"flows,omitempty"` - - // OpenId Connect URL to discover OAuth2 configuration values. This MUST be - // in the form of a URL. The OpenID Connect standard requires the use of - // TLS. - // - // *required* - OpenIDConnectURL string `json:"openIdConnect,omitempty"` - Extensions `json:"-"` -} - -type securityscheme SecuritySchemeObj - -// UnmarshalJSON unmarshals JSON -func (sso *SecuritySchemeObj) UnmarshalJSON(data []byte) error { - var v securityscheme - err := unmarshalExtendedJSON(data, &v) - *sso = SecuritySchemeObj(v) - return err -} - -// MarshalJSON marshals JSON -func (sso SecuritySchemeObj) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(securityscheme(sso)) -} - -// MarshalYAML marshals YAML -func (sso SecuritySchemeObj) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(sso) -} - -// UnmarshalYAML unmarshals YAML -func (sso *SecuritySchemeObj) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, sso) -} - -// SecuritySchemeKind returns SecuritySchemeKindObj -func (sso *SecuritySchemeObj) SecuritySchemeKind() SecuritySchemeKind { return SecuritySchemeKindObj } - -// ResolveSecurityScheme resolves SecuritySchemeObj by returning itself. resolve is not called. -func (sso *SecuritySchemeObj) ResolveSecurityScheme(SecuritySchemeResolver) (*SecuritySchemeObj, error) { - return sso, nil -} - -// SecurityScheme can either be a ScecuritySchemeObj or a Reference -type SecurityScheme interface { - ResolveSecurityScheme(SecuritySchemeResolver) (*SecuritySchemeObj, error) - SecuritySchemeKind() SecuritySchemeKind -} diff --git a/security_requirement.go b/security_requirement.go new file mode 100644 index 0000000..6ac038c --- /dev/null +++ b/security_requirement.go @@ -0,0 +1,151 @@ +package openapi + +import ( + "encoding/json" + "fmt" + + "github.com/chanced/jsonx" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +// TODO: make SecurityRequirement an ordered slice. + +// SecurityRequirementMap is a list of SecurityRequirement +type SecurityRequirementMap = ObjMap[*SecurityRequirement] + +type SecurityRequirementSlice = ObjSlice[*SecurityRequirement] + +type SecurityRequirementItem struct { + Location + Key Text + Value Texts +} + +func (sri *SecurityRequirementItem) Nodes() []Node { + if sri == nil { + return nil + } + return downcastNodes(sri.nodes()) +} +func (sri *SecurityRequirementItem) nodes() []node { return nil } + +func (sri *SecurityRequirementItem) Refs() []Ref { return nil } +func (sri *SecurityRequirementItem) isNil() bool { return sri == nil } + +func (sri *SecurityRequirementItem) Anchors() (*Anchors, error) { return nil, nil } + +func (sri *SecurityRequirementItem) setLocation(loc Location) error { + if sri == nil { + return nil + } + sri.Location = loc + return nil +} + +func (*SecurityRequirementItem) Kind() Kind { return KindSecurityRequirementItem } +func (*SecurityRequirementItem) mapKind() Kind { return KindSecurityRequirement } +func (*SecurityRequirementItem) sliceKind() Kind { return KindUndefined } +func (*SecurityRequirementItem) objSliceKind() Kind { return KindSecurityRequirementSlice } + +// func (sri *SecurityRequirementItem) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return sri.resolveNodeByPointer(ptr) +// } + +// func (sri *SecurityRequirementItem) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return sri, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(sri.AbsoluteLocation(), tok) +// } + +func (sri SecurityRequirementItem) MarshalJSON() ([]byte, error) { + return json.Marshal(sri.Value) +} + +func (sri *SecurityRequirementItem) UnmarshalJSON(data []byte) error { + *sri = SecurityRequirementItem{} + if len(data) == 0 { + return nil + } + t := jsonx.TypeOf(data) + switch t { + case jsonx.TypeString: + var v Texts + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + sri.Value = v + return nil + default: + var v map[Text]Texts + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + if len(v) > 1 { + return fmt.Errorf("can not unmarshal more than a single key/value pair into a Scope") + } + for k, v := range v { + sri.Key = k + sri.Value = v + break + } + return nil + } +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (sri SecurityRequirementItem) MarshalYAML() (interface{}, error) { + j, err := sri.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (sri *SecurityRequirementItem) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, sri) +} + +// SecurityRequirement lists the required security schemes to execute this +// operation. The name used for each property MUST correspond to a security +// scheme declared in the Security Schemes under the Components Object. +// +// Security Requirement Objects that contain multiple schemes require that all +// schemes MUST be satisfied for a request to be authorized. This enables +// support for scenarios where multiple query parameters or HTTP headers are +// required to convey security information. +// +// When a list of Security Requirement Objects is defined on the OpenAPI Object +// or Operation Object, only one of the Security Requirement Objects in the list +// needs to be satisfied to authorize the request. +// +// Each name MUST correspond to a security scheme which is declared in the +// Security Schemes under the Components Object. If the security scheme is of +// type "oauth2" or "openIdConnect", then the value is a list of scope names +// required for the execution, and the list MAY be empty if authorization does +// not require a specified scope. For other security scheme types, the array MAY +// contain a list of role names which are required for the execution, but are +// not otherwise defined or exchanged in-band. +type SecurityRequirement = ObjMap[*SecurityRequirementItem] + +var ( + _ node = (*SecuritySchemeMap)(nil) + + _ node = (*SecurityRequirementMap)(nil) + + _ node = (*SecurityRequirement)(nil) + + _ node = (*SecurityRequirementItem)(nil) +) diff --git a/security_scheme.go b/security_scheme.go new file mode 100644 index 0000000..83e64ad --- /dev/null +++ b/security_scheme.go @@ -0,0 +1,181 @@ +package openapi + +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +const ( + // SecuritySchemeTypeAPIKey = "apiKey" + SecuritySchemeTypeAPIKey Text = "apiKey" + // SecuritySchemeTypeHTTP = "http" + SecuritySchemeTypeHTTP Text = "http" + // SecuritySchemeTypeMutualTLS = mutualTLS + SecuritySchemeTypeMutualTLS Text = "mutualTLS" + // SecuritySchemeTypeOAuth2 = oauth2 + SecuritySchemeTypeOAuth2 Text = "oauth2" + // SecuritySchemeTypeOpenIDConnect = "openIdConnect" + SecuritySchemeTypeOpenIDConnect Text = "openIdConnect" +) + +// SecuritySchemeMap is a map of SecurityScheme +type SecuritySchemeMap = ComponentMap[*SecurityScheme] + +// SecurityScheme defines a security scheme that can be used by the operations. +type SecurityScheme struct { + Extensions `json:"-"` + Location `json:"-"` + + // The type of the security scheme. + // + // *required + Type Text `json:"type,omitempty"` + + // Any description for security scheme. CommonMark syntax MAY be used for + // rich text representation. + Description Text `json:"description,omitempty"` + // The name of the header, query or cookie parameter to be used. + // + // Applies to: API Key + // + // *required* + Name Text `json:"name,omitempty"` + // The location of the API key. Valid values are "query", "header" or "cookie". + // + // Applies to: APIKey + // + // *required* + In In `json:"in,omitempty"` + // The name of the HTTP Authorization scheme to be used in the Authorization + // header as defined in RFC7235. The values used SHOULD be registered in the + // IANA Authentication Scheme registry. + // + // *required* + Scheme Text `json:"scheme,omitempty"` + + // http ("bearer") A hint to the client to identify how the bearer token is + // formatted. Bearer tokens are usually generated by an authorization + // server, so this information is primarily for documentation purposes. + BearerFormat Text `json:"bearerFormat,omitempty"` + + // An object containing configuration information for the flow types supported. + // + // *required* + Flows *OAuthFlows `json:"flows,omitempty"` + + // OpenId Connect URL to discover OAuth2 configuration values. This MUST be + // in the form of a URL. The OpenID Connect standard requires the use of + // TLS. + // + // *required* + OpenIDConnectURL Text `json:"openIdConnect,omitempty"` +} + +func (ss *SecurityScheme) Nodes() []Node { + if ss == nil { + return nil + } + return downcastNodes(ss.nodes()) +} + +func (ss *SecurityScheme) nodes() []node { + if ss == nil { + return nil + } + return appendEdges(nil, ss.Flows) +} + +func (ss *SecurityScheme) Refs() []Ref { + if ss == nil { + return nil + } + return ss.Flows.Refs() +} +func (ss *SecurityScheme) isNil() bool { return ss == nil } + +func (ss *SecurityScheme) Anchors() (*Anchors, error) { + if ss == nil { + return nil, nil + } + + return ss.Flows.Anchors() +} + +func (s *SecurityScheme) setLocation(loc Location) error { + if s == nil { + return nil + } + s.Location = loc + return s.Flows.setLocation(loc.AppendLocation("flows")) +} + +// UnmarshalJSON unmarshals JSON +func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { + type securityscheme SecurityScheme + + var v securityscheme + err := unmarshalExtendedJSON(data, &v) + *ss = SecurityScheme(v) + return err +} + +// MarshalJSON marshals JSON +func (ss SecurityScheme) MarshalJSON() ([]byte, error) { + type securityscheme SecurityScheme + + return marshalExtendedJSON(securityscheme(ss)) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (ss SecurityScheme) MarshalYAML() (interface{}, error) { + j, err := ss.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (ss *SecurityScheme) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, ss) +} + +func (*SecurityScheme) Kind() Kind { return KindSecurityScheme } +func (*SecurityScheme) mapKind() Kind { return KindSecuritySchemeMap } +func (*SecurityScheme) sliceKind() Kind { return KindUndefined } + +func (*SecurityScheme) refable() {} + +var _ node = (*SecurityScheme)(nil) + +// func (ss *SecurityScheme) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return ss.resolveNodeByPointer(ptr) +// } + +// func (ss *SecurityScheme) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return ss, nil +// } +// nxt, tok, _ := ptr.Next() +// switch nxt { +// case "flows": +// if nxt.IsRoot() { +// return ss.Flows, nil +// } +// if ss.Flows == nil { +// return nil, newErrNotFound(ss.AbsoluteLocation(), tok) +// } +// return ss.Flows.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(ss.AbsoluteLocation(), tok) +// } +// } diff --git a/server.go b/server.go index a72f1a2..4f20be2 100644 --- a/server.go +++ b/server.go @@ -1,87 +1,132 @@ package openapi -import "github.com/chanced/openapi/yamlutil" +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +type ( + ServerSlice = ObjSlice[*Server] + ServerVariableMap = ObjMap[*ServerVariable] +) // Server represention of a Server. type Server struct { + Location `json:"-"` + Extensions `json:"-"` + // A URL to the target host. This URL supports Server Variables and MAY be // relative, to indicate that the host location is relative to the location // where the OpenAPI document is being served. Variable substitutions will // be made when a variable is named in {brackets}. - URL string `json:"url"` + URL Text `json:"url"` + // Description of the host designated by the URL. CommonMark syntax MAY be // used for rich text representation. - Description string `json:"description,omitempty"` + Description Text `json:"description,omitempty"` + // A map between a variable name and its value. The value is used for // substitution in the server's URL template. - Variables map[string]*ServerVariable `json:"variables,omitempty"` - Extensions `json:"-"` + Variables *ServerVariableMap `json:"variables,omitempty"` } -type server Server -// MarshalJSON marshals JSON -func (s Server) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(server(s)) +func (s *Server) Nodes() []Node { + if s == nil { + return nil + } + return downcastNodes(s.nodes()) } -// UnmarshalJSON unmarshals JSON -func (s *Server) UnmarshalJSON(data []byte) error { - var v server - err := unmarshalExtendedJSON(data, &v) - *s = Server(v) - return err +func (s *Server) nodes() []node { + if s == nil { + return nil + } + return appendEdges(nil, s.Variables) } -// MarshalYAML marshals YAML -func (s Server) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(s) +func (s *Server) Refs() []Ref { + if s == nil { + return nil + } + var refs []Ref + if s.Variables != nil { + refs = append(refs, s.Variables.Refs()...) + } + return refs } -// UnmarshalYAML unmarshals YAML data into s -func (s *Server) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, s) -} +// func (s *Server) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return s.resolveNodeByPointer(ptr) +// } -// ServerVariable for server URL template substitution. -type ServerVariable struct { - // An enumeration of string values to be used if the substitution options - // are from a limited set. The array MUST NOT be empty. - Enum []string `json:"enum" yaml:"enum"` - // The default value to use for substitution, which SHALL be sent if an - // alternate value is not supplied. Note this behavior is different than the - // Schema Object's treatment of default values, because in those cases - // parameter values are optional. If the enum is defined, the value MUST - // exist in the enum's values. - // - // *required* - Default string `json:"default" yaml:"default"` - // An optional description for the server variable. CommonMark syntax MAY be - // used for rich text representation. - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Extensions `json:"-"` -} +// func (s *Server) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return s, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "variables": +// if s.Variables == nil { +// return nil, newErrNotFound(s.AbsoluteLocation(), tok) +// } +// return s.Variables.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(s.Location.AbsoluteLocation(), tok) +// } +// } -type servervariable ServerVariable +func (*Server) Kind() Kind { return KindServer } +func (*Server) mapKind() Kind { return KindUndefined } +func (*Server) sliceKind() Kind { return KindServerSlice } + +func (*Server) Anchors() (*Anchors, error) { return nil, nil } + +func (s *Server) setLocation(loc Location) error { + if s == nil { + return nil + } + s.Location = loc + return s.Variables.setLocation(loc.AppendLocation("variables")) +} // MarshalJSON marshals JSON -func (sv ServerVariable) MarshalJSON() ([]byte, error) { - return marshalExtendedJSON(servervariable(sv)) +func (s Server) MarshalJSON() ([]byte, error) { + type server Server + return marshalExtendedJSON(server(s)) } // UnmarshalJSON unmarshals JSON -func (sv *ServerVariable) UnmarshalJSON(data []byte) error { - var v servervariable +func (s *Server) UnmarshalJSON(data []byte) error { + type server Server + var v server err := unmarshalExtendedJSON(data, &v) - *sv = ServerVariable(v) + *s = Server(v) return err } -// MarshalYAML marshals YAML -func (sv ServerVariable) MarshalYAML() (interface{}, error) { - return yamlutil.Marshal(sv) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (s Server) MarshalYAML() (interface{}, error) { + j, err := s.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals YAML data into s -func (sv *ServerVariable) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, sv) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (s *Server) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, s) } + +func (s *Server) isNil() bool { return s == nil } + +var _ node = (*Server)(nil) diff --git a/server_variable.go b/server_variable.go new file mode 100644 index 0000000..6686041 --- /dev/null +++ b/server_variable.go @@ -0,0 +1,106 @@ +package openapi + +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +// ServerVariable for server URL template substitution. +type ServerVariable struct { + // An enumeration of string values to be used if the substitution options + // are from a limited set. The array MUST NOT be empty. + Enum Texts `json:"enum"` + // The default value to use for substitution, which SHALL be sent if an + // alternate value is not supplied. Note this behavior is different than the + // Schema Object's treatment of default values, because in those cases + // parameter values are optional. If the enum is defined, the value MUST + // exist in the enum's values. + // + // *required* + Default Text `json:"default"` + // An optional description for the server variable. CommonMark syntax MAY be + // used for rich text representation. + Description Text `json:"description,omitempty"` + + Location `json:"-"` + Extensions `json:"-"` +} + +func (sv *ServerVariable) Nodes() []Node { + if sv == nil { + return nil + } + return downcastNodes(sv.nodes()) +} +func (sv *ServerVariable) nodes() []node { return nil } + +func (*ServerVariable) Refs() []Ref { return nil } + +// func (sv *ServerVariable) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return sv.resolveNodeByPointer(ptr) +// } + +// func (sv *ServerVariable) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return sv, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(sv.Location.AbsoluteLocation(), tok) +// } + +func (*ServerVariable) Kind() Kind { return KindServerVariable } +func (*ServerVariable) mapKind() Kind { return KindServerVariableMap } +func (*ServerVariable) sliceKind() Kind { return KindUndefined } + +func (sv *ServerVariable) setLocation(loc Location) error { + if sv == nil { + return nil + } + sv.Location = loc + return nil +} + +// MarshalJSON marshals JSON +func (sv ServerVariable) MarshalJSON() ([]byte, error) { + type servervariable ServerVariable + return marshalExtendedJSON(servervariable(sv)) +} + +// UnmarshalJSON unmarshals JSON +func (sv *ServerVariable) UnmarshalJSON(data []byte) error { + type servervariable ServerVariable + var v servervariable + err := unmarshalExtendedJSON(data, &v) + *sv = ServerVariable(v) + return err +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (sv ServerVariable) MarshalYAML() (interface{}, error) { + j, err := sv.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (sv *ServerVariable) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, sv) +} + +func (sv *ServerVariable) Anchors() (*Anchors, error) { + return nil, nil +} +func (sv *ServerVariable) isNil() bool { return sv == nil } + +var _ node = (*ServerVariable)(nil) diff --git a/style.go b/style.go new file mode 100644 index 0000000..36d0c7b --- /dev/null +++ b/style.go @@ -0,0 +1,25 @@ +package openapi + +const ( + // StyleForm for + StyleForm Text = "form" + // StyleSimple comma-separated values. Corresponds to the + // {param_name} URI template. + StyleSimple Text = "simple" + // StyleMatrix is semicolon-prefixed values, also known as path-style + // expansion. Corresponds to the {;param_name} URI template. + StyleMatrix Text = "matrix" + // StyleLabel dot-prefixed values, also known as label expansion. + // Corresponds to the {.param_name} URI template. + StyleLabel Text = "label" + // StyleDeepObject a simple way of rendering nested objects using + // form parameters (applies to objects only). + StyleDeepObject Text = "deepObject" + // StylePipeDelimited is pipeline-separated array values. + // + // Same as collectionFormat: pipes in OpenAPI 2.0. Has effect only for + // non-exploded arrays (explode: false), that is, the pipe separates the + // array values if the array is a single parameter, as in + // arr=a|b|c + StylePipeDelimited Text = "pipeDelimited" +) diff --git a/tag.go b/tag.go index 05dd79a..e7c8fb9 100644 --- a/tag.go +++ b/tag.go @@ -3,58 +3,122 @@ package openapi import ( "encoding/json" - "github.com/chanced/openapi/yamlutil" + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" ) +type TagMap = ObjMap[*Tag] + // Tag adds metadata that is used by the Operation Object. // // It is not mandatory to have a Tag Object per tag defined in the Operation // Object instances. type Tag struct { + Location `json:"-"` + Extensions `json:"-"` // The name of the tag. // // *required* - Name string `json:"name" yaml:"name"` + Name Text `json:"name"` // A description for the tag. // // CommonMark syntax MAY be used for rich text representation. // // https://spec.commonmark.org/ - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Description Text `json:"description,omitempty"` // Additional external documentation for this tag. ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" bson:"externalDocs,omitempty"` +} - Extensions `json:"-"` +func (t *Tag) Nodes() []Node { + if t == nil { + return nil + } + return downcastNodes(t.nodes()) } -type tag Tag +func (t *Tag) nodes() []node { + if t == nil { + return nil + } + return appendEdges(nil, t.ExternalDocs) +} + +func (*Tag) Refs() []Ref { return nil } +func (*Tag) Anchors() (*Anchors, error) { return nil, nil } +func (t *Tag) isNil() bool { return t == nil } + +// func (t *Tag) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return t.resolveNodeByPointer(ptr) +// } + +// func (t *Tag) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return t, nil +// } +// nxt, tok, _ := ptr.Next() +// switch tok { +// case "externalDocs": +// if nxt.IsRoot() { +// return t.ExternalDocs, nil +// } +// if t.ExternalDocs == nil { +// return nil, newErrNotFound(t.Location.AbsoluteLocation(), tok) +// } +// return t.ExternalDocs.resolveNodeByPointer(nxt) +// default: +// return nil, newErrNotResolvable(t.Location.AbsoluteLocation(), tok) +// } +// } + +func (*Tag) Kind() Kind { return KindTag } +func (*Tag) mapKind() Kind { return KindUndefined } +func (*Tag) sliceKind() Kind { return KindTagSlice } + +func (t *Tag) setLocation(loc Location) error { + if t == nil { + return nil + } + t.Location = loc + return t.ExternalDocs.setLocation(loc.AppendLocation("externalDocs")) +} // MarshalJSON marshals t into JSON func (t Tag) MarshalJSON() ([]byte, error) { + type tag Tag + return marshalExtendedJSON(tag(t)) } // UnmarshalJSON unmarshals json into t func (t *Tag) UnmarshalJSON(data []byte) error { + type tag Tag + v := tag{} err := unmarshalExtendedJSON(data, &v) *t = Tag(v) return err } -// MarshalYAML first marshals and unmarshals into JSON and then marshals into -// YAML +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface func (t Tag) MarshalYAML() (interface{}, error) { - b, err := json.Marshal(t) + j, err := t.MarshalJSON() if err != nil { return nil, err } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err + return transcode.YAMLFromJSON(j) } -// UnmarshalYAML unmarshals yaml into t -func (t *Tag) UnmarshalYAML(unmarshal func(interface{}) error) error { - return yamlutil.Unmarshal(unmarshal, t) +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (t *Tag) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, t) } + +var _ node = (*Tag)(nil) diff --git a/tag_slice.go b/tag_slice.go new file mode 100644 index 0000000..865211d --- /dev/null +++ b/tag_slice.go @@ -0,0 +1,123 @@ +package openapi + +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + +type TagSlice struct { + Location `json:"-"` + + Items []*Tag +} + +func (tl *TagSlice) Nodes() []Node { + if tl == nil { + return nil + } + return downcastNodes(tl.nodes()) +} + +func (tl *TagSlice) nodes() []node { + edges := make([]node, len(tl.Items)) + for i, item := range tl.Items { + edges[i] = item + } + return edges +} + +func (*TagSlice) Kind() Kind { return KindTagSlice } + +func (ts *TagSlice) Refs() []Ref { + if ts == nil { + return nil + } + var refs []Ref + for _, item := range ts.Items { + refs = append(refs, item.Refs()...) + } + return refs +} + +// func (ts TagSlice) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return ts.resolveNodeByPointer(ptr) +// } + +// func (ts *TagSlice) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return ts, nil +// } +// nxt, tok, _ := ptr.Next() +// idx, err := tok.Int() +// if err != nil { +// return nil, newErrNotResolvable(ts.Location.AbsoluteLocation(), tok) +// } +// if idx < 0 { +// return nil, newErrNotResolvable(ts.Location.AbsoluteLocation(), tok) +// } +// if idx >= len(ts.Items) { +// return nil, newErrNotFound(ts.Location.AbsoluteLocation(), tok) +// } +// return ts.Items[idx].resolveNodeByPointer(nxt) +// } + +func (ts *TagSlice) MarshalJSON() ([]byte, error) { + return json.Marshal(ts.Items) +} + +func (ts *TagSlice) UnmarshalJSON(data []byte) error { + var items []*Tag + if err := json.Unmarshal(data, &items); err != nil { + return err + } + *ts = TagSlice{ + Items: items, + } + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (t TagSlice) MarshalYAML() (interface{}, error) { + j, err := t.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (t *TagSlice) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, t) +} + +func (ts *TagSlice) isNil() bool { + return ts == nil +} + +func (ts TagSlice) location() Location { + return ts.Location +} + +func (*TagSlice) mapKind() Kind { return KindUndefined } +func (*TagSlice) sliceKind() Kind { return KindUndefined } + +func (ts *TagSlice) setLocation(loc Location) error { + if ts == nil { + return nil + } + ts.Location = loc + return nil +} + +func (*TagSlice) Anchors() (*Anchors, error) { return nil, nil } + +var _ node = (*TagSlice)(nil) diff --git a/testdata/documents/comprefs.yaml b/testdata/documents/comprefs.yaml new file mode 100644 index 0000000..eca75ff --- /dev/null +++ b/testdata/documents/comprefs.yaml @@ -0,0 +1,29 @@ +openapi: "3.1.0" +info: + version: 1.0.0 +paths: + /ref: + parameters: + - $ref: "#/components/parameters/Referenced" + post: + requestBody: + $ref: "#/components/requestBodies/Referenced" + responses: + "200": + $ref: "#/components/responses/Referenced" +components: + parameters: + Referenced: + description: /components/parameters/Referenced + style: matrix + schema: + type: string + responses: + Referenced: + description: /components/responses/Referenced + requestBodies: + Referenced: + description: /components/requestBodies/Referenced + links: + Referenced: + description: /components/links/Referenced diff --git a/testdata/documents/dynamic-refs.yaml b/testdata/documents/dynamic-refs.yaml new file mode 100644 index 0000000..6578762 --- /dev/null +++ b/testdata/documents/dynamic-refs.yaml @@ -0,0 +1,5 @@ +openapi: "3.1" +components: + schemas: + ListOfStrings: + $ref: https://json-schema.blog/list-of-strings diff --git a/testdata/documents/petstore.yaml b/testdata/documents/petstore.yaml new file mode 100644 index 0000000..acd2020 --- /dev/null +++ b/testdata/documents/petstore.yaml @@ -0,0 +1,294 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + summary: a petstore + description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification + termsOfService: http://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: http://petstore.swagger.io/api + +paths: + /generic: + # parameters: + # - name: objparam + # style: + post: + operationId: createGenericMap + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/StringMap" + responses: + "200": + description: string response + content: + application/json: + schema: + $ref: "#/components/schemas/StringMap" + /pets: + parameters: + - $ref: "#/components/parameters/Referenced" + - name: globalPetsParam + required: false + style: form + in: query + schema: + type: string + get: + summary: Returns all pets + description: | + Returns all pets from the system that the user has access to + 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. + + 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. + operationId: findPets + parameters: + # - name: tags + # in: query + # description: tags to filter by + # required: false + # style: form + # schema: + # type: array + # items: + # type: string + - name: filter + in: query + required: false + schema: + $ref: "#/components/schemas/PetFilter" + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + "200": + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Creates a new pet + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + description: Pet to add to the store + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewPet" + responses: + "200": + description: pet response + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{_id}: + get: + operationId: findPetByID + summary: Returns a pet by ID + description: Returns a pet based on a single ID + parameters: + - name: _id + in: path + description: ID of pet to fetch + required: true + schema: + type: string + format: uuid + responses: + "200": + description: pet response + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + delete: + summary: Deletes a pet by ID + description: deletes a single pet based on the ID supplied + operationId: deletePet + parameters: + - name: _id + in: path + description: ID of pet to delete + required: true + schema: + type: string + format: uuid + responses: + "204": + description: pet deleted + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + parameters: + Referenced: + name: referenced + required: false + style: form + in: query + schema: + type: string + headers: + ReferencedHeader: + description: "referenced header" + required: true + allowEmptyValue: false + style: simple + schema: + type: string + schemas: + FilterItem: + description: /components/schemas/FilterItem + type: object + properties: + include: + type: string + exclude: + type: string + StringMap: + description: /components/schemas/StringMap + type: object + additionalProperties: + type: string + PetFilter: + type: object + description: /components/schemas/PetFilter + properties: + tags: + description: /components/schemas/PetFilter/tags + type: array + items: + description: /components/schemas/PetFilter/properties/tags/items + type: string + kind: + type: string + nested: + type: object + properties: + prop: + type: string + additionalProperties: + $ref: "#/components/schemas/FilterItem" + Pet: + allOf: + - $ref: "#/components/schemas/NewPet" + - required: + - _id + properties: + _id: + type: string + format: uuid + description: Unique id of the pet + + NewPet: + required: + - name + properties: + name: + type: string + description: Name of the pet + tag: + type: string + description: Type of the pet + kind: + type: string + extra: + anyOf: + - type: object + properties: + firstField: + type: string + - type: string + additionalProperties: + type: string + GenericError: + breakpoint: true + type: object + properties: + statusCode: + type: number + error: + type: string + additionalProperties: true + ValidationError: + type: object + properties: + parameters: + $ref: "#/components/schemas/ValidationErrorOutputUnit" + body: + $ref: "#/components/schemas/ValidationErrorOutputUnit" + ValidationErrorOutputUnitArray: + type: array + items: + $ref: "#/components/schemas/ValidationErrorOutputUnit" + ValidationErrorOutputUnit: + description: A schema that validates the minimum requirements for validation output + properties: + valid: + type: boolean + keywordLocation: + type: string + format: uri-reference + absoluteKeywordLocation: + type: string + format: uri + instanceLocation: + type: string + format: uri-reference + errors: + $ref: "#/components/schemas/ValidationErrorOutputUnitArray" + annotations: + $ref: "#/components/schemas/ValidationErrorOutputUnitArray" + required: + - valid + - keywordLocation + - instanceLocation + + Error: + type: object + properties: + error: + type: string + # oneOf: + # - $ref: "#/components/schemas/ValidationError" + # - $ref: "#/components/schemas/GenericError" diff --git a/testdata/documents/validation/fail/invalid_schema_types.yaml b/testdata/documents/validation/fail/invalid_schema_types.yaml new file mode 100644 index 0000000..d295b1f --- /dev/null +++ b/testdata/documents/validation/fail/invalid_schema_types.yaml @@ -0,0 +1,13 @@ +openapi: 3.1.1 + +# this example shows invalid types for the schemaObject + +info: + title: API + version: 1.0.0 +components: + schemas: + invalid_null: null + invalid_number: 0 + invalid_array: [] + diff --git a/testdata/documents/validation/fail/no_containers.yaml b/testdata/documents/validation/fail/no_containers.yaml new file mode 100644 index 0000000..c158bcb --- /dev/null +++ b/testdata/documents/validation/fail/no_containers.yaml @@ -0,0 +1,7 @@ +openapi: 3.1.0 + +# this example should fail as there are no paths, components or webhooks containers (at least one of which must be present) + +info: + title: API + version: 1.0.0 diff --git a/testdata/documents/validation/fail/server_enum_empty.yaml b/testdata/documents/validation/fail/server_enum_empty.yaml new file mode 100644 index 0000000..cd6d30e --- /dev/null +++ b/testdata/documents/validation/fail/server_enum_empty.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.0 + +# this example should fail as the server variable enum is empty, and so does not contain the default value + +info: + title: API + version: 1.0.0 +servers: + - url: https://example.com/{var} + variables: + var: + enum: [] + default: a +components: {} diff --git a/testdata/documents/validation/fail/servers.yaml b/testdata/documents/validation/fail/servers.yaml new file mode 100644 index 0000000..1470fe1 --- /dev/null +++ b/testdata/documents/validation/fail/servers.yaml @@ -0,0 +1,11 @@ +openapi: 3.1.0 + +# this example should fail, as servers must be an array, not an object + +info: + title: API + version: 1.0.0 +paths: {} +servers: + url: /v1 + description: Run locally. diff --git a/testdata/documents/validation/fail/unknown_container.yaml b/testdata/documents/validation/fail/unknown_container.yaml new file mode 100644 index 0000000..7f31e86 --- /dev/null +++ b/testdata/documents/validation/fail/unknown_container.yaml @@ -0,0 +1,8 @@ +openapi: 3.1.0 + +# this example should fail as overlays is not a valid top-level object/keyword + +info: + title: API + version: 1.0.0 +overlays: {} diff --git a/testdata/documents/validation/pass/comp_pathitems.yaml b/testdata/documents/validation/pass/comp_pathitems.yaml new file mode 100644 index 0000000..502ca1f --- /dev/null +++ b/testdata/documents/validation/pass/comp_pathitems.yaml @@ -0,0 +1,6 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +components: + pathItems: {} diff --git a/testdata/documents/validation/pass/info_summary.yaml b/testdata/documents/validation/pass/info_summary.yaml new file mode 100644 index 0000000..30d224a --- /dev/null +++ b/testdata/documents/validation/pass/info_summary.yaml @@ -0,0 +1,6 @@ +openapi: 3.1.0 +info: + title: API + summary: My lovely API + version: 1.0.0 +components: {} diff --git a/testdata/documents/validation/pass/license_identifier.yaml b/testdata/documents/validation/pass/license_identifier.yaml new file mode 100644 index 0000000..fbdba5e --- /dev/null +++ b/testdata/documents/validation/pass/license_identifier.yaml @@ -0,0 +1,9 @@ +openapi: 3.1.0 +info: + title: API + summary: My lovely API + version: 1.0.0 + license: + name: Apache + identifier: Apache-2.0 +components: {} diff --git a/testdata/documents/validation/pass/mega.yaml b/testdata/documents/validation/pass/mega.yaml new file mode 100644 index 0000000..8838c03 --- /dev/null +++ b/testdata/documents/validation/pass/mega.yaml @@ -0,0 +1,49 @@ +openapi: 3.1.0 +info: + summary: My API's summary + title: My API + version: 1.0.0 + license: + name: Apache 2.0 + identifier: Apache-2.0 +jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base +paths: + /: + get: + parameters: [] + /{pathTest}: {} +webhooks: + myWebhook: + $ref: '#/components/pathItems/myPathItem' + description: Overriding description +components: + securitySchemes: + mtls: + type: mutualTLS + pathItems: + myPathItem: + post: + requestBody: + required: true + content: + 'application/json': + schema: + type: object + properties: + type: + type: string + int: + type: integer + exclusiveMaximum: 100 + exclusiveMinimum: 0 + none: + type: 'null' + arr: + type: array + $comment: Array without items keyword + either: + type: ['string','null'] + discriminator: + propertyName: type + x-extension: true + myArbitraryKeyword: true diff --git a/testdata/documents/validation/pass/minimal_comp.yaml b/testdata/documents/validation/pass/minimal_comp.yaml new file mode 100644 index 0000000..4553689 --- /dev/null +++ b/testdata/documents/validation/pass/minimal_comp.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +components: {} diff --git a/testdata/documents/validation/pass/minimal_hooks.yaml b/testdata/documents/validation/pass/minimal_hooks.yaml new file mode 100644 index 0000000..e67b288 --- /dev/null +++ b/testdata/documents/validation/pass/minimal_hooks.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +webhooks: {} diff --git a/testdata/documents/validation/pass/minimal_paths.yaml b/testdata/documents/validation/pass/minimal_paths.yaml new file mode 100644 index 0000000..016e867 --- /dev/null +++ b/testdata/documents/validation/pass/minimal_paths.yaml @@ -0,0 +1,5 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +paths: {} diff --git a/testdata/documents/validation/pass/path_no_response.yaml b/testdata/documents/validation/pass/path_no_response.yaml new file mode 100644 index 0000000..334608f --- /dev/null +++ b/testdata/documents/validation/pass/path_no_response.yaml @@ -0,0 +1,7 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +paths: + /: + get: {} diff --git a/testdata/documents/validation/pass/path_var_empty_pathitem.yaml b/testdata/documents/validation/pass/path_var_empty_pathitem.yaml new file mode 100644 index 0000000..ba92742 --- /dev/null +++ b/testdata/documents/validation/pass/path_var_empty_pathitem.yaml @@ -0,0 +1,6 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +paths: + /{var}: {} diff --git a/testdata/documents/validation/pass/schema.yaml b/testdata/documents/validation/pass/schema.yaml new file mode 100644 index 0000000..e192529 --- /dev/null +++ b/testdata/documents/validation/pass/schema.yaml @@ -0,0 +1,55 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +paths: {} +components: + schemas: + model: + type: object + properties: + one: + description: type array + type: + - integer + - string + two: + description: type 'null' + type: "null" + three: + description: type array including 'null' + type: + - string + - "null" + four: + description: array with no items + type: array + five: + description: singular example + type: string + examples: + - exampleValue + six: + description: exclusiveMinimum true + exclusiveMinimum: 10 + seven: + description: exclusiveMinimum false + minimum: 10 + eight: + description: exclusiveMaximum true + exclusiveMaximum: 20 + nine: + description: exclusiveMaximum false + maximum: 20 + ten: + description: nullable string + type: + - string + - "null" + eleven: + description: x-nullable string + type: + - string + - "null" + twelve: + description: file/binary diff --git a/testdata/documents/validation/pass/servers.yaml b/testdata/documents/validation/pass/servers.yaml new file mode 100644 index 0000000..77a2049 --- /dev/null +++ b/testdata/documents/validation/pass/servers.yaml @@ -0,0 +1,10 @@ +openapi: 3.1.0 +info: + title: API + version: 1.0.0 +paths: {} +servers: + - url: /v1 + description: Run locally. + - url: https://production.com/v1 + description: Run on production server. diff --git a/testdata/documents/validation/pass/valid_schema_types.yaml b/testdata/documents/validation/pass/valid_schema_types.yaml new file mode 100644 index 0000000..4431adc --- /dev/null +++ b/testdata/documents/validation/pass/valid_schema_types.yaml @@ -0,0 +1,14 @@ +openapi: 3.1.1 + +# this example shows that top-level schemaObjects MAY be booleans + +info: + title: API + version: 1.0.0 +components: + schemas: + anything_boolean: true + nothing_boolean: false + anything_object: {} + nothing_object: { not: {} } + diff --git a/testdata/schemas/address.json b/testdata/schemas/address.json new file mode 100644 index 0000000..5603e5b --- /dev/null +++ b/testdata/schemas/address.json @@ -0,0 +1,34 @@ +{ + "$id": "https://example.com/address", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "An address similar to http://microformats.org/wiki/h-card", + "type": "object", + "properties": { + "post-office-box": { + "type": "string" + }, + "extended-address": { + "type": "string" + }, + "street-address": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "region": { + "type": "string" + }, + "postal-code": { + "type": "string" + }, + "country-name": { + "type": "string" + } + }, + "required": ["locality", "region", "country-name"], + "dependentRequired": { + "post-office-box": ["street-address"], + "extended-address": ["street-address"] + } +} diff --git a/testdata/schemas/calendar.json b/testdata/schemas/calendar.json new file mode 100644 index 0000000..0539e09 --- /dev/null +++ b/testdata/schemas/calendar.json @@ -0,0 +1,47 @@ +{ + "$id": "https://example.com/calendar", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A representation of an event", + "type": "object", + "required": [ "dtstart", "summary" ], + "properties": { + "dtstart": { + "type": "string", + "description": "Event starting time" + }, + "dtend": { + "type": "string", + "description": "Event ending time" + }, + "summary": { + "type": "string" + }, + "location": { + "type": "string" + }, + "url": { + "type": "string" + }, + "duration": { + "type": "string", + "description": "Event duration" + }, + "rdate": { + "type": "string", + "description": "Recurrence date" + }, + "rrule": { + "type": "string", + "description": "Recurrence rule" + }, + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "geo": { + "$ref": "https://example.com/geographical-location" + } + } + } \ No newline at end of file diff --git a/testdata/schemas/card.json b/testdata/schemas/card.json new file mode 100644 index 0000000..cc24b8e --- /dev/null +++ b/testdata/schemas/card.json @@ -0,0 +1,99 @@ +{ + "$id": "https://example.com/card", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A representation of a person, company, organization, or place", + "type": "object", + "required": ["familyName", "givenName"], + "properties": { + "fn": { + "description": "Formatted Name", + "type": "string" + }, + "familyName": { + "type": "string" + }, + "givenName": { + "type": "string" + }, + "additionalName": { + "type": "array", + "items": { + "type": "string" + } + }, + "honorificPrefix": { + "type": "array", + "items": { + "type": "string" + } + }, + "honorificSuffix": { + "type": "array", + "items": { + "type": "string" + } + }, + "nickname": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "tel": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "adr": { "$ref": "https://example.com/address" }, + "geo": { "$ref": "https://example.com/geographical-location" }, + "tz": { + "type": "string" + }, + "photo": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "sound": { + "type": "string" + }, + "bday": { + "type": "string" + }, + "title": { + "type": "string" + }, + "role": { + "type": "string" + }, + "org": { + "type": "object", + "properties": { + "organizationName": { + "type": "string" + }, + "organizationUnit": { + "type": "string" + } + } + } + } +} diff --git a/testdata/schemas/enable-toggle.json b/testdata/schemas/enable-toggle.json new file mode 100644 index 0000000..c6d4dc5 --- /dev/null +++ b/testdata/schemas/enable-toggle.json @@ -0,0 +1,9 @@ +{ + "$ref": "#/$defs/enabledToggle", + "default": true, + "$defs": { + "enableToggle": { + "type": "boolean" + } + } +} diff --git a/testdata/schemas/geographic-location.json b/testdata/schemas/geographic-location.json new file mode 100644 index 0000000..9eaa059 --- /dev/null +++ b/testdata/schemas/geographic-location.json @@ -0,0 +1,20 @@ +{ + "$id": "https://example.com/geographical-location", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Longitude and Latitude Values", + "description": "A geographical coordinate.", + "required": ["latitude", "longitude"], + "type": "object", + "properties": { + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + } + } +} diff --git a/testdata/schemas/list-of-strings.json b/testdata/schemas/list-of-strings.json new file mode 100644 index 0000000..0dbcda1 --- /dev/null +++ b/testdata/schemas/list-of-strings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.blog/list-of-strings", + "$defs": { + "string-items": { + "$dynamicAnchor": "T", + "type": "string", + "$comment": "A string item" + } + }, + "$ref": "/list-of-t" +} diff --git a/testdata/schemas/list-of-t.json b/testdata/schemas/list-of-t.json new file mode 100644 index 0000000..8d04143 --- /dev/null +++ b/testdata/schemas/list-of-t.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.blog/list-of-t", + "$defs": { + "content": { + "$dynamicAnchor": "T", + "not": true + } + }, + "type": "array", + "items": { "$dynamicRef": "#T" } +} diff --git a/testdata/schemas/person.json b/testdata/schemas/person.json new file mode 100644 index 0000000..14e5d85 --- /dev/null +++ b/testdata/schemas/person.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/person", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + } +} diff --git a/testdata/schemas/petstore-schema-map-test-1.json b/testdata/schemas/petstore-schema-map-test-1.json new file mode 100644 index 0000000..ab27ce7 --- /dev/null +++ b/testdata/schemas/petstore-schema-map-test-1.json @@ -0,0 +1,160 @@ +{ + "FilterItem": { + "type": "object", + "properties": { + "include": { + "type": "string" + }, + "exclude": { + "type": "string" + } + } + }, + "StringMap": { + "testing": "testing", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "PetFilter": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "kind": { + "type": "string" + }, + "nested": { + "type": "object", + "properties": { + "prop": { + "type": "string" + } + } + } + }, + "additionalProperties": { + "$ref": "#/components/schemas/FilterItem" + } + }, + "Pet": { + "allOf": [ + { + "$ref": "#/components/schemas/NewPet" + }, + { + "required": ["_id"], + "properties": { + "_id": { + "type": "string", + "format": "uuid", + "description": "Unique id of the pet" + } + } + } + ] + }, + "NewPet": { + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Name of the pet" + }, + "tag": { + "type": "string", + "description": "Type of the pet" + }, + "kind": { + "type": "string" + }, + "extra": { + "anyOf": [ + { + "type": "object", + "properties": { + "firstField": { + "type": "string" + } + } + }, + { + "type": "string" + } + ] + } + }, + "additionalProperties": { + "type": "string" + } + }, + "GenericError": { + "type": "object", + "properties": { + "statusCode": { + "type": "number" + }, + "error": { + "type": "string" + } + }, + "additionalProperties": true + }, + "ValidationError": { + "type": "object", + "properties": { + "parameters": { + "$ref": "#/components/schemas/ValidationErrorOutputUnit" + }, + "body": { + "$ref": "#/components/schemas/ValidationErrorOutputUnit" + } + } + }, + "ValidationErrorOutputUnitArray": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationErrorOutputUnit" + } + }, + "ValidationErrorOutputUnit": { + "required": ["valid", "keywordLocation", "instanceLocation"], + "properties": { + "valid": { + "type": "boolean" + }, + "keywordLocation": { + "type": "string", + "format": "uri-reference" + }, + "absoluteKeywordLocation": { + "type": "string", + "format": "uri" + }, + "instanceLocation": { + "type": "string", + "format": "uri-reference" + }, + "errors": { + "$ref": "#/components/schemas/ValidationErrorOutputUnitArray" + }, + "annotations": { + "$ref": "#/components/schemas/ValidationErrorOutputUnitArray" + } + }, + "description": "A schema that validates the minimum requirements for validation output" + }, + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } +} diff --git a/testdata/schemas/tree.json b/testdata/schemas/tree.json new file mode 100644 index 0000000..7a41923 --- /dev/null +++ b/testdata/schemas/tree.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/tree", + "$dynamicAnchor": "node", + "type": "object", + "properties": { + "data": true, + "children": { + "type": "array", + "items": { "$dynamicRef": "#node" } + } + }, + "discriminator": { + "propertyName": "type", + "x-extension": true + } +} diff --git a/schema_type.go b/type.go similarity index 80% rename from schema_type.go rename to type.go index a7dd832..0cd0399 100644 --- a/schema_type.go +++ b/type.go @@ -3,54 +3,59 @@ package openapi import ( "encoding/json" - "github.com/chanced/dynamic" + "github.com/chanced/jsonx" ) const ( // TypeString = string // // https://json-schema.org/understanding-json-schema/reference/string.html#string - TypeString SchemaType = "string" + TypeString Type = "string" // TypeNumber = number // // https://json-schema.org/understanding-json-schema/reference/numeric.html#number - TypeNumber SchemaType = "number" + TypeNumber Type = "number" // TypeInteger = integer // // https://json-schema.org/understanding-json-schema/reference/numeric.html#integer - TypeInteger SchemaType = "integer" + TypeInteger Type = "integer" // TypeObject = object // // https://json-schema.org/understanding-json-schema/reference/object.html#object - TypeObject SchemaType = "object" + TypeObject Type = "object" // TypeArray = array // // https://json-schema.org/understanding-json-schema/reference/array.html#array - TypeArray SchemaType = "array" + TypeArray Type = "array" // TypeBoolean = boolean // // https://json-schema.org/understanding-json-schema/reference/boolean.html#boolean - TypeBoolean SchemaType = "boolean" + TypeBoolean Type = "boolean" // TypeNull = null // // https://json-schema.org/understanding-json-schema/reference/null.html#null - TypeNull SchemaType = "null" + TypeNull Type = "null" ) -// SchemaType restricts to a JSON Schema specific type +// Type restricts to a JSON Schema specific type // // https://json-schema.org/understanding-json-schema/reference/type.html#type -type SchemaType string - -func (t SchemaType) String() string { - return string(t) -} +type Type = Text // Types is a set of Types. A single Type marshals/unmarshals into a string // while 2+ marshals into an array. -type Types []SchemaType +type Types []Type type types Types +func (t Types) Clone() Types { + if t == nil { + return nil + } + c := make(Types, len(t)) + copy(c, t) + return c +} + // ContainsString returns true if TypeString is present func (t Types) ContainsString() bool { return t.Contains(TypeString) @@ -91,18 +96,13 @@ func (t Types) IsSingle() bool { return len(t) == 1 } -// IsEmpty returns true if len(t) == 0 -func (t SchemaType) IsEmpty() bool { - return len(t) == 0 -} - // Len returns len(t) func (t Types) Len() int { return len(t) } // Contains returns true if t contains typ -func (t Types) Contains(typ SchemaType) bool { +func (t Types) Contains(typ Type) bool { for _, v := range t { if v == typ { return true @@ -112,7 +112,7 @@ func (t Types) Contains(typ SchemaType) bool { } // Add adds typ if not present -func (t *Types) Add(typ SchemaType) Types { +func (t *Types) Add(typ Type) Types { if !t.Contains(typ) { *t = append(*t, typ) } @@ -120,7 +120,7 @@ func (t *Types) Add(typ SchemaType) Types { } // Remove removes typ if present -func (t *Types) Remove(typ SchemaType) Types { +func (t *Types) Remove(typ Type) Types { for i, v := range *t { if typ == v { copy((*t)[i:], (*t)[i+1:]) @@ -143,9 +143,8 @@ func (t Types) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals JSON func (t *Types) UnmarshalJSON(data []byte) error { - d := dynamic.JSON(data) - if d.IsString() { - var v SchemaType + if jsonx.IsString(data) { + var v Type err := json.Unmarshal(data, &v) *t = Types{v} return err diff --git a/validate.go b/validate.go deleted file mode 100644 index 92ea1a1..0000000 --- a/validate.go +++ /dev/null @@ -1,107 +0,0 @@ -package openapi - -import ( - "bytes" - "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "io/ioutil" - "log" - "path/filepath" - - "github.com/chanced/openapi/yamlutil" - "github.com/santhosh-tekuri/jsonschema/v5" - "github.com/tidwall/gjson" -) - -//go:embed schema -var schemaDir embed.FS -var schemas = map[string]io.ReadCloser{} -var jsonschemaval *jsonschema.Schema -var _ = func() error { - - if err := loadSchemas(); err != nil { - log.Fatal(err) - } - jsonschema.Loaders["https"] = loadSchema - sch, err := jsonschema.Compile("https://spec.openapis.org/oas/3.1/schema/2021-09-28") - if err != nil { - log.Fatal(err) - } - jsonschemaval = sch - return nil -}() - -func loadSchema(url string) (io.ReadCloser, error) { - s, ok := schemas[url] - if !ok { - return nil, fmt.Errorf("schema not found: %s", url) - } - return s, nil - -} - -func loadSchemas() error { - return fs.WalkDir(schemaDir, ".", func(path string, d fs.DirEntry, _ error) error { - if d.IsDir() || filepath.Ext(d.Name()) != ".json" { - return nil - } - f, err := schemaDir.Open(path) - if err != nil { - return err - } - j, err := ioutil.ReadAll(f) - if err != nil { - return err - } - g := gjson.GetBytes(j, "$id") - if !g.Exists() || len(g.String()) == 0 { - return errors.New("schema is missing $id") - } - schemas[g.String()] = ioutil.NopCloser(bytes.NewReader(j)) - return nil - }) -} - -// Validate unmarshals and validates either a single OpenAPI 3.1 specification -// or an array of OpenAPI 3.1 specifications. -// -// The input data can either be a single OpenAPI specification or an array. -func Validate(data []byte) error { - var list []map[string]interface{} - d := bytes.TrimSpace(data) - if len(d) == 0 { - return errors.New("data may not be empty") - } - switch d[0] { - case '{': - var o map[string]interface{} - if err := json.Unmarshal(data, &o); err != nil { - return err - } - list = []map[string]interface{}{o} - case '[': - if err := json.Unmarshal(data, &list); err != nil { - return err - } - default: - b, err := yamlutil.JSONToYAML(data) - if err != nil { - return err - } - return Validate(b) - } - for _, o := range list { - if err := validate(o); err != nil { - return err - } - } - return nil -} - -func validate(v map[string]interface{}) error { - return jsonschemaval.Validate(v) -} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..3a01df6 --- /dev/null +++ b/validator.go @@ -0,0 +1,498 @@ +package openapi + +import ( + "bytes" + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "log" + "path/filepath" + + "github.com/Masterminds/semver" + "github.com/chanced/uri" + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/tidwall/gjson" +) + +//go:embed schema +var embeddedSchemas embed.FS + +const ( + // URI for OpenAPI 3.1 schema + OPEN_API_3_1_SCHEMA = "https://spec.openapis.org/oas/3.1/schema/2022-02-27" + // URI for OpenAPI 3.0 schema + OPEN_API_3_0_SCHEMA = "https://spec.openapis.org/oas/3.0/schema/2021-09-28" + // URI for JSON Schema 2020-12 + JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema" + // URI for JSON Schema 2019-09 + JSON_SCHEMA_2019_09 = "https://json-schema.org/draft/2019-09/schema" +) + +var ( + // OpenAPI31Schema is the URI for the JSON Schema of OpenAPI 3.1 + OpenAPI31Schema = *uri.MustParse(OPEN_API_3_1_SCHEMA) + // OpenAPI30Schema is the URI for the JSON Schema of OpenAPI 3.0 + OpenAPI30Schema = *uri.MustParse(OPEN_API_3_0_SCHEMA) + // JSONSchema202012SchemaURI is the URI for JSON Schema 2020-12 + JSONSchemaDialect202012 = *uri.MustParse(JSON_SCHEMA_2020_12) + // JSONSchemaDialect201909 is the URI for JSON Schema 2019-09 + JSONSchemaDialect201909 = *uri.MustParse(JSON_SCHEMA_2019_09) + // // JSONSchemaDialect07 is the URI for JSON Schema 07 + // JSONSchemaDialect07 = *uri.MustParse("http://json-schema.org/draft-07/schema#") + // // JSONSchemaDialect04 is the URI for JSON Schema 04 + // JSONSchemaDialect04 = *uri.MustParse("http://json-schema.org/draft-04/schema#") + // VersionConstraints3_0 is a semantic versioning constraint for 3.0: + // >= 3.0.0, < 3.1.0 + VersionConstraints3_0 = mustParseConstraints(">= 3.0.0, < 3.1.0") + // SemanticVersion3_0 is a semantic versioning constraint for 3.1: + // >= 3.1.0, < 3.2.0 + VersionConstraints3_1 = mustParseConstraints(">= 3.1.0, < 3.2.0") + // SupportedVersions is a semantic versioning constraint for versions + // supported by openapi + // + // This is currently: + // >= 3.0.0, < 3.2.0 + SupportedVersions = mustParseConstraints(">= 3.0.0, < 3.2.0") + // Version3_1 is a semantic version for 3.1.x + Version3_1 = *semver.MustParse("3.1") + // Version3_0 is a semantic version for 3.0.x + Version3_0 = *semver.MustParse("3.0") +) + +var _ Validator = (*StdValidator)(nil) + +type Validator interface { + // Validate should validate the fully-resolved OpenAPI document. + ValidateDocument(document *Document) error + + // ValidateComponent should validate the structural integrity of a of an OpenAPI + // document or component. + // + // If $ref is present in the data and the data is not a Schema, the Kind will be KindReference. + // Otherwise, it will be the Kind of the data being loaded. + // + // openapi should only ever call Validate for the following: + // - OpenAPI Document (KindDocument) + // - JSON Schema (KindSchema) + // - Components (KindComponents) + // - Callbacks (KindCallbacks) + // - Example (KindExample) + // - Header (KindHeader) + // - Link (KindLink) + // - Parameter (KindParameter) + // - PathItem (KindPathItem) + // - Operation (KindOperation) + // - Reference (KindReference) + // - RequestBody (KindRequestBody) + // - Response (KindResponse) + // - SecurityScheme (KindSecurityScheme) + // + // StdComponentValidator will return an error if CompiledSchemas does not contain + // a CompiledSchema for the given Kind. + Validate(data []byte, resource uri.URI, kind Kind, openapi semver.Version, jsonschema uri.URI) error +} + +// NewStdValidator creates and returns a new StdValidator. +// +// compiler is used to compile JSON Schema for initial validation. + +// Each fs.FS in resources will be walked and all files ending in .json will be +// be added to the compiler. Defaults are provided from an embedded fs.FS. +// +// ## Resource Defaults +// - OpenAPI 3.1: "https://spec.openapis.org/oas/3.1/schema/2022-02-27" +// - OpenAPI 3.0: "https://spec.openapis.org/oas/3.0/schema/2021-09-28" +// - JSON Schema 2020-12: "https://json-schema.org/draft/2020-12/schema" +// - JSON Schema 2019-09: "https://json-schema.org/draft/2019-09/schema" +func NewValidator(compiler *jsonschema.Compiler, resources ...fs.FS) (*StdValidator, error) { + if compiler == nil { + return nil, errors.New("openapi: compiler is required") + } + compiled, err := CompileSchemas(compiler) + if err != nil { + return nil, fmt.Errorf("failed to compile schemas: %w", err) + } + return &StdValidator{ + Schemas: compiled, + }, nil +} + +// StdValidator is an implemtation of the Validator interface. +type StdValidator struct { + Schemas CompiledSchemas +} + +// Validate should validate the fully-resolved OpenAPI document. +// +// This currently only validates with JSON Schema. +func (sv *StdValidator) ValidateDocument(doc *Document) error { + // The openapi spec claims there are validations which json + // schema can not fully encompass. Those will need to be added here. + // TODO: Improve validation beyond JSON Schema + + d, err := doc.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal document: %w", err) + } + + dialect := doc.JSONSchemaDialect + if dialect == nil { + if VersionConstraints3_1.Check(doc.OpenAPI) { + dialect = &JSONSchemaDialect202012 + // } else if VersionConstraints3_0.Check(doc.OpenAPI) { + // dialect = &JSONSchemaDialect201909 + } else { + return fmt.Errorf("openapi: unable to detect OpenAPI version: %s", doc.OpenAPI) + } + } + if err = sv.Validate(d, doc.AbsoluteLocation(), KindDocument, *doc.OpenAPI, *dialect); err != nil { + return err + } + m := map[string]struct{}{} + + for _, r := range doc.Refs() { + u := r.URI() + if _, ok := m[r.ResolvedNode().AbsoluteLocation().String()]; ok { + continue + } else { + m[r.ResolvedNode().AbsoluteLocation().String()] = struct{}{} + } + if u.Path != "" || u.Host != "" { + if s, ok := r.ResolvedNode().(*Schema); ok { + sd := dialect + if s.Schema != nil { + sd = s.Schema + } + d, err := s.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + if err = sv.Validate(d, s.AbsoluteLocation(), KindSchema, *doc.OpenAPI, *sd); err != nil { + return err + } + } else { + d, err := r.ResolvedNode().MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal node: %w", err) + } + + if err = sv.Validate(d, r.ResolvedNode().AbsoluteLocation(), r.ResolvedNode().Kind(), *doc.OpenAPI, *dialect); err != nil { + return err + } + } + } + } + return nil +} + +func (sv *StdValidator) Validate(data []byte, resource uri.URI, kind Kind, openapi semver.Version, jsonschema uri.URI) error { + if kind == KindSchema { + schema, ok := sv.Schemas.JSONSchema[jsonschema] + if !ok { + return fmt.Errorf("openapi: no schema found for %q", jsonschema) + } + return schema.Validate(data) + } + var s CompiledSchema + var ok bool + if VersionConstraints3_1.Check(&openapi) { + s, ok = sv.Schemas.OpenAPI[Version3_1][kind] + } + + if !ok { + return fmt.Errorf("openapi: schema not found for %s", kind) + } + + var i interface{} + if err := json.Unmarshal(data, &i); err != nil { + return fmt.Errorf("failed to unmarshal data: %w", err) + } + if err := s.Validate(i); err != nil { + b, err := json.MarshalIndent(i, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal data: %w", err) + } + fmt.Println(string(b)) + return NewValidationError(err, kind, resource) + } + return nil +} + +// CompiledSchema is an interface satisfied by a JSON Schema implementation that +// validates primitive interface{} types. +// +// github.com/santhosh-tekuri/jsonschema/v5 satisfies this interface. +type CompiledSchema interface { + Validate(data interface{}) error +} + +// // Compiler is an interface satisfied by any type which manages and compiles +// // resources (received in the form of io.Reader) based off of a URIs (including +// // fragments). +// // +// // github.com/santhosh-tekuri/jsonschema/v5 satisfies this interface. +// type Compiler interface { +// AddResource(id string, r io.Reader) error +// Compile(url string) (CompiledSchema, error) +// } + +// CompiledSchemas are used in the the StdValidator +type CompiledSchemas struct { + OpenAPI map[semver.Version]map[Kind]CompiledSchema + JSONSchema map[uri.URI]CompiledSchema +} + +// SetupCompiler adds OpenAPI and JSON Schema resources to a Compiler. +// +// Each fs.FS in resources will be walked and all files ending in .json will be +// be added to the compiler. +// +// # Defaults +// - OpenAPI 3.1: "https://spec.openapis.org/oas/3.1/schema/2022-02-27" +// - OpenAPI 3.0: "https://spec.openapis.org/oas/3.0/schema/2021-09-28" +// - JSON Schema 2020-12: "https://json-schema.org/draft/2020-12/schema" +// - JSON Schema 2019-09: "https://json-schema.org/draft/2019-09/schema" +func SetupCompiler(compiler *jsonschema.Compiler, resources ...fs.FS) (*jsonschema.Compiler, error) { + if compiler == nil { + return nil, errors.New("openapi: compiler is required") + } + resources = append([]fs.FS{embeddedSchemas}, resources...) + err := addCompilerResources(compiler, resources) + if err != nil { + return nil, fmt.Errorf("failed to add resources to compiler: %w", err) + } + return compiler, nil +} + +// addCompilerResources adds the following schemas to a compiler: +// - OpenAPI 3.1 ("https://spec.openapis.org/oas/3.1/schema/2022-02-27)") +// - OpenAPI 3.0 ("https://spec.openapis.org/oas/3.0/schema/2021-09-28") +// - JSON Schema 2020-12 +// - JSON Schema 2019-09 +func addCompilerResources(compiler *jsonschema.Compiler, dirs []fs.FS) error { + var err error + for _, dir := range dirs { + err = fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if filepath.Ext(path) != ".json" { + return nil + } + f, err := dir.Open(path) + if err != nil { + return nil + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return err + } + + id := gjson.GetBytes(b, "$id").String() + if id == "" { + id = gjson.GetBytes(b, "id").String() + } + if id == "" { + return fmt.Errorf("openapi: $id, id not found in %s", path) + } + err = compiler.AddResource(id, bytes.NewReader(b)) + return err + }) + if err != nil { + return err + } + } + return nil +} + +func openAPISchemaVMap(openAPISchemas []map[string]uri.URI) (map[semver.Version]uri.URI, error) { + res := make(map[semver.Version]uri.URI, 2) + for _, vers := range openAPISchemas { + for k, v := range vers { + vers, err := semver.NewVersion(k) + if err != nil { + return nil, fmt.Errorf("failed to parse openAPISchemaID version: %w", err) + } + + if _, errs := SupportedVersions.Validate(vers); err != nil { + return nil, &UnsupportedVersionError{Version: k, Errs: errs} + } + + k = fmt.Sprintf("%d.%d", vers.Major(), vers.Minor()) + vers, _ = semver.NewVersion(k) + res[*vers] = v + } + } + if _, ok := res[Version3_1]; !ok { + res[Version3_1] = OpenAPI31Schema + } + if _, ok := res[Version3_0]; !ok { + res[Version3_0] = OpenAPI30Schema + } + return res, nil +} + +// CompileSchemas compiles the OpenAPI and JSON Schema resources using compiler. +// +// openAPISchemas is a variadic map of OpenAPI versions to their +// respective schema ids. The keys must be valid semver versions; only the major +// and minor versions are used. The last value for a given major and minor will be +// used +// +// Default openAPISchemas: +// +// { "3.1": "https://spec.openapis.org/oas/3.1/schema/2022-02-27)" } +// { "3.0": "https://spec.openapis.org/oas/3.0/schema/2021-09-28" } +func CompileSchemas(compiler *jsonschema.Compiler, openAPISchemas ...map[string]uri.URI) (CompiledSchemas, error) { + var err error + + openapis, err := compileOpenAPISchemas(compiler, openAPISchemas) + if err != nil { + return CompiledSchemas{}, fmt.Errorf("openapi: failed to compile openAPISchemas: %w", err) + } + jsonschemas, err := compileJSONSchemaSchemas(compiler) + if err != nil { + return CompiledSchemas{}, fmt.Errorf("openapi: failed to compile jsonschema schemas: %w", err) + } + return CompiledSchemas{ + OpenAPI: openapis, + JSONSchema: jsonschemas, + }, nil +} + +func compileJSONSchemaSchemas(c *jsonschema.Compiler) (map[uri.URI]CompiledSchema, error) { + var err error + jsonschemas := make(map[uri.URI]CompiledSchema, 2) + jsonschemas[JSONSchemaDialect202012], err = c.Compile(JSON_SCHEMA_2020_12) + if err != nil { + return nil, err + } + jsonschemas[JSONSchemaDialect201909], err = c.Compile(JSON_SCHEMA_2019_09) + if err != nil { + return nil, err + } + return jsonschemas, nil +} + +func compileOpenAPISchemas(c *jsonschema.Compiler, openAPISchemas []map[string]uri.URI) (map[semver.Version]map[Kind]CompiledSchema, error) { + vm, err := openAPISchemaVMap(openAPISchemas) + if err != nil { + return nil, err + } + compiled := make(map[semver.Version]map[Kind]CompiledSchema, len(vm)) + + for k, v := range vm { + compiled[k], err = compileOpenAPISchemasFor(c, v) + if err != nil { + return nil, fmt.Errorf("openapi: failed to compile OpenAPI %s Schema %s: %w", k.String(), v.String(), err) + } + } + return compiled, nil +} + +func compileOpenAPISchemasFor(compiler *jsonschema.Compiler, uri uri.URI) (map[Kind]CompiledSchema, error) { + uri.Fragment = "" + uri.RawFragment = "" + spec := uri.String() + compileDef := func(name string) (CompiledSchema, error) { + if uri.String() == "https://spec.openapis.org/oas/3.0/schema/2021-09-28" { + if name == "callbacks" { + name = "paths" + } + return compiler.Compile(spec + "#/definitions/" + Text(name).ToCamel().String()) + } else { + return compiler.Compile(spec + "#/$defs/" + name) + } + } + + document, err := compiler.Compile(spec) + if err != nil { + return nil, fmt.Errorf("error compiling Dcument schema: %w", err) + } + + operation, err := compileDef("operation") + if err != nil { + return nil, fmt.Errorf("error compiling Operation schema: %w", err) + } + + callbacks, err := compileDef("callbacks") + if err != nil { + return nil, fmt.Errorf("error compiling Callbacks schema: %w", err) + } + + example, err := compileDef("example") + if err != nil { + return nil, fmt.Errorf("error compiling Example schema: %w", err) + } + + header, err := compileDef("header") + if err != nil { + return nil, fmt.Errorf("error compiling Header schema: %w", err) + } + + link, err := compileDef("link") + if err != nil { + return nil, fmt.Errorf("error compiling Link schema: %w", err) + } + + parameter, err := compileDef("parameter") + if err != nil { + return nil, fmt.Errorf("error compiling Parameter schema: %w", err) + } + + requestBody, err := compileDef("request-body") + if err != nil { + return nil, fmt.Errorf("error compiling RequestBody schema: %w", err) + } + + pathItem, err := compileDef("path-item") + if err != nil { + return nil, fmt.Errorf("error compiling PathItem schema: %w", err) + } + + response, err := compileDef("response") + if err != nil { + return nil, fmt.Errorf("error compiling Response schema: %w", err) + } + + securityScheme, err := compileDef("security-scheme") + if err != nil { + return nil, fmt.Errorf("error compiling SecurityScheme schema: %w", err) + } + reference, err := compileDef("reference") + if err != nil { + return nil, fmt.Errorf("error compiling Reference schema: %w", err) + } + + o := map[Kind]CompiledSchema{ + KindDocument: document, + KindOperation: operation, + KindCallbacks: callbacks, + KindExample: example, + KindHeader: header, + KindLink: link, + KindParameter: parameter, + KindRequestBody: requestBody, + KindResponse: response, + KindSecurityScheme: securityScheme, + KindPathItem: pathItem, + KindReference: reference, + } + return o, nil +} + +func mustParseConstraints(str string) semver.Constraints { + c, err := semver.NewConstraint(str) + if err != nil { + log.Fatalf("failed to parse semver constraint %s: %v", str, err) + } + return *c +} diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..93b2b2a --- /dev/null +++ b/validator_test.go @@ -0,0 +1,98 @@ +package openapi_test + +import ( + "context" + "errors" + "io" + "io/fs" + "strings" + "testing" + + "github.com/chanced/openapi" + "github.com/chanced/uri" + "github.com/santhosh-tekuri/jsonschema/v5" +) + +func TestValidation(t *testing.T) { + ctx := context.Background() + + c, err := openapi.SetupCompiler(jsonschema.NewCompiler()) // adding schema files + if err != nil { + t.Fatal(err) + } + v, err := openapi.NewValidator(c) + if err != nil { + t.Fatal(err) + } + + // you can Load either JSON or YAML + err = fs.WalkDir(testdata, "testdata/documents/validation/pass", func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + t.Run(strings.TrimPrefix(p, "testdata/documents/validation/"), func(t *testing.T) { + f, err := testdata.Open(p) + if err != nil { + t.Fatal(err) + } + fn := func(_ context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error) { + d, err := io.ReadAll(f) + if err != nil { + return 0, nil, err + } + return openapi.KindDocument, d, nil + } + doc, err := openapi.Load(ctx, p, v, fn) + if err != nil { + t.Errorf("failed to load document: %v", err) + } + + err = v.ValidateDocument(doc) + if err != nil { + t.Errorf("failed to validate document: %v", err) + } + }) + return nil + }) + if err != nil { + t.Errorf("expected document to be valid, received: %v", err) + } + // you can Load either JSON or YAML + err = fs.WalkDir(testdata, "testdata/documents/validation/fail", func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + t.Run(strings.TrimPrefix(p, "testdata/documents/validation/"), func(t *testing.T) { + f, err := testdata.Open(p) + if err != nil { + t.Fatal(err) + } + fn := func(_ context.Context, uri uri.URI, kind openapi.Kind) (openapi.Kind, []byte, error) { + d, err := io.ReadAll(f) + if err != nil { + return 0, nil, err + } + return openapi.KindDocument, d, nil + } + _, err = openapi.Load(ctx, p, v, fn) + if err == nil { + t.Errorf("expected document to be invalid, received: %v", err) + } + + var ve *openapi.ValidationError + if !errors.As(err, &ve) { + t.Errorf("expected document to be invalid, received: %v", err) + } + }) + return nil + }) + if err != nil { + t.Errorf("expected document to be valid, received: %v", err) + } +} diff --git a/visitor.go b/visitor.go new file mode 100644 index 0000000..183654a --- /dev/null +++ b/visitor.go @@ -0,0 +1,111 @@ +package openapi + +// type Walker interface { +// Walk(v Visitor) error +// } + +// type Visitor interface { +// Visit(n Node) (Visitor, error) + +// VisitDocument(node *Document) (Visitor, error) + +// VisitCallbacks(node *Callbacks) (Visitor, error) + +// VisitComponents(node *Components) (Visitor, error) + +// VisitCallbacksMap(node *CallbacksMap) (Visitor, error) + +// VisitContact(node *Contact) (Visitor, error) + +// VisitDiscriminator(node *Discriminator) (Visitor, error) + +// VisitEncoding(node *Encoding) (Visitor, error) + +// VisitEncodingMap(node *EncodingMap) (Visitor, error) + +// VisitExample(node *Example) (Visitor, error) + +// VisitExternalDocs(node *ExternalDocs) (Visitor, error) + +// VisitHeader(node *Header) (Visitor, error) + +// VisitHeaderMap(node *HeaderMap) (Visitor, error) + +// VisitInfo(node *Info) (Visitor, error) + +// VisitLicense(node *License) (Visitor, error) + +// VistLink(node *Link) (Visitor, error) + +// VistMediaType(node *MediaType) (Visitor, error) + +// VisitOAuthFlows(node *OAuthFlows) (Visitor, error) + +// VisitOAuthFlow(node *OAuthFlow) (Visitor, error) + +// VisitOperation(node *Operation) (Visitor, error) + +// VisitOperationItem(node *OperationItem) (Visitor, error) + +// VisitOperationRef(node *OperationRef) (Visitor, error) + +// VisitParameter(node *Parameter) (Visitor, error) + +// VisitParameterSlice(node *ParameterSlice) (Visitor, error) + +// VisitParameterMap(node *ParameterMap) (Visitor, error) + +// VisitPathItemComponent(node *Component[*PathItem]) (Visitor, error) + +// VisitPathItem(node *PathItem) (Visitor, error) + +// // A map of PathItems may not contain references +// VisitPathItemObjs(node *PathItemObjs) (Visitor, error) + +// // Visits a a map of PathItems which may contain references +// VisitPathItemMap(node *PathItemMap) (Visitor, error) + +// VisitPaths(node *Paths) (Visitor, error) + +// VisitReference(node *Reference) (Visitor, error) + +// VisitRequestBody(node *RequestBody) (Visitor, error) + +// VisitRequestBodyMap(node *RequestBodyMap) (Visitor, error) + +// VisitResponse(node *Response) (Visitor, error) + +// VisitResponseMap(node *ResponseMap) (Visitor, error) + +// VisitSchema(node *Schema) (Visitor, error) + +// VisitSchemaMap(node *SchemaMap) (Visitor, error) + +// VisitSchemaSlice(node *SchemaSlice) (Visitor, error) + +// VisitSchemaRef(node *SchemaRef) (Visitor, error) + +// VisitScope(node *Scope) (Visitor, error) + +// VisitSecurityRequirement(node *SecurityRequirement) (Visitor, error) + +// VisitSecurityScheme(node *SecurityScheme) (Visitor, error) + +// VisitSecuritySchemeMap(node *SecuritySchemeMap) (Visitor, error) + +// VisitServer(node *Server) (Visitor, error) + +// VisitServerSlice(node *ServerSlice) (Visitor, error) + +// VisitServerVariable(node *ServerVariable) (Visitor, error) + +// VisitServerVariableMap(node *ServerVariableMap) (Visitor, error) + +// VisitTagSlice(node *TagSlice) (Visitor, error) + +// VisitTag(node *Tag) (Visitor, error) + +// VisitXML(node *XML) (Visitor, error) +// } + +type BaseVisitor struct{} diff --git a/xml.go b/xml.go index 4ccaa82..f1d0401 100644 --- a/xml.go +++ b/xml.go @@ -1,5 +1,12 @@ package openapi +import ( + "encoding/json" + + "github.com/chanced/transcode" + "gopkg.in/yaml.v3" +) + // XML is a metadata object that allows for more fine-tuned XML model // definitions. // @@ -7,24 +14,124 @@ package openapi // forms) and the name property SHOULD be used to add that information. See // examples for expected behavior. type XML struct { + Extensions `json:"-"` + Location `json:"-"` + // Replaces the name of the element/attribute used for the described schema // property. When defined within items, it will affect the name of the // individual XML elements within the list. When defined alongside type // being array (outside the items), it will affect the wrapping element and // only if wrapped is true. If wrapped is false, it will be ignored. - Name string `json:"name,omitempty"` + Name Text `json:"name,omitempty"` // The URI of the namespace definition. This MUST be in the form of an // absolute URI. - Namespace string `json:"namespace,omitempty"` + Namespace Text `json:"namespace,omitempty"` // The prefix to be used for the name. - Prefix string `json:"prefix,omitempty"` + Prefix Text `json:"prefix,omitempty"` // Declares whether the property definition translates to an attribute // instead of an element. Default value is false. - Attribute bool `json:"attribute,omitempty"` + Attribute *bool `json:"attribute,omitempty"` // MAY be used only for an array definition. Signifies whether the array is // wrapped (for example, ) or unwrapped // (). Default value is false. The definition takes effect // only when defined alongside type being array (outside the items). - Wrapped bool `json:"wrapped,omitempty"` - Extensions `json:"-"` + Wrapped *bool `json:"wrapped,omitempty"` +} + +func (xml *XML) Clone() *XML { + if xml == nil { + return nil + } + var a *bool + if xml.Attribute != nil { + *a = *xml.Attribute + } + var w *bool + if xml.Wrapped != nil { + *w = *xml.Wrapped + } + return &XML{ + Extensions: cloneExtensions(xml.Extensions), + Location: xml.Location, + Name: xml.Name.Clone(), + Namespace: xml.Namespace.Clone(), + Prefix: xml.Prefix.Clone(), + Attribute: a, + Wrapped: w, + } +} + +func (*XML) Anchors() (*Anchors, error) { return nil, nil } + +// func (x *XML) ResolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if err := ptr.Validate(); err != nil { +// return nil, err +// } +// return x.resolveNodeByPointer(ptr) +// } + +// func (x *XML) resolveNodeByPointer(ptr jsonpointer.Pointer) (Node, error) { +// if ptr.IsRoot() { +// return x, nil +// } +// tok, _ := ptr.NextToken() +// return nil, newErrNotResolvable(x.Location.AbsoluteLocation(), tok) +// } + +func (*XML) Kind() Kind { return KindXML } +func (*XML) mapKind() Kind { return KindUndefined } +func (*XML) sliceKind() Kind { return KindUndefined } + +func (x XML) MarshalJSON() ([]byte, error) { + type xml XML + return marshalExtendedJSON(xml(x)) +} + +func (x *XML) UnmarshalJSON(data []byte) error { + type xml XML + var v xml + err := unmarshalExtendedJSON(data, &v) + if err != nil { + return err + } + *x = XML(v) + return nil +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Marshaler interface +func (xml XML) MarshalYAML() (interface{}, error) { + j, err := xml.MarshalJSON() + if err != nil { + return nil, err + } + return transcode.YAMLFromJSON(j) +} + +// UnmarshalYAML satisfies gopkg.in/yaml.v3 Unmarshaler interface +func (xml *XML) UnmarshalYAML(value *yaml.Node) error { + j, err := transcode.YAMLFromJSON([]byte(value.Value)) + if err != nil { + return err + } + return json.Unmarshal(j, xml) +} + +func (xml *XML) setLocation(loc Location) error { + if xml == nil { + return nil + } + xml.Location = loc + return nil } + +func (xml *XML) isNil() bool { return xml == nil } +func (xml *XML) Refs() []Ref { return nil } +func (xml *XML) Nodes() []Node { + if xml == nil { + return nil + } + return downcastNodes(xml.nodes()) +} +func (xml *XML) nodes() []node { return nil } + +var _ node = (*XML)(nil) diff --git a/yamlutil/yamlutil.go b/yamlutil/yamlutil.go deleted file mode 100644 index fd7134f..0000000 --- a/yamlutil/yamlutil.go +++ /dev/null @@ -1,108 +0,0 @@ -package yamlutil - -import ( - "encoding/json" - - "github.com/chanced/dynamic" - "sigs.k8s.io/yaml" -) - -// Unmarshal unmarshals in from YAML by marshaling and unmarshaling to json -func Unmarshal(unmarshal func(in interface{}) error, out interface{}) error { - var i interface{} - if err := unmarshal(&i); err != nil { - return err - } - v, err := subset(i) - if err != nil { - return err - } - b, err := json.Marshal(v) - if err != nil { - return err - } - jb, err := YAMLToJSON(b) - - if err != nil { - return err - } - return json.Unmarshal(jb, out) -} - -// YAMLToJSON converts YAML to JSON -func YAMLToJSON(data []byte) ([]byte, error) { - // found sigs.k8s.io/yaml which does a great job of converting this over so - // I'm just going to use it instead of what I had. - // TODO: Re-implement YAMLToJSON as sigs/yaml doesn't handle big numbers - return yaml.YAMLToJSON(data) -} - -// Marshal returns an interface{} representation of src to be marshaled into -// YAML -func Marshal(src interface{}) (interface{}, error) { - b, err := json.Marshal(src) - if err != nil { - return nil, err - } - var v interface{} - err = json.Unmarshal(b, &v) - return v, err -} - -// JSONToYAML converts JSON to YAML. -func JSONToYAML(data []byte) ([]byte, error) { - return yaml.JSONToYAML(data) -} - -func subset(u interface{}) (interface{}, error) { - switch t := u.(type) { - case []interface{}: - return subsetSlice(t) - case map[interface{}]interface{}: - return subsetObj(t) - case map[string]interface{}: - return subsetMap(t) - default: - return t, nil - } -} - -func subsetSlice(t []interface{}) ([]interface{}, error) { - res := make([]interface{}, len(t)) - for i, v := range t { - iv, err := subset(v) - if err != nil { - return nil, err - } - res[i] = iv - } - return res, nil -} -func subsetMap(t map[string]interface{}) (map[string]interface{}, error) { - // this should be okay but going to check it regardless. - res := make(map[string]interface{}, len(t)) - for k, v := range t { - cv, err := subset(v) - if err != nil { - return nil, err - } - res[k] = cv - } - return res, nil -} -func subsetObj(t map[interface{}]interface{}) (map[string]interface{}, error) { - res := make(map[string]interface{}, len(t)) - for k, v := range t { - ks := new(dynamic.String) - err := ks.Set(k) - if err != nil { - return nil, err - } - cv, err := subset(v) - if err != nil { - return nil, err - } - res[ks.String()] = cv - } - return res, nil -}