Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Work towards OpenAPI 3.1 support #22

Draft
wants to merge 53 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0cb50b5
Add 3.1 to list of supported versions
kevindew Dec 24, 2021
58256ed
Put v3.0 examples into a specific directory
kevindew Dec 24, 2021
9ec9260
Add some 3.1 integration tests
kevindew Dec 24, 2021
b97bc98
Add #openapi_version methods to context classes
kevindew Mar 15, 2022
aee5278
Add OpenapiVersion class for version comparisons
kevindew Mar 15, 2022
dca077d
Required option in field config accepts lambda
kevindew Mar 15, 2022
8109b18
Only require paths for OpenAPI < 3.1
kevindew Mar 15, 2022
12bd103
Add a webhooks field to the root OpenAPI node
kevindew Mar 15, 2022
c3d6e9b
Fields have a concept of allowed
kevindew Mar 16, 2022
a0f747c
Only allow webhooks on OpenAPI >= 3.1 documents
kevindew Mar 16, 2022
11994dc
Start an OpenAPI v3.1 checklist
kevindew Mar 16, 2022
93cab13
Don't require responses on an Operation in OpenAPI 3.1
kevindew Mar 16, 2022
ecd50e0
Require at least one of components, paths and webhooks
kevindew Mar 16, 2022
c94e21c
Add summary field to Info for OpenAPI v3.1
kevindew Mar 16, 2022
5c2d043
Fix typo on "at least"
kevindew Mar 16, 2022
16a9806
Put together notes on OpenAPI 3.1
kevindew Mar 16, 2022
a96a733
Breaking: Create OpenAPI version specific Schema class
kevindew Mar 20, 2022
734b005
Create an object for the OAS Dialect 3.1 schema
kevindew Mar 20, 2022
08e1a5b
Make extension regex configurable
kevindew Mar 20, 2022
24b4264
Refactor reference resolution for OpenAPI 3.1
kevindew Apr 10, 2022
79df56c
Mark Node::Context as a long class
kevindew Apr 10, 2022
4f213a1
Allow 3.1 references to have summary and description
kevindew May 1, 2022
13854eb
Add identifier, as a mutually exclusive field, to License
kevindew May 1, 2022
995b5ae
No changes needed for ServerVariable
kevindew Jul 8, 2022
8b4c4a0
Add pathItems to components for OpenAPI 3.1
kevindew Jul 8, 2022
4711f4c
Mark mutualTLS as resolved
kevindew Jul 8, 2022
145cef3
Version specific allow_extensions for Discriminator
kevindew Jul 8, 2022
c6b9618
Fix rubocop issues from rebase
kevindew Jan 9, 2025
4c60fbe
Add explicit require for node_factory
kevindew Jan 9, 2025
ab44778
Share behaviour between schema objects
kevindew Jan 10, 2025
fe52a9a
Configure type field for OpenAPI 3.1
kevindew Jan 16, 2025
882ab1a
Add const field to OpenAPI 3.1 schema
kevindew Jan 16, 2025
43d197b
Add a few basic JSON schema fields
kevindew Jan 16, 2025
e8163b2
Update fields we're tracking for Schema in 3.1
kevindew Jan 16, 2025
d7f2f81
Add content* fields for v3.1 schema object
kevindew Jan 17, 2025
70b10af
Add definitions for if, then and else schema fields
kevindew Jan 17, 2025
a35b54b
Add prefixItems field to Schema
kevindew Jan 27, 2025
af96354
Contains field for schema
kevindew Jan 27, 2025
123b76b
Correct handling of minimum and maximum of schema
kevindew Jan 27, 2025
ed007b9
Add patternProperties to Schema
kevindew Jan 27, 2025
8733278
Add some notes about boolean schemas
kevindew Jan 27, 2025
3ed1a7d
Add dependentRequired field for 3.1 schema
kevindew Jan 29, 2025
2f11358
Add DependentSchemas schema field
kevindew Jan 29, 2025
f75b2d6
Check explicitly for ::Hash rather than respond_to?(:[])
kevindew Jan 29, 2025
6d16da6
Support JsonSchemas that are a boolean value
kevindew Jan 29, 2025
8a52e58
Add some dubious hacks to get appropriate errors
kevindew Jan 29, 2025
07dabbf
Move additionalProperties to 3.0 Schema node
kevindew Jan 30, 2025
f43d88e
Add methods for boolean schemas
kevindew Jan 30, 2025
b12715f
Add support for additionalProperties, unevaluatedItems and unevaluate…
kevindew Feb 3, 2025
53fae01
Add output of warnings to Kernel#warn
kevindew Feb 14, 2025
2215772
Rename Validators::Url to Validators::Uri
kevindew Feb 21, 2025
a4d6dbb
Add jsonSchemaDialect field to openapi node
kevindew Feb 21, 2025
0292aff
JSON schema dialect warnings
kevindew Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,25 @@ These are the steps defined to reach 1.0. Assistance is very welcome.
- [ ] Support validating a Server URL based on default values
- [ ] Validate paths to check path parameters within them appear in paths
see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#fixed-fields-10

For OpenAPI 3.1

- [x] Conditional nodes
- [x] Support webhooks
- [x] No longer require responses field on an Operation node
- [x] Require OpenAPI node to have webhooks, paths or components
- [x] Support the switch to a fixed schema dialect
- [x] Support summary field on Info node
- [ ] Create a maxi OpenAPI 3.1 integration test to collate all the known changes
- [x] jsonSchemaDialect should default to OAS one
- [x] Allow summary and description in Reference objects
- [x] Add identifier to License node, make mutually exclusive with URL
- [x] ServerVariable enum must not be empty
- [x] Add pathItems to components
- [ ] Callbacks can now reference a PathItem - previously required them
- [ ] Check out whether pathItem references match the rules for relative resolution
- [ ] Parameter object can have space delimited or pipeDelimited styles
- [x] Discriminator object can be extended
- [x] mutualTLS as a security scheme
- [ ] I think strictness of Security Requirement rules has changed

118 changes: 118 additions & 0 deletions json-schema-for-3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# JSON schema with 3.1

Temporary document to be removed with the merge of support for OpenAPI 3.1

Things have got complex with schemas in OpenAPI 3.1

How things might work:

- when a schema factory is created, it determines whether the dialect is supported
- it then creates a factory based on the dialect
- if there is a reference in it this is resolved
- there could be complexities in the resolving process because of the id field - does it become relative to this?
- skip dynamicAnchor and dynamicRef for now - they are quite complex: https://stackoverflow.com/questions/69728686/explanation-of-dynamicref-dynamicanchor-in-json-schema-as-opposed-to-ref-and
- lets allow extra properties for schema since it's complex
- there's all the $defs stuff but this might just work as being a type of reference - presumably not used in OpenAPI anyway really

So how might we start:

- Perhaps add a class method to Schema which can identify which Schema factory is used: a OAS 3.1 one, an optionally referenced OAS 3.0 one, or non optional reference (if such a need exists), based on context. Error if given an unexpected dialect
- Learn whether you have to care about $id for resolving
- Create a node factory for OAS 3.1 Schema:
- allow arbitrary fields perhaps? Probably not needed, just a pain to keep up with JsonSchema
- load a merged reference
- perhaps have context support a merge concept for source location
- Think about dealing with recursive defined as "#"

Dealing with the new JSON Schema approach for OpenAPI 3.1.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about piggyback on existing implementation like https://github.com/voxpupuli/json-schema?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit rusty in memory but there's some tricky challenges as I recall. If I remember right, that gem doesn't support a version of JSON schema as new as what OpenAPI supports and I have recollections that it's more useful for validation than it is for traversal.


There is some meta fields:

$ref - in 3.0
$dynamicRef
$defs
$schema
$id
$comment
$anchor
$dynamicAnchor

Then a ton of fields:

type: string - in 3.0
enum: array - in 3.0
const: any type - done
multipleOf: number - in 3.0
maximum: number - in 3.0
exclusiveMaximum: number - done
minimum: number - in 3.0
exclusiveMinimum: number - done
maxLength: integer >= 0 - in 3.0 (missing >= val)
minLength: integer >= 0 - in 3.0
pattern: string - in 3.0
maxItems: integer >= 0 - in 3.0
minItems: integer >= 0 - in 3.0
uniqueItems: boolean - in 3.0
maxContains: integer >= 0 - done
minContains: integer >= 0 - done
maxProperties: integer >= 0 - in 3.0
minProperties: integer >= 0 - in 3.0
required: array, strings, unique - in 3.0 (missing unique)
dependentRequired: something complex done
contentEncoding: string - done
contentMediaType: string / media type - done
contentSchema: schema - done
title: string - in 3.0
description: string - in 3.0
default: any - in 3.0
deprecated: boolean (default false) - in 3.0
readOnly: boolean (default false) - in 3.0
writeOnly: boolean (default false) - in 3.0
examples: array - done
format: any - in 3.0

allOf - non empty array of schemas - in 3.0
anyOf - non empty array of schemas - in 3.0
oneOf - non empty array of schemas - in 3.0
not - schema - in 3.0

if - single schema - done
then - single schema - done
else - single schema - done
dependentSchemas - map of schemas - done

prefixItems: array of schema - done
items: schema - in 3.0
contains: schema - done

properties: object, each value json schema - in 3.0
patternProperties: object each value JSON schema key regex - done
additionalProperties: single json schema - done

unevaluatedItems - single schema - done
unevaluatedProperties: single schema - done


## Returning to this in 2025

Assumption: it'll be extremely rare for usage of the advanced schema fields like dynamicRefs and dynamicAnchors, let's see what we can implement that meets most use cases and hopefully doesn't crash on complex ones

Current idea is create a Schema::Common which can share methods between both schema objects that are shared, then add distinctions for differences

At point of shutting down on 10th January 2025 I was wondering about how schemas merge. I also decided to defer thinking about referenceable node object factory.

I learnt that merging seems largely undefined in JSON Schema, as far as I can tell and I'm just going with a strategy of most recent field wins.

I've set up a Node::Schema class for common schema methods and Node::Schema::v3_0 and v3_1Up classes for specific changes. Need to flesh out
tests and then behaviour that differs between them.

Little things:
- schema integer fields generally are required to be non-negative
- quite common for arrays to be invalid if not unique (required, type)
- probably want a quick way to get coverage of the methods on nodes
- could validate that pattern and patternProperties contain regexs

JSON Schema specs:

meta: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00
validation: https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00
30 changes: 21 additions & 9 deletions lib/openapi3_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,44 @@ module Openapi3Parser
# For a variety of inputs this will construct an OpenAPI document. For a
# String/File input it will try to determine if the input is JSON or YAML.
#
# @param [String, Hash, File] input Source for the OpenAPI document
# @param [String, Hash, File] input Source for the OpenAPI document
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
# warnings are output, best set to
# false when parsing specification
# files you've not authored
#
# @return [Document]
def self.load(input)
Document.new(SourceInput::Raw.new(input))
def self.load(input, emit_warnings: true)
Document.new(SourceInput::Raw.new(input), emit_warnings:)
end

# For a given string filename this will read the file and parse it as an
# OpenAPI document. It will try detect automatically whether the contents
# are JSON or YAML.
#
# @param [String] path Filename of the OpenAPI document
# @param [String] path Filename of the OpenAPI document
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
# warnings are output, best set to
# false when parsing specification
# files you've not authored
#
# @return [Document]
def self.load_file(path)
Document.new(SourceInput::File.new(path))
def self.load_file(path, emit_warnings: true)
Document.new(SourceInput::File.new(path), emit_warnings:)
end

# For a given string URL this will request the resource and parse it as an
# OpenAPI document. It will try detect automatically whether the contents
# are JSON or YAML.
#
# @param [String] url URL of the OpenAPI document
# @param [String] url URL of the OpenAPI document
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
# warnings are output, best set to
# false when parsing specification
# files you've not authored
#
# @return [Document]
def self.load_url(url)
Document.new(SourceInput::Url.new(url.to_s))
def self.load_url(url, emit_warnings: true)
Document.new(SourceInput::Url.new(url.to_s), emit_warnings:)
end
end
77 changes: 55 additions & 22 deletions lib/openapi3_parser/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ module Openapi3Parser
# Document is the root construct of a created OpenAPI Document and can be
# used to navigate the contents of a document or to check it's validity.
#
# @attr_reader [String] openapi_version
# @attr_reader [Source] root_source
# @attr_reader [Array<String>] warnings
# @attr_reader [OpenapiVersion] openapi_version
# @attr_reader [Source] root_source
# @attr_reader [Array<String>] warnings
# @attr_reader [Boolean] emit_warnings
# rubocop:disable Metrics/ClassLength
class Document
extend Forwardable
include Enumerable

attr_reader :openapi_version, :root_source, :warnings
attr_reader :openapi_version, :root_source, :emit_warnings

# A collection of the openapi versions that are supported
SUPPORTED_OPENAPI_VERSIONS = %w[3.0].freeze
SUPPORTED_OPENAPI_VERSIONS = %w[3.0 3.1].freeze

# The version of OpenAPI that will be used by default for
# validation/construction
Expand All @@ -37,6 +39,10 @@ class Document
# The value of the info field on the OpenAPI document
# @see Node::Openapi#info
# @return [Node::Info]
# @!method jsonSchemaDialect
# The value of the jsonSchemaDialect field on the OpenAPI document
# @see Node::Openapi#json_schema_dialect
# @return [String, nil]
# @!method servers
# The value of the servers field on the OpenAPI document
# @see Node::Openapi#servers
Expand Down Expand Up @@ -74,15 +80,21 @@ class Document
# Iterate through the attributes of the root object
# @!method keys
# Access keys of the root object
def_delegators :root, :openapi, :info, :servers, :paths, :components,
:security, :tags, :external_docs, :extension, :[], :each,
:keys

# @param [SourceInput] source_input
def initialize(source_input)
def_delegators :root, :openapi, :info, :json_schema_dialect, :servers,
:paths, :components, :security, :tags, :external_docs,
:extension, :[], :each, :keys

# @param [SourceInput] source_input
# @param [Boolean] emit_warnings Whether to call Kernel.warn when
# warnings are output, best set to
# false when parsing specification
# files you've not authored
def initialize(source_input, emit_warnings: true)
@reference_registry = ReferenceRegistry.new
@root_source = Source.new(source_input, self, reference_registry)
@warnings = []
@emit_warnings = emit_warnings
@build_warnings = []
@unsupported_schema_dialects = Set.new
@openapi_version = determine_openapi_version(root_source.data["openapi"])
@build_in_progress = false
@built = false
Expand Down Expand Up @@ -152,15 +164,35 @@ def node_at(pointer, relative_to = nil)
look_up_pointer(pointer, relative_to, root)
end

# An array of any warnings enountered in the initialisation / validation
# of the document. Reflects warnings related to this gems ability to parse
# the document.
#
# @return [Array<String>]
def warnings
@warnings ||= begin
factory.errors # ensure factory has completed validation
@build_warnings.freeze
end
end

# @return [String]
def inspect
%{#{self.class.name}(openapi_version: #{openapi_version}, } +
%{root_source: #{root_source.inspect})}
end

#  :nodoc:
def unsupported_schema_dialect(schema_dialect)
return if @build_warnings.frozen? || unsupported_schema_dialects.include?(schema_dialect)

unsupported_schema_dialects << schema_dialect
add_warning("Unsupported schema dialect (#{schema_dialect}), it may not parse or validate correctly.")
end

private

attr_reader :reference_registry, :built, :build_in_progress
attr_reader :reference_registry, :built, :build_in_progress, :unsupported_schema_dialects, :build_warnings

def look_up_pointer(pointer, relative_pointer, subject)
merged_pointer = Source::Pointer.merge_pointers(relative_pointer,
Expand All @@ -169,7 +201,8 @@ def look_up_pointer(pointer, relative_pointer, subject)
end

def add_warning(text)
@warnings << text
warn("Warning: #{text} Disable these warnings by opening a document with emit_warnings: false.") if emit_warnings
@build_warnings << text
end

def build
Expand All @@ -179,29 +212,28 @@ def build
context = NodeFactory::Context.root(root_source.data, root_source)
@factory = NodeFactory::Openapi.new(context)
reference_registry.freeze
@warnings.freeze
@build_in_progress = false
@built = true
end

def determine_openapi_version(version)
minor_version = (version || "").split(".").first(2).join(".")

if SUPPORTED_OPENAPI_VERSIONS.include?(minor_version)
minor_version
elsif version
return OpenapiVersion.new(minor_version) if SUPPORTED_OPENAPI_VERSIONS.include?(minor_version)

if version
add_warning(
"Unsupported OpenAPI version (#{version}), treating as a " \
"#{DEFAULT_OPENAPI_VERSION} document"
"#{DEFAULT_OPENAPI_VERSION} document."
)
DEFAULT_OPENAPI_VERSION
else
add_warning(
"Unspecified OpenAPI version, treating as a " \
"#{DEFAULT_OPENAPI_VERSION} document"
"#{DEFAULT_OPENAPI_VERSION} document."
)
DEFAULT_OPENAPI_VERSION
end

OpenapiVersion.new(DEFAULT_OPENAPI_VERSION)
end

def factory
Expand All @@ -214,4 +246,5 @@ def reference_factories
reference_registry.factories.reject { |f| f.context.source.root? }
end
end
# rubocop:enable Metrics/ClassLength
end
2 changes: 1 addition & 1 deletion lib/openapi3_parser/node/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def each(&)
# @return [Boolean]
def ==(other)
other.instance_of?(self.class) &&
node_context.same_data_and_source?(other.node_context)
node_context.same_data_inputs?(other.node_context)
end

# Used to access a node relative to this node
Expand Down
Loading