├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── LICENCE ├── README.md ├── Rakefile ├── TODO.md ├── lib ├── openapi3_parser.rb └── openapi3_parser │ ├── array_sentence.rb │ ├── cautious_dig.rb │ ├── document.rb │ ├── document │ └── reference_registry.rb │ ├── error.rb │ ├── markdown.rb │ ├── node │ ├── array.rb │ ├── callback.rb │ ├── components.rb │ ├── contact.rb │ ├── context.rb │ ├── discriminator.rb │ ├── encoding.rb │ ├── example.rb │ ├── external_documentation.rb │ ├── header.rb │ ├── info.rb │ ├── license.rb │ ├── link.rb │ ├── map.rb │ ├── media_type.rb │ ├── oauth_flow.rb │ ├── oauth_flows.rb │ ├── object.rb │ ├── openapi.rb │ ├── operation.rb │ ├── parameter.rb │ ├── parameter_like.rb │ ├── path_item.rb │ ├── paths.rb │ ├── placeholder.rb │ ├── request_body.rb │ ├── response.rb │ ├── responses.rb │ ├── schema.rb │ ├── security_requirement.rb │ ├── security_scheme.rb │ ├── server.rb │ ├── server_variable.rb │ ├── tag.rb │ └── xml.rb │ ├── node_factory.rb │ ├── node_factory │ ├── array.rb │ ├── callback.rb │ ├── components.rb │ ├── contact.rb │ ├── context.rb │ ├── discriminator.rb │ ├── encoding.rb │ ├── example.rb │ ├── external_documentation.rb │ ├── field.rb │ ├── fields │ │ └── reference.rb │ ├── header.rb │ ├── info.rb │ ├── license.rb │ ├── link.rb │ ├── map.rb │ ├── media_type.rb │ ├── oauth_flow.rb │ ├── oauth_flows.rb │ ├── object.rb │ ├── object_factory │ │ ├── dsl.rb │ │ ├── field_config.rb │ │ ├── node_builder.rb │ │ └── validator.rb │ ├── openapi.rb │ ├── operation.rb │ ├── optional_reference.rb │ ├── parameter.rb │ ├── parameter_like.rb │ ├── path_item.rb │ ├── paths.rb │ ├── reference.rb │ ├── request_body.rb │ ├── response.rb │ ├── responses.rb │ ├── schema.rb │ ├── security_requirement.rb │ ├── security_scheme.rb │ ├── server.rb │ ├── server_variable.rb │ ├── tag.rb │ ├── type_checker.rb │ └── xml.rb │ ├── source.rb │ ├── source │ ├── location.rb │ ├── pointer.rb │ ├── reference.rb │ └── resolved_reference.rb │ ├── source_input.rb │ ├── source_input │ ├── file.rb │ ├── raw.rb │ ├── resolve_next.rb │ ├── string_parser.rb │ └── url.rb │ ├── validation │ ├── error.rb │ ├── error_collection.rb │ ├── input_validator.rb │ └── validatable.rb │ ├── validators │ ├── absolute_uri.rb │ ├── component_keys.rb │ ├── duplicate_parameters.rb │ ├── email.rb │ ├── media_type.rb │ ├── mutually_exclusive_fields.rb │ ├── reference.rb │ ├── required_fields.rb │ ├── unexpected_fields.rb │ └── url.rb │ └── version.rb ├── openapi3_parser.gemspec └── spec ├── integration ├── default_servers_cascade_through_a_document_spec.rb ├── iterate_through_a_document_spec.rb ├── open_a_document_with_cross_document_references_spec.rb ├── open_a_document_with_defaults_spec.rb ├── open_a_document_with_recursive_references_spec.rb ├── open_a_document_with_reference_issues_spec.rb ├── open_a_yaml_document_spec.rb ├── open_a_yaml_document_with_dates_spec.rb ├── open_a_yaml_url_document_spec.rb └── open_an_invalid_document_spec.rb ├── lib └── openapi3_parser │ ├── cautious_dig_spec.rb │ ├── document │ └── reference_registry_spec.rb │ ├── document_spec.rb │ ├── markdown_spec.rb │ ├── node │ ├── array_spec.rb │ ├── context_spec.rb │ ├── map_spec.rb │ ├── object_spec.rb │ ├── operation_spec.rb │ ├── path_item_spec.rb │ ├── placeholder_spec.rb │ └── schema_spec.rb │ ├── node_factory │ ├── array_spec.rb │ ├── callback_spec.rb │ ├── components_spec.rb │ ├── contact_spec.rb │ ├── context_spec.rb │ ├── discriminator_spec.rb │ ├── encoding_spec.rb │ ├── example_spec.rb │ ├── external_documentation_spec.rb │ ├── field_spec.rb │ ├── fields │ │ └── reference_spec.rb │ ├── header_spec.rb │ ├── info_spec.rb │ ├── license_spec.rb │ ├── link_spec.rb │ ├── map_spec.rb │ ├── media_type_spec.rb │ ├── oauth_flow_spec.rb │ ├── oauth_flows_spec.rb │ ├── object_factory │ │ ├── field_config_spec.rb │ │ ├── node_builder_spec.rb │ │ └── validator_spec.rb │ ├── object_spec.rb │ ├── openapi_spec.rb │ ├── operation_spec.rb │ ├── parameter_spec.rb │ ├── path_item_spec.rb │ ├── paths_spec.rb │ ├── reference_spec.rb │ ├── request_body_spec.rb │ ├── response_spec.rb │ ├── responses_spec.rb │ ├── schema_spec.rb │ ├── security_requirement_spec.rb │ ├── security_scheme_spec.rb │ ├── server_spec.rb │ ├── server_variable_spec.rb │ ├── tag_spec.rb │ ├── type_checker_spec.rb │ └── xml_spec.rb │ ├── source │ ├── location_spec.rb │ ├── pointer_spec.rb │ ├── reference_spec.rb │ └── resolved_reference_spec.rb │ ├── source_input │ ├── file_spec.rb │ ├── raw_spec.rb │ ├── resolve_next_spec.rb │ ├── string_parser_spec.rb │ └── url_spec.rb │ ├── source_spec.rb │ ├── validation │ ├── error_collection_spec.rb │ └── error_spec.rb │ └── validators │ ├── absolute_uri_spec.rb │ ├── component_keys_spec.rb │ ├── duplicate_parameters_spec.rb │ ├── email_spec.rb │ ├── media_type_spec.rb │ ├── mutually_exclusive_fields_spec.rb │ ├── reference_spec.rb │ ├── required_fields_spec.rb │ ├── unexpected_fields_spec.rb │ └── url_spec.rb ├── spec_helper.rb └── support ├── default_field.rb ├── examples ├── petstore-expanded.yaml └── uber.yaml ├── helpers ├── context.rb └── source.rb ├── matchers └── have_validation_error.rb ├── mutually_exclusive_example.rb ├── node_equality.rb ├── node_factory.rb └── node_object_factory.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | test: 4 | strategy: 5 | matrix: 6 | ruby: [3.1, 3.2, 3.3, 3.4] 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: ruby/setup-ruby@v1 11 | with: 12 | bundler-cache: true 13 | ruby-version: ${{ matrix.ruby }} 14 | - name: Test Ruby ${{ matrix.ruby }} 15 | run: bundle exec rake 16 | if: matrix.ruby != '3.1' 17 | - name: Test and publish coverage for Ruby ${{ matrix.ruby }} 18 | uses: paambaati/codeclimate-action@v9.0.0 19 | env: 20 | CC_TEST_REPORTER_ID: 8bc2d8e54331569aeb442094c21cb64a58d6efa0670f65ff00d9ae887f63c0b4 21 | with: 22 | coverageCommand: bundle exec rake 23 | if: matrix.ruby == '3.1' 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /pkg 3 | /.yardoc 4 | /doc 5 | /coverage 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --warnings 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | AllCops: 5 | NewCops: enable 6 | Style/StringLiterals: 7 | EnforcedStyle: double_quotes 8 | Metrics/MethodLength: 9 | Max: 30 10 | Metrics/AbcSize: 11 | Max: 30 12 | RSpec/ExampleLength: 13 | Max: 30 14 | Style/Documentation: 15 | Enabled: false 16 | Metrics/BlockLength: 17 | Exclude: 18 | - 'spec/**/*.rb' 19 | - '*.gemspec' 20 | RSpec/DescribeClass: 21 | Exclude: 22 | - 'spec/integration/**/*.rb' 23 | # I'd rather have multiple expectations than lots of duplicate tests 24 | RSpec/MultipleExpectations: 25 | Enabled: false 26 | # The default arbitrary number (5) is a little painful 27 | RSpec/MultipleMemoizedHelpers: 28 | Enabled: false 29 | RSpec/MessageSpies: 30 | Enabled: false 31 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.5 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "bundler" 8 | gem "byebug", "~> 11.0" 9 | gem "rake" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop", "~> 1" 12 | gem "rubocop-rake", "~> 0.5" 13 | gem "rubocop-rspec", "~> 2" 14 | gem "simplecov", "~> 0.19" 15 | gem "webmock", "~> 3.8" 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Kevin Dew 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | require "rubocop/rake_task" 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new 9 | 10 | task default: %I[rubocop spec] 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | These are the steps defined to reach 1.0. Assistance is very welcome. 4 | 5 | - [x] Handle mutually exclusive fields 6 | - [x] Refactor the various NodeFactory modules to be a less confusing 7 | hierachical structure. Consider having factories subclass instead of use 8 | mixin 9 | - [x] Decouple Document class for the source file. Consider a source file class 10 | instead 11 | - [x] Validate that a reference creates the type of node that is expected in 12 | a context 13 | - [x] Allow opening of references from additional files 14 | - [x] Allow opening of openapi documents by URL 15 | - [x] Support references by URL 16 | - [ ] Consider option to limit open by URL/path behaviour 17 | - [x] Support converting CommonMark to HTML 18 | - [ ] Reach parity with OpenAPI specification for validation 19 | - [ ] Consider a lenient mode for a document to only have to comply with type 20 | based validation 21 | - [x] Improve test coverage 22 | - [ ] Publish documentation of the interface through the structure 23 | - [x] Consider a resolved context class for representing context with a node 24 | that can handle scenarios where a node is represented by both a reference 25 | and resolved context 26 | - [x] Create error classes for various scenarios 27 | - [ ] Associate/resolve operation id / operation references 28 | - [ ] Do something to model expressions 29 | - [x] Improve the modelling of namespace 30 | - [x] Set up nicer string representations of key classes to help them be 31 | debugged 32 | - [x] Ensure Array and Map nodes return empty ones by default rather than nil 33 | - [ ] Make JSON pointer public access to be consistent accepting string, array 34 | or (potentially) a pointer class 35 | - [x] Support creating a default Server object on servers property of OpenAPI 36 | Node 37 | - [ ] Support relative URLs being able to be relative the first server object 38 | see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#relative-references-in-urls 39 | - [ ] Support validating a Server URL based on default values 40 | - [ ] Validate paths to check path parameters within them appear in paths 41 | see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#fixed-fields-10 42 | -------------------------------------------------------------------------------- /lib/openapi3_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | files = Dir.glob(File.join(__dir__, "openapi3_parser", "**", "*.rb")) 4 | files.each { |file| require file } 5 | 6 | module Openapi3Parser 7 | # For a variety of inputs this will construct an OpenAPI document. For a 8 | # String/File input it will try to determine if the input is JSON or YAML. 9 | # 10 | # @param [String, Hash, File] input Source for the OpenAPI document 11 | # 12 | # @return [Document] 13 | def self.load(input) 14 | Document.new(SourceInput::Raw.new(input)) 15 | end 16 | 17 | # For a given string filename this will read the file and parse it as an 18 | # OpenAPI document. It will try detect automatically whether the contents 19 | # are JSON or YAML. 20 | # 21 | # @param [String] path Filename of the OpenAPI document 22 | # 23 | # @return [Document] 24 | def self.load_file(path) 25 | Document.new(SourceInput::File.new(path)) 26 | end 27 | 28 | # For a given string URL this will request the resource and parse it as an 29 | # OpenAPI document. It will try detect automatically whether the contents 30 | # are JSON or YAML. 31 | # 32 | # @param [String] url URL of the OpenAPI document 33 | # 34 | # @return [Document] 35 | def self.load_url(url) 36 | Document.new(SourceInput::Url.new(url.to_s)) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/openapi3_parser/array_sentence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module ArraySentence 5 | refine ::Array do 6 | def sentence_join 7 | return join if count < 2 8 | 9 | "#{self[0..-2].join(', ')} and #{self[-1]}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/openapi3_parser/cautious_dig.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | class CautiousDig 5 | private_class_method :new 6 | 7 | def self.call(*args) 8 | new.call(*args) 9 | end 10 | 11 | def call(collection, *segments) 12 | segments.inject(collection) do |next_depth, segment| 13 | break unless next_depth 14 | 15 | if next_depth.respond_to?(:keys) 16 | hash_like(next_depth, segment) 17 | elsif next_depth.respond_to?(:[]) 18 | array_like(next_depth, segment) 19 | end 20 | end 21 | end 22 | 23 | private 24 | 25 | def hash_like(item, segment) 26 | key = item.keys.find { |k| segment == k || segment.to_s == k.to_s } 27 | item[key] 28 | end 29 | 30 | def array_like(item, segment) 31 | index = if segment.is_a?(String) && segment =~ /\A\d+\z/ 32 | segment.to_i 33 | else 34 | segment 35 | end 36 | index.is_a?(Integer) ? item[index] : nil 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/openapi3_parser/document/reference_registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | class Document 5 | class ReferenceRegistry 6 | attr_reader :sources 7 | 8 | def initialize 9 | @sources = [] 10 | @factories_by_type = {} 11 | end 12 | 13 | def freeze 14 | sources.freeze 15 | factories_by_type.freeze.each(&:freeze) 16 | super 17 | end 18 | 19 | def factories 20 | factories_by_type.values.flatten 21 | end 22 | 23 | def register(unbuilt_factory, source_location, reference_factory_context) 24 | register_source(source_location.source) 25 | object_type = unbuilt_factory.object_type 26 | existing_factory = factory(object_type, source_location) 27 | 28 | return existing_factory if existing_factory 29 | 30 | build_factory( 31 | unbuilt_factory, 32 | source_location, 33 | reference_factory_context 34 | ).tap { |f| register_factory(object_type, f) } 35 | end 36 | 37 | def factory(object_type, source_location) 38 | factories_by_type[object_type]&.find do |f| 39 | f.context.source_location == source_location 40 | end 41 | end 42 | 43 | private 44 | 45 | attr_reader :factories_by_type 46 | 47 | def register_source(source) 48 | sources << source unless sources.include?(source) 49 | end 50 | 51 | def register_factory(object_type, factory) 52 | factories_by_type[object_type] ||= [] 53 | factories_by_type[object_type] << factory 54 | end 55 | 56 | def build_factory(unbuilt_factory, 57 | source_location, 58 | reference_factory_context) 59 | next_context = NodeFactory::Context.resolved_reference( 60 | reference_factory_context, 61 | source_location: 62 | ) 63 | 64 | if unbuilt_factory.is_a?(Class) 65 | unbuilt_factory.new(next_context) 66 | else 67 | unbuilt_factory.call(next_context) 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/openapi3_parser/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | # An abstract class for Exceptions produced by this Gem 5 | class Error < ::RuntimeError 6 | # Raised in cases where we have been provided a path or URL to a file and 7 | # at runtime when we have tried to access that resource it is not available 8 | # for whatever reason. 9 | class InaccessibleInput < Error; end 10 | 11 | # Raised in cases where we provided data that we expected to be parsable 12 | # (such as a string of JSON data) but when we tried to parse it an error 13 | # is raised 14 | class UnparsableInput < Error; end 15 | 16 | # Raised in cases where an object that is in an immutable state is modified 17 | # 18 | # Typically this would occur when a component that is frozen is modififed. 19 | # Some components are mutable during the construction of a document and 20 | # then frozen afterwards. 21 | class ImmutableObject < Error; end 22 | 23 | # Raised when a node is provided data as a type that is outside the allowed 24 | # list 25 | class InvalidType < Error; end 26 | 27 | # Raised when we have to abort creating an object due to invalid data 28 | class InvalidData < Error; end 29 | 30 | # Used when there are fields that are missing from an object which prevents 31 | # us from creating a node 32 | class MissingFields < Error; end 33 | 34 | # Used when there are extra fields that are not expected in the data for 35 | # a node 36 | class UnexpectedFields < Error; end 37 | 38 | # Used when a method we expect to be able to call (through symbol or proc) 39 | # is not callable 40 | class NotCallable < Error; end 41 | 42 | # Raised when we in a recursive data structure and can't perform an 43 | # operation 44 | class InRecursiveStructure < Error; end 45 | 46 | # Used when we're trying to validate that a type is something that is not 47 | # validatable, most likely a sign that we're in a bug 48 | class UnvalidatableType < Error; end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/openapi3_parser/markdown.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "commonmarker" 4 | 5 | module Openapi3Parser 6 | # Wrapper around a gem to render markdown, used a single place for options 7 | # and handling the gem 8 | module Markdown 9 | # @param [String] text 10 | # @return [String] 11 | def self.to_html(text) 12 | Commonmarker.to_html(text) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # An array within a OpenAPI document. 8 | # Very similar to a normal Ruby array, however this is read only and knows 9 | # the context of where it sits in an OpenAPI document 10 | # 11 | # The contents of the data will be dependent on where this document is in 12 | # the document hierachy. 13 | class Array 14 | extend Forwardable 15 | include Enumerable 16 | 17 | def_delegators :node_data, :empty?, :length, :size 18 | attr_reader :node_data, :node_context 19 | 20 | # @param [::Array] data data used to populate this node 21 | # @param [Context] context The context of this node in the document 22 | def initialize(data, context) 23 | @node_data = data 24 | @node_context = context 25 | end 26 | 27 | def [](index) 28 | Placeholder.resolve(node_data[index]) 29 | end 30 | 31 | # Iterates through the data of this node, used by Enumerable 32 | # 33 | # @return [Object] 34 | def each(&) 35 | Placeholder.each(node_data, &) 36 | end 37 | 38 | # @param [Any] other 39 | # 40 | # @return [Boolean] 41 | def ==(other) 42 | other.instance_of?(self.class) && 43 | node_context.same_data_and_source?(other.node_context) 44 | end 45 | 46 | # Used to access a node relative to this node 47 | # @param [Source::Pointer, ::Array, ::String] pointer_like 48 | # @return anything 49 | def node_at(pointer_like) 50 | current_pointer = node_context.document_location.pointer 51 | node_context.document.node_at(pointer_like, current_pointer) 52 | end 53 | 54 | # @return [String] 55 | def inspect 56 | fragment = node_context.document_location.pointer.fragment 57 | %{#{self.class.name}(#{fragment})} 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/callback.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/map" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#callbackObject 8 | class Callback < Node::Map; end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/components.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#componentsObject 8 | class Components < Node::Object 9 | # @return [Map] 10 | def schemas 11 | self["schemas"] 12 | end 13 | 14 | # @return [Map] 15 | def responses 16 | self["responses"] 17 | end 18 | 19 | # @return [Map] 20 | def parameters 21 | self["parameters"] 22 | end 23 | 24 | # @return [Map] 25 | def examples 26 | self["examples"] 27 | end 28 | 29 | # @return [Map] 30 | def request_bodies 31 | self["requestBodies"] 32 | end 33 | 34 | # @return [Map] 35 | def headers 36 | self["headers"] 37 | end 38 | 39 | # @return [Map] 40 | def security_schemes 41 | self["securitySchemes"] 42 | end 43 | 44 | # @return [Map] 45 | def links 46 | self["links"] 47 | end 48 | 49 | # @return [Map] 50 | def callbacks 51 | self["callbacks"] 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/contact.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#contactObject 8 | class Contact < Node::Object 9 | # @return [String, nil] 10 | def name 11 | self["name"] 12 | end 13 | 14 | # @return [String, nil] 15 | def url 16 | self["url"] 17 | end 18 | 19 | # @return [String, nil] 20 | def email 21 | self["email"] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/discriminator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#discriminatorObject 8 | class Discriminator < Node::Object 9 | # @return [String] 10 | def property_name 11 | self["propertyName"] 12 | end 13 | 14 | # @return [Map] 15 | def mapping 16 | self["mapping"] 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/encoding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#encodingObject 8 | class Encoding < Node::Object 9 | # @return [String, nil] 10 | def content_type 11 | self["contentType"] 12 | end 13 | 14 | # @return [Map] 15 | def headers 16 | self["headers"] 17 | end 18 | 19 | # @return [String, nil] 20 | def style 21 | self["style"] 22 | end 23 | 24 | # @return [Boolean] 25 | def explode? 26 | self["explode"] 27 | end 28 | 29 | # @return [Boolean] 30 | def allow_reserved? 31 | self["allowReserved"] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#exampleObject 8 | class Example < Node::Object 9 | # @return [String, nil] 10 | def summary 11 | self["summary"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [Object] 25 | def value 26 | self["value"] 27 | end 28 | 29 | # @return [String, nil] 30 | def external_value 31 | self["externalValue"] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/external_documentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#externalDocumentationObject 8 | class ExternalDocumentation < Node::Object 9 | # @return [String, nil] 10 | def description 11 | self["description"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description_html 16 | render_markdown(description) 17 | end 18 | 19 | # @return [String] 20 | def url 21 | self["url"] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | require "openapi3_parser/node/parameter_like" 5 | 6 | module Openapi3Parser 7 | module Node 8 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#headerObject 9 | class Header < Node::Object 10 | include ParameterLike 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#infoObject 8 | class Info < Node::Object 9 | # @return [String] 10 | def title 11 | self["title"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [String, nil] 25 | def terms_of_service 26 | self["termsOfService"] 27 | end 28 | 29 | # @return [Contact, nil] 30 | def contact 31 | self["contact"] 32 | end 33 | 34 | # @return [License, nil] 35 | def license 36 | self["license"] 37 | end 38 | 39 | # @return [String] 40 | def version 41 | self["version"] 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/license.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#licenseObject 8 | class License < Node::Object 9 | # @return [String] 10 | def name 11 | self["name"] 12 | end 13 | 14 | # @return [String, nil] 15 | def url 16 | self["url"] 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject 8 | class Link < Node::Object 9 | # @return [String, nil] 10 | def operation_ref 11 | self["operationRef"] 12 | end 13 | 14 | # @return [String, nil] 15 | def operation_id 16 | self["operationId"] 17 | end 18 | 19 | # @return [Map] 20 | def parameters 21 | self["parameters"] 22 | end 23 | 24 | # @return [Any] 25 | def request_body 26 | self["requestBody"] 27 | end 28 | 29 | # @return [String, nil] 30 | def description 31 | self["description"] 32 | end 33 | 34 | # @return [String, nil] 35 | def description_html 36 | render_markdown(description) 37 | end 38 | 39 | # @return [Server, nil] 40 | def server 41 | self["server"] 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | module Node 7 | class Map 8 | extend Forwardable 9 | include Enumerable 10 | 11 | def_delegators :node_data, :keys, :empty?, :length, :size 12 | attr_reader :node_data, :node_context 13 | 14 | def initialize(data, context) 15 | @node_data = data 16 | @node_context = context 17 | end 18 | 19 | # Look up an attribute of the node by the name it has in the OpenAPI 20 | # document. 21 | # 22 | # @example Look up by OpenAPI naming 23 | # obj["externalDocs"] 24 | # 25 | # @example Look up by symbol 26 | # obj[:servers] 27 | # 28 | # @example Look up an extension 29 | # obj["x-myExtension"] 30 | # 31 | # @param [String, Symbol] value 32 | # 33 | # @return anything 34 | def [](value) 35 | Placeholder.resolve(node_data[value.to_s]) 36 | end 37 | 38 | # Look up an extension provided for this map, doesn't need a prefix of 39 | # "x-" 40 | # 41 | # @example Looking up an extension provided as "x-extra" 42 | # obj.extension("extra") 43 | # 44 | # @param [String, Symbol] value 45 | # 46 | # @return [Hash, Array, Numeric, String, true, false, nil] 47 | def extension(value) 48 | self["x-#{value}"] 49 | end 50 | 51 | # @param [Any] other 52 | # 53 | # @return [Boolean] 54 | def ==(other) 55 | other.instance_of?(self.class) && 56 | node_context.same_data_and_source?(other.node_context) 57 | end 58 | 59 | # Iterates through the data of this node, used by Enumerable 60 | # 61 | # @return [Object] 62 | def each(&) 63 | Placeholder.each(node_data, &) 64 | end 65 | 66 | # Provide an array of values for this object, a partner to the #keys 67 | # method 68 | # 69 | # @return [Array] 70 | def values 71 | map(&:last) 72 | end 73 | 74 | # Used to access a node relative to this node 75 | # @param [Source::Pointer, ::Array, ::String] pointer_like 76 | # @return anything 77 | def node_at(pointer_like) 78 | current_pointer = node_context.document_location.pointer 79 | node_context.document.node_at(pointer_like, current_pointer) 80 | end 81 | 82 | # @return [String] 83 | def inspect 84 | fragment = node_context.document_location.pointer.fragment 85 | %{#{self.class.name}(#{fragment})} 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/media_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#mediaTypeObject 8 | class MediaType < Node::Object 9 | # @return [Schema, nil] 10 | def schema 11 | self["schema"] 12 | end 13 | 14 | # @return [Any] 15 | def example 16 | self["example"] 17 | end 18 | 19 | # @return [Map, nil] 20 | def examples 21 | self["examples"] 22 | end 23 | 24 | # @return [Map] 25 | def encoding 26 | self["encoding"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/oauth_flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oauthFlowObject 8 | class OauthFlow < Node::Object 9 | # @return [String, nil] 10 | def authorization_url 11 | self["authorizationUrl"] 12 | end 13 | 14 | # @return [String, nil] 15 | def token_url 16 | self["tokenUrl"] 17 | end 18 | 19 | # @return [String, nil] 20 | def refresh_url 21 | self["refreshUrl"] 22 | end 23 | 24 | # @return [Map] 25 | def scopes 26 | self["scopes"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/oauth_flows.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oauthFlowsObject 8 | class OauthFlows < Node::Object 9 | # @return [OauthFlow, nil] 10 | def implicit 11 | self["implicit"] 12 | end 13 | 14 | # @return [OauthFlow, nil] 15 | def password 16 | self["password"] 17 | end 18 | 19 | # @return [OauthFlow, nil] 20 | def client_credentials 21 | self["clientCredentials"] 22 | end 23 | 24 | # @return [OauthFlow, nil] 25 | def authorization_code 26 | self["authorizationCode"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | module Node 7 | class Object 8 | extend Forwardable 9 | include Enumerable 10 | 11 | def_delegators :node_data, :keys, :empty? 12 | attr_reader :node_data, :node_context 13 | 14 | def initialize(data, context) 15 | @node_data = data 16 | @node_context = context 17 | end 18 | 19 | # Look up an attribute of the node by the name it has in the OpenAPI 20 | # document. 21 | # 22 | # @example Look up by OpenAPI naming 23 | # obj["externalDocs"] 24 | # 25 | # @example Look up by symbol 26 | # obj[:servers] 27 | # 28 | # @example Look up an extension 29 | # obj["x-myExtension"] 30 | # 31 | # @param [String, Symbol] value 32 | # 33 | # @return anything 34 | def [](value) 35 | Placeholder.resolve(node_data[value.to_s]) 36 | end 37 | 38 | # Look up an extension provided for this object, doesn't need a prefix of 39 | # "x-" 40 | # 41 | # @example Looking up an extension provided as "x-extra" 42 | # obj.extension("extra") 43 | # 44 | # @param [String, Symbol] value 45 | # 46 | # @return [Hash, Array, Numeric, String, true, false, nil] 47 | def extension(value) 48 | self["x-#{value}"] 49 | end 50 | 51 | # @param [Any] other 52 | # 53 | # @return [Boolean] 54 | def ==(other) 55 | other.instance_of?(self.class) && 56 | node_context.same_data_and_source?(other.node_context) 57 | end 58 | 59 | # Iterates through the data of this node, used by Enumerable 60 | # 61 | # @return [Object] 62 | def each(&) 63 | Placeholder.each(node_data, &) 64 | end 65 | 66 | # Provide an array of values for this object, a partner to the #keys 67 | # method 68 | # 69 | # @return [Array] 70 | def values 71 | map(&:last) 72 | end 73 | 74 | # Used to render fields that can be in markdown syntax into HTML 75 | # @param [String, nil] value 76 | # @return [String, nil] 77 | def render_markdown(value) 78 | return if value.nil? 79 | 80 | Markdown.to_html(value) 81 | end 82 | 83 | # Used to access a node relative to this node 84 | # 85 | # @example Looking up the parent node of this node 86 | # obj.node_at("#..") 87 | # 88 | # @example Jumping way down the tree 89 | # obj.node_at("#properties/Field/type") 90 | # 91 | # @param [Source::Pointer, ::Array, ::String] pointer_like 92 | # @return anything 93 | def node_at(pointer_like) 94 | current_pointer = node_context.document_location.pointer 95 | node_context.document.node_at(pointer_like, current_pointer) 96 | end 97 | 98 | # @return [String] 99 | def inspect 100 | fragment = node_context.document_location.pointer.fragment 101 | %{#{self.class.name}(#{fragment})} 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/openapi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # OpenAPI Root Object 8 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oasObject 9 | class Openapi < Node::Object 10 | # @return [String] 11 | def openapi 12 | self["openapi"] 13 | end 14 | 15 | # @return [Info] 16 | def info 17 | self["info"] 18 | end 19 | 20 | # @return [Node::Array] 21 | def servers 22 | self["servers"] 23 | end 24 | 25 | # @return [Paths] 26 | def paths 27 | self["paths"] 28 | end 29 | 30 | # @return [Components] 31 | def components 32 | self["components"] 33 | end 34 | 35 | # @return [Node::Array] 36 | def security 37 | self["security"] 38 | end 39 | 40 | # @return [Node::Array] 41 | def tags 42 | self["tags"] 43 | end 44 | 45 | # @return [ExternalDocumentation, nil] 46 | def external_docs 47 | self["externalDocs"] 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject 8 | class Operation < Node::Object 9 | # @return [Node::Array] 10 | def tags 11 | self["tags"] 12 | end 13 | 14 | # @return [String, nil] 15 | def summary 16 | self["summary"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description 21 | self["description"] 22 | end 23 | 24 | # @return [String, nil] 25 | def description_html 26 | render_markdown(description) 27 | end 28 | 29 | # @return [ExternalDocumentation, nil] 30 | def external_docs 31 | self["externalDocs"] 32 | end 33 | 34 | # @return [String, nil] 35 | def operation_id 36 | self["operationId"] 37 | end 38 | 39 | # @return [Node::Array] 40 | def parameters 41 | self["parameters"] 42 | end 43 | 44 | # @return [RequestBody, nil] 45 | def request_body 46 | self["requestBody"] 47 | end 48 | 49 | # @return [Responses] 50 | def responses 51 | self["responses"] 52 | end 53 | 54 | # @return [Map] 55 | def callbacks 56 | self["callbacks"] 57 | end 58 | 59 | # @return [Boolean] 60 | def deprecated? 61 | self["deprecated"] 62 | end 63 | 64 | # @return [Node::Array] 65 | def security 66 | self["security"] 67 | end 68 | 69 | # @return [Node::Array] 70 | def servers 71 | self["servers"] 72 | end 73 | 74 | # Whether this object uses it's own defined servers instead of falling 75 | # back to the path items' ones. 76 | # 77 | # @return [Boolean] 78 | def alternative_servers? 79 | servers != node_context.parent_node.servers 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | require "openapi3_parser/node/parameter_like" 5 | 6 | module Openapi3Parser 7 | module Node 8 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject 9 | class Parameter < Node::Object 10 | include ParameterLike 11 | 12 | # @return [String] 13 | def name 14 | self["name"] 15 | end 16 | 17 | # @return [String] 18 | def in 19 | self["in"] 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/parameter_like.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Node 5 | # This contains methods that are shared between nodes that act like a 6 | # Parameter, at the time of writing this was {Header}[../Header.html] 7 | # and {Parameter}[../Paramater.html] 8 | module ParameterLike 9 | # @return [String] 10 | def description 11 | self["description"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description_html 16 | render_markdown(description) 17 | end 18 | 19 | # @return [Boolean] 20 | def required? 21 | self["required"] 22 | end 23 | 24 | # @return [Boolean] 25 | def deprecated? 26 | self["deprecated"] 27 | end 28 | 29 | # @return [Boolean] 30 | def allow_empty_value? 31 | self["allowEmptyValue"] 32 | end 33 | 34 | # @return [String, nil] 35 | def style 36 | self["style"] 37 | end 38 | 39 | # @return [Boolean] 40 | def explode? 41 | self["explode"] 42 | end 43 | 44 | # @return [Boolean] 45 | def allow_reserved? 46 | self["allowReserved"] 47 | end 48 | 49 | # @return [Schema, nil] 50 | def schema 51 | self["schema"] 52 | end 53 | 54 | # @return [Any] 55 | def example 56 | self["example"] 57 | end 58 | 59 | # @return [Map, nil] 60 | def examples 61 | self["examples"] 62 | end 63 | 64 | # @return [Map, nil] 65 | def content 66 | self["content"] 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/path_item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#pathItemObject 8 | class PathItem < Node::Object 9 | # @return [String, nil] 10 | def summary 11 | self["summary"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [Operation, nil] 25 | def get 26 | self["get"] 27 | end 28 | 29 | # @return [Operation, nil] 30 | def put 31 | self["put"] 32 | end 33 | 34 | # @return [Operation, nil] 35 | def post 36 | self["post"] 37 | end 38 | 39 | # @return [Operation, nil] 40 | def delete 41 | self["delete"] 42 | end 43 | 44 | # @return [Operation, nil] 45 | def options 46 | self["options"] 47 | end 48 | 49 | # @return [Operation, nil] 50 | def head 51 | self["head"] 52 | end 53 | 54 | # @return [Operation, nil] 55 | def patch 56 | self["patch"] 57 | end 58 | 59 | # @return [Operation, nil] 60 | def trace 61 | self["trace"] 62 | end 63 | 64 | # @return [Node::Array] 65 | def servers 66 | self["servers"] 67 | end 68 | 69 | # Whether this object uses it's own defined servers instead of falling 70 | # back to the root ones. 71 | # 72 | # @return [Boolean] 73 | def alternative_servers? 74 | servers != node_context.document.root.servers 75 | end 76 | 77 | # @return [Node::Array] 78 | def parameters 79 | self["parameters"] 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/paths.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/map" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#pathsObject 8 | class Paths < Node::Map; end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/placeholder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | module Node 7 | class Placeholder 8 | extend Forwardable 9 | 10 | def self.resolve(potential_placeholder) 11 | if potential_placeholder.is_a?(Placeholder) 12 | potential_placeholder.node 13 | else 14 | potential_placeholder 15 | end 16 | end 17 | 18 | # Used to iterate through hashes or arrays that may contain 19 | # Placeholder objects where these are resolved to being nodes 20 | # before iteration 21 | def self.each(node_data, &) 22 | resolved = 23 | if node_data.respond_to?(:keys) 24 | node_data.transform_values do |value| 25 | resolve(value) 26 | end 27 | else 28 | node_data.map { |item| resolve(item) } 29 | end 30 | 31 | resolved.each(&) 32 | end 33 | 34 | attr_reader :node_factory, :field, :parent_context 35 | 36 | def_delegators :node_factory, :nil_input? 37 | 38 | def initialize(node_factory, field, parent_context) 39 | @node_factory = node_factory 40 | @field = field 41 | @parent_context = parent_context 42 | end 43 | 44 | def node 45 | @node ||= begin 46 | node_context = Context.next_field(parent_context, 47 | field, 48 | node_factory.context) 49 | node_factory.node(node_context) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/request_body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#requestBodyObject 8 | class RequestBody < Node::Object 9 | # @return [String, nil] 10 | def description 11 | self["description"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description_html 16 | render_markdown(description) 17 | end 18 | 19 | # @return [Map] 20 | def content 21 | self["content"] 22 | end 23 | 24 | # @return [Boolean] 25 | def required? 26 | self["required"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject 8 | class Response < Node::Object 9 | # @return [String] 10 | def description 11 | self["description"] 12 | end 13 | 14 | # @return [String] 15 | def description_html 16 | render_markdown(description) 17 | end 18 | 19 | # @return [Map] 20 | def headers 21 | self["headers"] 22 | end 23 | 24 | # @return [Map] 25 | def content 26 | self["content"] 27 | end 28 | 29 | # @return [Map] 30 | def links 31 | self["links"] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/responses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/map" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responsesObject 8 | class Responses < Node::Map 9 | # @return [Response] 10 | def default 11 | self["default"] 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/security_requirement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/map" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject 8 | class SecurityRequirement < Node::Map; end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/security_scheme.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject 8 | class SecurityScheme < Node::Object 9 | # @return [String, nil] 10 | def type 11 | self["type"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [String, nil] 25 | def name 26 | self["name"] 27 | end 28 | 29 | # @return [String, nil] 30 | def in 31 | self["in"] 32 | end 33 | 34 | # @return [String, nil] 35 | def scheme 36 | self["scheme"] 37 | end 38 | 39 | # @return [String, nil] 40 | def bearer_format 41 | self["bearerFormat"] 42 | end 43 | 44 | # @return [OauthFlows, nil] 45 | def flows 46 | self["flows"] 47 | end 48 | 49 | # @return [String, nil] 50 | def open_id_connect_url 51 | self["openIdConnectUrl"] 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#serverObject 8 | class Server < Node::Object 9 | # @return [String] 10 | def url 11 | self["url"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [Map] 25 | def variables 26 | self["variables"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/server_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#serverVariableObject 8 | class ServerVariable < Node::Object 9 | # @return [Node::Array, nil] 10 | def enum 11 | self["enum"] 12 | end 13 | 14 | # @return [String] 15 | def default 16 | self["default"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description 21 | self["description"] 22 | end 23 | 24 | # @return [String, nil] 25 | def description_html 26 | render_markdown(description) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#tagObject 8 | class Tag < Node::Object 9 | # @return [String] 10 | def name 11 | self["name"] 12 | end 13 | 14 | # @return [String, nil] 15 | def description 16 | self["description"] 17 | end 18 | 19 | # @return [String, nil] 20 | def description_html 21 | render_markdown(description) 22 | end 23 | 24 | # @return [ExternalDocumentation, nil] 25 | def external_docs 26 | self["externalDocs"] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node/object" 4 | 5 | module Openapi3Parser 6 | module Node 7 | # @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#xmlObject 8 | class Xml < Node::Object 9 | # @return [String, nil] 10 | def name 11 | self["name"] 12 | end 13 | 14 | # @return [String, nil] 15 | def namespace 16 | self["namespace"] 17 | end 18 | 19 | # @return [String, nil] 20 | def prefix 21 | self["prefix"] 22 | end 23 | 24 | # @return [Boolean] 25 | def attribute? 26 | self["attribute"] 27 | end 28 | 29 | # @return [Boolean] 30 | def wrapped? 31 | self["wrapped"] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | EXTENSION_REGEX = /^x-(.*)/ 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/callback.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/map" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Callback < NodeFactory::Map 8 | def initialize(context) 9 | super(context, 10 | allow_extensions: true, 11 | value_factory: NodeFactory::PathItem) 12 | end 13 | 14 | private 15 | 16 | def build_node(data, node_context) 17 | Node::Callback.new(data, node_context) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/components.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Components < NodeFactory::Object 8 | allow_extensions 9 | field "schemas", factory: :schemas_factory 10 | field "responses", factory: :responses_factory 11 | field "parameters", factory: :parameters_factory 12 | field "examples", factory: :examples_factory 13 | field "requestBodies", factory: :request_bodies_factory 14 | field "headers", factory: :headers_factory 15 | field "securitySchemes", factory: :security_schemes_factory 16 | field "links", factory: :links_factory 17 | field "callbacks", factory: :callbacks_factory 18 | 19 | private 20 | 21 | def build_object(data, context) 22 | Node::Components.new(data, context) 23 | end 24 | 25 | def schemas_factory(context) 26 | referenceable_map_factory(context, NodeFactory::Schema) 27 | end 28 | 29 | def responses_factory(context) 30 | referenceable_map_factory(context, NodeFactory::Response) 31 | end 32 | 33 | def parameters_factory(context) 34 | referenceable_map_factory(context, NodeFactory::Parameter) 35 | end 36 | 37 | def examples_factory(context) 38 | referenceable_map_factory(context, NodeFactory::Example) 39 | end 40 | 41 | def request_bodies_factory(context) 42 | referenceable_map_factory(context, NodeFactory::RequestBody) 43 | end 44 | 45 | def headers_factory(context) 46 | referenceable_map_factory(context, NodeFactory::Header) 47 | end 48 | 49 | def security_schemes_factory(context) 50 | referenceable_map_factory(context, NodeFactory::SecurityScheme) 51 | end 52 | 53 | def links_factory(context) 54 | referenceable_map_factory(context, NodeFactory::Link) 55 | end 56 | 57 | def callbacks_factory(context) 58 | referenceable_map_factory(context, NodeFactory::Callback) 59 | end 60 | 61 | def referenceable_map_factory(context, factory) 62 | NodeFactory::Map.new( 63 | context, 64 | value_factory: NodeFactory::OptionalReference.new(factory), 65 | validate: Validation::InputValidator.new(Validators::ComponentKeys) 66 | ) 67 | end 68 | 69 | def default 70 | {} 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/contact.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/validation/input_validator" 5 | require "openapi3_parser/validators/email" 6 | require "openapi3_parser/validators/url" 7 | 8 | module Openapi3Parser 9 | module NodeFactory 10 | class Contact < NodeFactory::Object 11 | allow_extensions 12 | 13 | field "name", input_type: String 14 | field "url", 15 | input_type: String, 16 | validate: Validation::InputValidator.new(Validators::Url) 17 | field "email", 18 | input_type: String, 19 | validate: Validation::InputValidator.new(Validators::Email) 20 | 21 | private 22 | 23 | def build_object(data, context) 24 | Node::Contact.new(data, context) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/discriminator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Discriminator < NodeFactory::Object 8 | field "propertyName", input_type: String, required: true 9 | field "mapping", input_type: Hash, 10 | validate: :validate_mapping, 11 | default: -> { {}.freeze } 12 | 13 | private 14 | 15 | def build_object(data, context) 16 | Node::Discriminator.new(data, context) 17 | end 18 | 19 | def validate_mapping(validatable) 20 | input = validatable.input 21 | return if input.empty? 22 | 23 | string_keys = input.keys.map(&:class).uniq == [String] 24 | string_values = input.values.map(&:class).uniq == [String] 25 | return if string_keys && string_values 26 | 27 | validatable.add_error("Expected string keys and string values") 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/encoding.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Encoding < NodeFactory::Object 8 | allow_extensions 9 | 10 | field "contentType", input_type: String 11 | field "headers", factory: :headers_factory 12 | field "style", input_type: String 13 | field "explode", input_type: :boolean, default: :default_explode 14 | field "allowReserved", input_type: :boolean, default: false 15 | 16 | private 17 | 18 | def build_object(data, context) 19 | Node::Encoding.new(data, context) 20 | end 21 | 22 | def headers_factory(context) 23 | factory = NodeFactory::OptionalReference.new(NodeFactory::Header) 24 | NodeFactory::Map.new(context, value_factory: factory) 25 | end 26 | 27 | def default_explode 28 | context.input["style"] == "form" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/validation/input_validator" 5 | require "openapi3_parser/validators/url" 6 | 7 | module Openapi3Parser 8 | module NodeFactory 9 | class Example < NodeFactory::Object 10 | allow_extensions 11 | 12 | field "summary", input_type: String 13 | field "description", input_type: String 14 | field "value" 15 | field "externalValue", 16 | input_type: String, 17 | validate: Validation::InputValidator.new(Validators::Url) 18 | 19 | mutually_exclusive "value", "externalValue" 20 | 21 | private 22 | 23 | def build_object(data, context) 24 | Node::Example.new(data, context) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/external_documentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/validation/input_validator" 5 | require "openapi3_parser/validators/url" 6 | 7 | module Openapi3Parser 8 | module NodeFactory 9 | class ExternalDocumentation < NodeFactory::Object 10 | allow_extensions 11 | 12 | field "description", input_type: String 13 | field "url", 14 | required: true, 15 | input_type: String, 16 | validate: Validation::InputValidator.new(Validators::Url) 17 | 18 | private 19 | 20 | def build_object(data, context) 21 | Node::ExternalDocumentation.new(data, context) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/node_factory/parameter_like" 5 | 6 | module Openapi3Parser 7 | module NodeFactory 8 | class Header < NodeFactory::Object 9 | include ParameterLike 10 | 11 | allow_extensions 12 | 13 | field "description", input_type: String 14 | field "required", input_type: :boolean, default: false 15 | field "deprecated", input_type: :boolean, default: false 16 | field "allowEmptyValue", input_type: :boolean, default: false 17 | 18 | field "style", input_type: String, default: "simple" 19 | field "explode", input_type: :boolean, default: :default_explode 20 | field "allowReserved", input_type: :boolean, default: false 21 | field "schema", factory: :schema_factory 22 | field "example" 23 | field "examples", factory: :examples_factory 24 | 25 | field "content", factory: :content_factory 26 | 27 | private 28 | 29 | def build_object(data, context) 30 | Node::Header.new(data, context) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/contact" 4 | require "openapi3_parser/node_factory/license" 5 | require "openapi3_parser/node_factory/object" 6 | require "openapi3_parser/validation/input_validator" 7 | require "openapi3_parser/validators/url" 8 | 9 | module Openapi3Parser 10 | module NodeFactory 11 | class Info < NodeFactory::Object 12 | allow_extensions 13 | field "title", input_type: String, required: true 14 | field "description", input_type: String 15 | field "termsOfService", 16 | input_type: String, 17 | validate: Validation::InputValidator.new(Validators::Url) 18 | field "contact", factory: NodeFactory::Contact 19 | field "license", factory: NodeFactory::License 20 | field "version", input_type: String, required: true 21 | 22 | private 23 | 24 | def build_object(data, context) 25 | Node::Info.new(data, context) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/license.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/validation/input_validator" 5 | require "openapi3_parser/validators/url" 6 | 7 | module Openapi3Parser 8 | module NodeFactory 9 | class License < NodeFactory::Object 10 | allow_extensions 11 | field "name", input_type: String, required: true 12 | field "url", 13 | input_type: String, 14 | validate: Validation::InputValidator.new(Validators::Url) 15 | 16 | private 17 | 18 | def build_object(data, context) 19 | Node::License.new(data, context) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Link < NodeFactory::Object 8 | allow_extensions 9 | 10 | # @todo The link object in OAS is pretty meaty and there's lot of scope 11 | # for further work here to make use of its functionality 12 | 13 | field "operationRef", input_type: String 14 | field "operationId", input_type: String 15 | field "parameters", factory: :parameters_factory 16 | field "requestBody" 17 | field "description", input_type: String 18 | field "server", factory: :server_factory 19 | 20 | mutually_exclusive "operationRef", "operationId", required: true 21 | 22 | private 23 | 24 | def build_object(data, context) 25 | Node::Link.new(data, context) 26 | end 27 | 28 | def parameters_factory(context) 29 | NodeFactory::Map.new(context) 30 | end 31 | 32 | def server_factory(context) 33 | NodeFactory::Server.new(context) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/media_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class MediaType < NodeFactory::Object 8 | allow_extensions 9 | 10 | field "schema", factory: :schema_factory 11 | field "example" 12 | field "examples", factory: :examples_factory 13 | field "encoding", factory: :encoding_factory 14 | 15 | mutually_exclusive "example", "examples" 16 | 17 | private 18 | 19 | def build_object(data, context) 20 | Node::MediaType.new(data, context) 21 | end 22 | 23 | def schema_factory(context) 24 | factory = NodeFactory::Schema 25 | NodeFactory::OptionalReference.new(factory).call(context) 26 | end 27 | 28 | def examples_factory(context) 29 | factory = NodeFactory::OptionalReference.new(NodeFactory::Example) 30 | NodeFactory::Map.new(context, 31 | default: nil, 32 | value_factory: factory) 33 | end 34 | 35 | def encoding_factory(context) 36 | NodeFactory::Map.new( 37 | context, 38 | validate: EncodingValidator.new(self), 39 | value_factory: NodeFactory::Encoding 40 | ) 41 | end 42 | 43 | class EncodingValidator 44 | def initialize(factory) 45 | @factory = factory 46 | end 47 | 48 | def call(validatable) 49 | missing_keys = validatable.input.keys - properties 50 | return if missing_keys.empty? 51 | 52 | validatable.add_error(error_message(missing_keys)) 53 | end 54 | 55 | private 56 | 57 | attr_reader :factory 58 | 59 | def properties 60 | properties = factory.resolved_input.dig("schema", "properties") 61 | properties.respond_to?(:keys) ? properties.keys : [] 62 | end 63 | 64 | def error_message(missing_keys) 65 | keys = missing_keys.join(", ") 66 | "Keys are not defined as schema properties: #{keys}" 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/oauth_flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class OauthFlow < NodeFactory::Object 8 | allow_extensions 9 | field "authorizationUrl", input_type: String 10 | field "tokenUrl", input_type: String 11 | field "refreshUrl", input_type: String 12 | field "scopes", input_type: Hash 13 | 14 | private 15 | 16 | def build_object(data, context) 17 | Node::OauthFlow.new(data, context) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/oauth_flows.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class OauthFlows < NodeFactory::Object 8 | allow_extensions 9 | field "implicit", factory: :oauth_flow_factory 10 | field "password", factory: :oauth_flow_factory 11 | field "clientCredentials", factory: :oauth_flow_factory 12 | field "authorizationCode", factory: :oauth_flow_factory 13 | 14 | private 15 | 16 | def oauth_flow_factory(context) 17 | NodeFactory::OptionalReference.new(NodeFactory::OauthFlow) 18 | .call(context) 19 | end 20 | 21 | def build_object(data, context) 22 | Node::OauthFlows.new(data, context) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/object_factory/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object_factory/field_config" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | module ObjectFactory 8 | module Dsl 9 | MutuallyExclusiveField = Struct.new(:fields, :required, keyword_init: true) 10 | 11 | def field(name, **options) 12 | @field_configs ||= {} 13 | @field_configs[name] = FieldConfig.new(**options) 14 | end 15 | 16 | def field_configs 17 | @field_configs ||= {} 18 | end 19 | 20 | def allow_extensions 21 | @allow_extensions = true 22 | end 23 | 24 | def allowed_extensions? 25 | if instance_variable_defined?(:@allow_extensions) 26 | @allow_extensions == true 27 | else 28 | false 29 | end 30 | end 31 | 32 | def mutually_exclusive(*fields, required: false) 33 | @mutually_exclusive_fields ||= [] 34 | @mutually_exclusive_fields << MutuallyExclusiveField.new( 35 | fields:, 36 | required: 37 | ) 38 | end 39 | 40 | def mutually_exclusive_fields 41 | @mutually_exclusive_fields ||= [] 42 | end 43 | 44 | def validate(*items, &block) 45 | @validations ||= [] 46 | @validations.concat(items) 47 | @validations << block if block 48 | end 49 | 50 | def validations 51 | @validations ||= [] 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/object_factory/field_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | module ObjectFactory 6 | class FieldConfig 7 | def initialize( 8 | input_type: nil, 9 | factory: nil, 10 | required: false, 11 | default: nil, 12 | validate: nil 13 | ) 14 | @given_input_type = input_type 15 | @given_factory = factory 16 | @given_required = required 17 | @given_default = default 18 | @given_validate = validate 19 | end 20 | 21 | def factory? 22 | !given_factory.nil? 23 | end 24 | 25 | def initialize_factory(context, parent_factory = nil) 26 | case given_factory 27 | when Class 28 | given_factory.new(context) 29 | when Symbol 30 | parent_factory.send(given_factory, context) 31 | else 32 | given_factory.call(context) 33 | end 34 | end 35 | 36 | def required? 37 | given_required 38 | end 39 | 40 | def check_input_type(validatable, building_node: false) 41 | return true if !given_input_type || validatable.input.nil? 42 | 43 | if building_node 44 | TypeChecker.raise_on_invalid_type(validatable.context, 45 | type: given_input_type) 46 | else 47 | TypeChecker.validate_type(validatable, type: given_input_type) 48 | end 49 | end 50 | 51 | def validate_field(validatable, building_node: false) 52 | return true if !given_validate || validatable.input.nil? 53 | 54 | run_validation(validatable) 55 | 56 | return validatable.errors.empty? unless building_node 57 | return true if validatable.errors.empty? 58 | 59 | error = validatable.errors.first 60 | location_summary = error.context.location_summary 61 | raise Error::InvalidData, 62 | "Invalid data for #{location_summary}: #{error.message}" 63 | end 64 | 65 | def default(factory = nil) 66 | return given_default.call if given_default.is_a?(Proc) 67 | return factory&.send(given_default) if given_default.is_a?(Symbol) 68 | 69 | given_default 70 | end 71 | 72 | private 73 | 74 | attr_reader :given_input_type, :given_factory, :given_required, 75 | :given_default, :given_validate 76 | 77 | def run_validation(validatable) 78 | if given_validate.is_a?(Symbol) 79 | validatable.factory.send(given_validate, validatable) 80 | else 81 | given_validate.call(validatable) 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/object_factory/node_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | module ObjectFactory 6 | class NodeBuilder 7 | def self.errors(factory) 8 | new(factory).errors 9 | end 10 | 11 | def self.node_data(factory, node_context) 12 | new(factory).node_data(node_context) 13 | end 14 | 15 | def initialize(factory) 16 | @factory = factory 17 | @validatable = Validation::Validatable.new(factory) 18 | end 19 | 20 | def errors 21 | return validatable.collection if empty_and_allowed_to_be? 22 | 23 | TypeChecker.validate_type(validatable, type: ::Hash) 24 | 25 | validatable.add_errors(validate(raise_on_invalid: false)) if validatable.errors.empty? 26 | 27 | validatable.collection 28 | end 29 | 30 | def node_data(node_context) 31 | return build_node_data(node_context) if empty_and_allowed_to_be? 32 | 33 | TypeChecker.raise_on_invalid_type(factory.context, type: ::Hash) 34 | validate(raise_on_invalid: true) 35 | build_node_data(node_context) 36 | end 37 | 38 | private_class_method :new 39 | 40 | private 41 | 42 | attr_reader :factory, :validatable 43 | 44 | def empty_and_allowed_to_be? 45 | factory.nil_input? && factory.can_use_default? 46 | end 47 | 48 | def validate(raise_on_invalid:) 49 | Validator.call(factory, raise_on_invalid:) 50 | end 51 | 52 | def build_node_data(node_context) 53 | return if factory.nil_input? && factory.data.nil? 54 | 55 | factory.data.each_with_object({}) do |(key, value), memo| 56 | memo[key] = resolve_value(key, value, node_context) 57 | end 58 | end 59 | 60 | def resolve_value(key, value, node_context) 61 | resolved = determine_value_or_default(key, value) 62 | 63 | if resolved.respond_to?(:node) 64 | Node::Placeholder.new(value, key, node_context) 65 | else 66 | resolved 67 | end 68 | end 69 | 70 | def determine_value_or_default(key, value) 71 | config = factory.field_configs[key] 72 | 73 | # let a field config default take precedence if value is a nil_input? 74 | if (value.respond_to?(:nil_input?) && value.nil_input?) || value.nil? 75 | default = config&.default(factory) 76 | default.nil? ? value : default 77 | else 78 | value 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/openapi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/node_factory/info" 5 | require "openapi3_parser/node_factory/paths" 6 | require "openapi3_parser/node_factory/components" 7 | require "openapi3_parser/node_factory/external_documentation" 8 | 9 | module Openapi3Parser 10 | module NodeFactory 11 | class Openapi < NodeFactory::Object 12 | allow_extensions 13 | 14 | field "openapi", input_type: String, required: true 15 | field "info", factory: NodeFactory::Info, required: true 16 | field "servers", factory: :servers_factory 17 | field "paths", factory: NodeFactory::Paths, required: true 18 | field "components", factory: NodeFactory::Components 19 | field "security", factory: :security_factory 20 | field "tags", factory: :tags_factory 21 | field "externalDocs", factory: NodeFactory::ExternalDocumentation 22 | 23 | def can_use_default? 24 | false 25 | end 26 | 27 | private 28 | 29 | def build_object(data, context) 30 | Node::Openapi.new(data, context) 31 | end 32 | 33 | def servers_factory(context) 34 | NodeFactory::Array.new(context, 35 | default: [{ "url" => "/" }], 36 | use_default_on_empty: true, 37 | value_factory: NodeFactory::Server) 38 | end 39 | 40 | def security_factory(context) 41 | NodeFactory::Array.new(context, 42 | value_factory: NodeFactory::SecurityRequirement) 43 | end 44 | 45 | def tags_factory(context) 46 | validate_unique_tags = lambda do |validatable| 47 | names = validatable.factory.context.input.map { |i| i["name"] } 48 | return if names.uniq.count == names.count 49 | 50 | dupes = names.find_all { |name| names.count(name) > 1 } 51 | validatable.add_error( 52 | "Duplicate tag names: #{dupes.uniq.join(', ')}" 53 | ) 54 | end 55 | 56 | NodeFactory::Array.new(context, 57 | value_factory: NodeFactory::Tag, 58 | validate: validate_unique_tags) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/optional_reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | class OptionalReference 6 | def initialize(factory) 7 | @factory = factory 8 | end 9 | 10 | def object_type 11 | "#{self.class}[#{factory.object_type}]}" 12 | end 13 | 14 | def call(context) 15 | reference = context.input.is_a?(Hash) && context.input["$ref"] 16 | 17 | if reference 18 | NodeFactory::Reference.new(context, self) 19 | else 20 | factory.new(context) 21 | end 22 | end 23 | 24 | private 25 | 26 | attr_reader :factory 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/node_factory/parameter_like" 5 | 6 | module Openapi3Parser 7 | module NodeFactory 8 | class Parameter < NodeFactory::Object 9 | include ParameterLike 10 | 11 | allow_extensions 12 | 13 | field "name", input_type: String, required: true 14 | field "in", input_type: String, 15 | required: true, 16 | validate: :validate_in 17 | field "description", input_type: String 18 | field "required", input_type: :boolean, default: false 19 | field "deprecated", input_type: :boolean, default: false 20 | field "allowEmptyValue", input_type: :boolean, default: false 21 | 22 | field "style", input_type: String, default: :default_style 23 | field "explode", input_type: :boolean, default: :default_explode 24 | field "allowReserved", input_type: :boolean, default: false 25 | field "schema", factory: :schema_factory 26 | field "example" 27 | field "examples", factory: :examples_factory 28 | 29 | field "content", factory: :content_factory 30 | 31 | mutually_exclusive "example", "examples" 32 | 33 | validate do |validatable| 34 | if validatable.input["in"] == "path" && !validatable.input["required"] 35 | validatable.add_error( 36 | "Must be included and true for a path parameter", 37 | Context.next_field(validatable.context, "required") 38 | ) 39 | end 40 | end 41 | 42 | private 43 | 44 | def build_object(data, context) 45 | Node::Parameter.new(data, context) 46 | end 47 | 48 | def default_style 49 | return "simple" if %w[path header].include?(context.input["in"]) 50 | 51 | "form" 52 | end 53 | 54 | def validate_in(validatable) 55 | return if %w[header query cookie path].include?(validatable.input) 56 | 57 | validatable.add_error("in can only be header, query, cookie, or path") 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/parameter_like.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | module ParameterLike 6 | def default_explode 7 | context.input["style"] == "form" 8 | end 9 | 10 | def schema_factory(context) 11 | factory = NodeFactory::OptionalReference.new(NodeFactory::Schema) 12 | factory.call(context) 13 | end 14 | 15 | def examples_factory(context) 16 | factory = NodeFactory::OptionalReference.new(NodeFactory::Example) 17 | NodeFactory::Map.new(context, 18 | default: nil, 19 | value_factory: factory) 20 | end 21 | 22 | def content_factory(context) 23 | NodeFactory::Map.new(context, 24 | default: nil, 25 | value_factory: NodeFactory::MediaType, 26 | validate: method(:validate_content)) 27 | end 28 | 29 | def validate_content(validatable) 30 | return if validatable.input.size == 1 31 | 32 | validatable.add_error("Must only have one item") 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/paths.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/map" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Paths < NodeFactory::Map 8 | PATH_REGEX = %r{ 9 | \A 10 | # required prefix slash 11 | / 12 | ( 13 | # Match a path 14 | ([\-;_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})* 15 | # Match a path template parameter 16 | ({([\-;_.!~*'()a-zA-Z\d:@&=+$,]|%[a-fA-F\d]{2})+})* 17 | # optional segment separating slash 18 | /? 19 | )* 20 | \Z 21 | }x 22 | 23 | def initialize(context) 24 | factory = NodeFactory::OptionalReference.new(NodeFactory::PathItem) 25 | 26 | super(context, 27 | allow_extensions: true, 28 | value_factory: factory, 29 | validate: :validate) 30 | end 31 | 32 | private 33 | 34 | def build_node(data, node_context) 35 | Node::Paths.new(data, node_context) 36 | end 37 | 38 | def validate(validatable) 39 | paths = validatable.input.keys.grep_v(NodeFactory::EXTENSION_REGEX) 40 | validate_paths(validatable, paths) 41 | end 42 | 43 | def validate_paths(validatable, paths) 44 | invalid_paths = paths.reject { |p| PATH_REGEX.match(p) } 45 | unless invalid_paths.empty? 46 | joined = invalid_paths.map { |p| "'#{p}'" }.join(", ") 47 | validatable.add_error("There are invalid paths: #{joined}") 48 | end 49 | 50 | conflicts = conflicting_paths(paths) 51 | 52 | return if conflicts.empty? 53 | 54 | joined = conflicts.map { |p| "'#{p}'" }.join(", ") 55 | validatable.add_error("There are paths that conflict: #{joined}") 56 | end 57 | 58 | def conflicting_paths(paths) 59 | potential_conflicts = paths.each_with_object({}) do |path, memo| 60 | without_params = path.gsub(/{.*?}/, "") 61 | memo[path] = without_params if path != without_params 62 | end 63 | 64 | grouped_paths = potential_conflicts.group_by(&:last) 65 | .map { |_k, v| v.map(&:first) } 66 | 67 | grouped_paths.select { |group| group.size > 1 }.flatten 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Reference < NodeFactory::Object 8 | field "$ref", input_type: String, required: true, factory: :ref_factory 9 | 10 | attr_reader :factory 11 | 12 | def initialize(context, factory) 13 | @factory = factory 14 | super(context) 15 | end 16 | 17 | def in_recursive_loop? 18 | data["$ref"].self_referencing? 19 | end 20 | 21 | def referenced_factory 22 | data["$ref"].referenced_factory 23 | end 24 | 25 | def resolves?(control_factory = nil) 26 | control_factory ||= self 27 | 28 | return true unless referenced_factory.is_a?(Reference) 29 | # recursive loop of references that never references an object 30 | return false if referenced_factory == control_factory 31 | 32 | referenced_factory.resolves?(control_factory) 33 | end 34 | 35 | def errors 36 | if in_recursive_loop? 37 | @errors ||= Validation::ErrorCollection.new 38 | else 39 | super 40 | end 41 | end 42 | 43 | private 44 | 45 | def build_node(node_context) 46 | TypeChecker.raise_on_invalid_type(context, type: ::Hash) 47 | ObjectFactory::Validator.call(self, raise_on_invalid: true) 48 | data["$ref"].node(node_context) 49 | end 50 | 51 | def ref_factory(context) 52 | NodeFactory::Fields::Reference.new(context, factory) 53 | end 54 | 55 | def build_resolved_input 56 | data["$ref"].resolved_input 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/request_body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class RequestBody < NodeFactory::Object 8 | allow_extensions 9 | field "description", input_type: String 10 | field "content", factory: :content_factory, required: true 11 | field "required", input_type: :boolean, default: false 12 | 13 | private 14 | 15 | def build_object(data, context) 16 | Node::RequestBody.new(data, context) 17 | end 18 | 19 | def content_factory(context) 20 | NodeFactory::Map.new( 21 | context, 22 | value_factory: NodeFactory::MediaType, 23 | validate: ContentValidator 24 | ) 25 | end 26 | 27 | class ContentValidator 28 | def self.call(*args) 29 | new.call(*args) 30 | end 31 | 32 | def call(validatable) 33 | # This validation isn't actually mentioned in the spec, but it 34 | # doesn't seem to make sense if this is an empty hash. 35 | return validatable.add_error("Expected to have at least 1 item") if validatable.input.empty? 36 | 37 | validatable.input.each_key do |key| 38 | message = Validators::MediaType.call(key) 39 | next unless message 40 | 41 | context = Context.next_field(validatable.context, key) 42 | validatable.add_error(message, context) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Response < NodeFactory::Object 8 | allow_extensions 9 | field "description", input_type: String, required: true 10 | field "headers", factory: :headers_factory 11 | field "content", factory: :content_factory 12 | field "links", factory: :links_factory 13 | 14 | private 15 | 16 | def build_object(data, context) 17 | Node::Response.new(data, context) 18 | end 19 | 20 | def headers_factory(context) 21 | factory = NodeFactory::OptionalReference.new(NodeFactory::Header) 22 | NodeFactory::Map.new(context, value_factory: factory) 23 | end 24 | 25 | def content_factory(context) 26 | NodeFactory::Map.new( 27 | context, 28 | validate: method(:validate_content), 29 | value_factory: NodeFactory::MediaType 30 | ) 31 | end 32 | 33 | def links_factory(context) 34 | factory = NodeFactory::OptionalReference.new(NodeFactory::Link) 35 | NodeFactory::Map.new( 36 | context, 37 | validate: Validation::InputValidator.new(Validators::ComponentKeys), 38 | value_factory: factory 39 | ) 40 | end 41 | 42 | def validate_content(validatable) 43 | validatable.input.each_key do |key| 44 | message = Validators::MediaType.call(key) 45 | next unless message 46 | 47 | validatable.add_error(message, 48 | Context.next_field(validatable.context, key)) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/responses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/map" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Responses < NodeFactory::Map 8 | KEY_REGEX = / 9 | \A 10 | ( 11 | default 12 | | 13 | [1-5]([0-9][0-9]|XX) 14 | ) 15 | \Z 16 | /x 17 | 18 | def initialize(context) 19 | factory = NodeFactory::OptionalReference.new(NodeFactory::Response) 20 | 21 | super(context, 22 | allow_extensions: true, 23 | value_factory: factory, 24 | validate: :validate_keys) 25 | end 26 | 27 | private 28 | 29 | def build_node(data, node_context) 30 | Node::Responses.new(data, node_context) 31 | end 32 | 33 | def validate_keys(validatable) 34 | invalid = validatable.input.keys.reject do |key| 35 | NodeFactory::EXTENSION_REGEX.match(key) || 36 | KEY_REGEX.match(key) 37 | end 38 | 39 | return if invalid.empty? 40 | 41 | codes = invalid.map { |k| "'#{k}'" }.join(", ") 42 | validatable.add_error("Invalid responses keys: #{codes} - default, " \ 43 | "status codes and status code ranges allowed") 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/security_requirement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/map" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class SecurityRequirement < NodeFactory::Map 8 | def initialize(context) 9 | super(context, value_factory: NodeFactory::Array) 10 | end 11 | 12 | private 13 | 14 | def build_node(data, node_context) 15 | Node::SecurityRequirement.new(data, node_context) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/security_scheme.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class SecurityScheme < NodeFactory::Object 8 | allow_extensions 9 | 10 | field "type", input_type: String, required: true 11 | field "description", input_type: String 12 | field "name", input_type: String 13 | field "in", input_type: String 14 | field "scheme", input_type: String 15 | field "bearerFormat", input_type: String 16 | field "flows", factory: :flows_factory 17 | field "openIdConnectUrl", input_type: String 18 | 19 | private 20 | 21 | def build_object(data, context) 22 | Node::SecurityScheme.new(data, context) 23 | end 24 | 25 | def flows_factory(context) 26 | NodeFactory::OauthFlows.new(context) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class Server < NodeFactory::Object 8 | allow_extensions 9 | field "url", input_type: String, required: true 10 | field "description", input_type: String 11 | field "variables", factory: :variables_factory 12 | 13 | private 14 | 15 | def build_object(data, context) 16 | Node::Server.new(data, context) 17 | end 18 | 19 | def variables_factory(context) 20 | NodeFactory::Map.new( 21 | context, 22 | value_factory: NodeFactory::ServerVariable 23 | ) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/server_variable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | 5 | module Openapi3Parser 6 | module NodeFactory 7 | class ServerVariable < NodeFactory::Object 8 | allow_extensions 9 | field "enum", factory: :enum_factory 10 | field "default", input_type: String, required: true 11 | field "description", input_type: String 12 | 13 | private 14 | 15 | def enum_factory(context) 16 | NodeFactory::Array.new( 17 | context, 18 | default: nil, 19 | value_input_type: String, 20 | validate: lambda do |validatable| 21 | return if validatable.input.any? 22 | 23 | validatable.add_error("Expected atleast one value") 24 | end 25 | ) 26 | end 27 | 28 | def build_object(data, context) 29 | Node::ServerVariable.new(data, context) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/node_factory/external_documentation" 5 | 6 | module Openapi3Parser 7 | module NodeFactory 8 | class Tag < NodeFactory::Object 9 | allow_extensions 10 | field "name", input_type: String, required: true 11 | field "description", input_type: String 12 | field "externalDocs", factory: NodeFactory::ExternalDocumentation 13 | 14 | private 15 | 16 | def build_object(data, context) 17 | Node::Tag.new(data, context) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/type_checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module NodeFactory 5 | class TypeChecker 6 | def self.validate_type(validatable, type:, context: nil) 7 | new(type).validate_type(validatable, context) 8 | end 9 | 10 | def self.raise_on_invalid_type(context, type:) 11 | new(type).raise_on_invalid_type(context) 12 | end 13 | 14 | def self.validate_keys(validatable, type:, context: nil) 15 | new(type).validate_keys(validatable, context) 16 | end 17 | 18 | def self.raise_on_invalid_keys(context, type:) 19 | new(type).raise_on_invalid_keys(context) 20 | end 21 | 22 | private_class_method :new 23 | 24 | def initialize(type) 25 | @type = type 26 | end 27 | 28 | def validate_type(validatable, context) 29 | return true unless type 30 | 31 | context ||= validatable.context 32 | valid_type?(context.input).tap do |valid| 33 | next if valid 34 | 35 | validatable.add_error("Invalid type. #{field_error_message}", 36 | context) 37 | end 38 | end 39 | 40 | def validate_keys(validatable, context) 41 | return true unless type 42 | 43 | context ||= validatable.context 44 | valid_keys?(context.input).tap do |valid| 45 | next if valid 46 | 47 | validatable.add_error("Invalid keys. #{keys_error_message}", 48 | context) 49 | end 50 | end 51 | 52 | def raise_on_invalid_type(context) 53 | return true if !type || valid_type?(context.input) 54 | 55 | raise Error::InvalidType, 56 | "Invalid type for #{context.location_summary}: " \ 57 | "#{field_error_message}" 58 | end 59 | 60 | def raise_on_invalid_keys(context) 61 | return true if !type || valid_keys?(context.input) 62 | 63 | raise Error::InvalidType, 64 | "Invalid keys for #{context.location_summary}: " \ 65 | "#{keys_error_message}" 66 | end 67 | 68 | private 69 | 70 | attr_reader :type 71 | 72 | def valid_type?(input) 73 | return [true, false].include?(input) if type == :boolean 74 | 75 | unless type.is_a?(Class) 76 | raise Error::UnvalidatableType, 77 | "Expected #{type} to be a Class not a #{type.class}" 78 | end 79 | 80 | input.is_a?(type) 81 | end 82 | 83 | def field_error_message 84 | "Expected #{type_name_for_error}" 85 | end 86 | 87 | def keys_error_message 88 | "Expected keys to be of type #{type_name_for_error}" 89 | end 90 | 91 | def type_name_for_error 92 | if type == Hash 93 | "Object" 94 | elsif type == :boolean 95 | "Boolean" 96 | else 97 | type.to_s 98 | end 99 | end 100 | 101 | def valid_keys?(input) 102 | input.keys.all? { |key| valid_type?(key) } 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/openapi3_parser/node_factory/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/node_factory/object" 4 | require "openapi3_parser/validation/input_validator" 5 | require "openapi3_parser/validators/absolute_uri" 6 | 7 | module Openapi3Parser 8 | module NodeFactory 9 | class Xml < NodeFactory::Object 10 | allow_extensions 11 | field "name", input_type: String 12 | field "namespace", 13 | input_type: String, 14 | validate: Validation::InputValidator.new(Validators::AbsoluteUri) 15 | field "prefix", input_type: String 16 | field "attribute", input_type: :boolean, default: false 17 | field "wrapped", input_type: :boolean, default: false 18 | 19 | private 20 | 21 | def build_object(data, context) 22 | Node::Xml.new(data, context) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source/location.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | class Source 7 | # Class used to represent a location within an OpenAPI document. 8 | # It contains a source, which is the source file/data used for the contents 9 | # and the pointer which indicates where in the object like file the data is 10 | class Location 11 | extend Forwardable 12 | 13 | def self.next_field(location, field) 14 | new(location.source, location.pointer.segments + [field]) 15 | end 16 | 17 | def_delegators :pointer, :root? 18 | attr_reader :source, :pointer 19 | 20 | # @param [Openapi3Parser::Source] source 21 | # @param [::Array] pointer_segments 22 | def initialize(source, pointer_segments) 23 | @source = source 24 | @pointer = Pointer.new(pointer_segments.freeze) 25 | end 26 | 27 | def ==(other) 28 | return false unless other.instance_of?(self.class) 29 | 30 | source == other.source && pointer == other.pointer 31 | end 32 | 33 | def to_s 34 | source.relative_to_root + pointer.fragment 35 | end 36 | 37 | def data 38 | source.data_at_pointer(pointer.segments) 39 | end 40 | 41 | def pointer_defined? 42 | source.has_pointer?(pointer.segments) 43 | end 44 | 45 | def source_available? 46 | source.available? 47 | end 48 | 49 | def inspect 50 | %{#{self.class.name}(source: #{source.inspect}, pointer: #{pointer})} 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source/reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cgi" 4 | 5 | module Openapi3Parser 6 | class Source 7 | # An object which represents a reference that can be indicated in a OpenAPI 8 | # file. Given a string reference it can be used to answer key questions 9 | # that aid in resolving the reference 10 | # 11 | # e.g. 12 | # r = Openapi3Parser::Source::Reference.new("test.yaml#/path/to/item") 13 | # 14 | # r.only_fragment? 15 | # => false 16 | # 17 | # r.rsource_uri 18 | # => "test.yaml" 19 | class Reference 20 | # @param [String] reference reference from an OpenAPI file 21 | def initialize(reference) 22 | @given_reference = reference 23 | end 24 | 25 | def to_s 26 | given_reference.to_s 27 | end 28 | 29 | def only_fragment? 30 | resource_uri.to_s == "" 31 | end 32 | 33 | # @return [String, nil] 34 | def fragment 35 | uri.fragment 36 | end 37 | 38 | # @return [URI] 39 | def resource_uri 40 | uri_without_fragment 41 | end 42 | 43 | def absolute? 44 | uri.absolute? 45 | end 46 | 47 | # @return [::Array] an array of strings of the components in the fragment 48 | def json_pointer 49 | @json_pointer ||= (fragment || "").split("/").drop(1).map do |field| 50 | CGI.unescape(field.gsub("+", "%20")) 51 | end 52 | end 53 | 54 | private 55 | 56 | attr_reader :given_reference 57 | 58 | def uri 59 | @uri = URI.parse(given_reference) 60 | end 61 | 62 | def uri_without_fragment 63 | @uri_without_fragment ||= uri.dup.tap { |u| u.fragment = nil } 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source/resolved_reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | class Source 7 | class ResolvedReference 8 | extend Forwardable 9 | 10 | def_delegators :source_location, :source 11 | def_delegators :factory, :resolved_input, :node 12 | 13 | attr_reader :source_location, :object_type 14 | 15 | def initialize(source_location:, 16 | object_type:, 17 | reference_registry:) 18 | @source_location = source_location 19 | @object_type = object_type 20 | @reference_registry = reference_registry 21 | end 22 | 23 | def valid? 24 | errors.empty? 25 | end 26 | 27 | def errors 28 | @errors ||= Array(build_errors) 29 | end 30 | 31 | def factory 32 | @factory ||= reference_registry.factory(object_type, source_location).tap do |factory| 33 | message = "Unregistered node factory at #{source_location}" 34 | raise Openapi3Parser::Error, message unless factory 35 | end 36 | end 37 | 38 | private 39 | 40 | attr_reader :reference_registry 41 | 42 | def build_errors 43 | return source_unavailabe_error unless source.available? 44 | return pointer_missing_error unless source_location.pointer_defined? 45 | 46 | resolution_error unless factory.valid? 47 | end 48 | 49 | def source_unavailabe_error 50 | "Failed to open source #{source.relative_to_root}" 51 | end 52 | 53 | def pointer_missing_error 54 | suffix = source.root? ? "" : " in source #{source.relative_to_root}" 55 | "#{source_location.pointer} is not defined#{suffix}" 56 | end 57 | 58 | def resolution_error 59 | "#{source_location} does not resolve to a valid object" 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | # An abstract class which is used to provide a foundation for classes that 5 | # represent the different means of input an OpenAPI document can have. It is 6 | # used to represent the underlying source of the data which is used as a 7 | # source within an OpenAPI document. 8 | # 9 | # @see SourceInput::Raw SourceInput::Raw for an input that is done through 10 | # data, such as a Hash. 11 | # 12 | # @see SourceInput::File SourceInput::File for an input that is done through 13 | # the path to a file within the local file system. 14 | # 15 | # @see SourceInput::Url SourceInput::Url for an input that is done through] 16 | # a URL to an OpenAPI Document 17 | # 18 | # @attr_reader [Error::InaccessibleInput, nil] access_error 19 | # @attr_reader [Error::UnparsableInput, nil] parse_error 20 | class SourceInput 21 | attr_reader :access_error, :parse_error 22 | 23 | def initialize 24 | return if access_error 25 | 26 | @contents = parse_contents 27 | rescue ::StandardError => e 28 | @parse_error = Error::UnparsableInput.new(e.message) 29 | end 30 | 31 | # Indicates that the data within this input is suitable (i.e. can parse 32 | # underlying JSON or YAML) for trying to use as part of a Document 33 | def available? 34 | access_error.nil? && parse_error.nil? 35 | end 36 | 37 | # For a given reference use the context of the current SourceInput to 38 | # determine which file is required for the reference. This allows 39 | # references to use relative file paths because we can combine them witt 40 | # the current SourceInput location to determine the next one 41 | def resolve_next(_reference); end 42 | 43 | # Used to determine whether a different instance of SourceInput is 44 | # the same file/data 45 | def ==(_other); end 46 | 47 | # The parsed data from the input 48 | # 49 | # @raise [Error::InaccessibleInput] In cases where the file does not exist 50 | # @raise [Error::UnparsableInput] In cases where the data is not parsable 51 | # 52 | # @return Object 53 | def contents 54 | raise access_error if access_error 55 | raise parse_error if parse_error 56 | 57 | @contents 58 | end 59 | 60 | # The relative path, if possible, for this source_input compared to a 61 | # different one. Defaults to empty string and should be specialised in 62 | # subclasses 63 | # 64 | # @return [String] 65 | def relative_to(_source_input) 66 | "" 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source_input/raw.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/source_input" 4 | 5 | module Openapi3Parser 6 | class SourceInput 7 | # An input of data (typically a Hash) to for initialising an OpenAPI 8 | # document. Most likely used in development scenarios when you want to 9 | # test things without creating/tweaking an OpenAPI source file 10 | # 11 | # @attr_reader [Object] raw_input The data for the document 12 | # @attr_reader [String, nil] base_url A url to be used for 13 | # resolving relative 14 | # references 15 | # @attr_reader [String, nil] working_directory A path to be used for 16 | # resolving relative 17 | # references 18 | class Raw < SourceInput 19 | attr_reader :raw_input, :base_url, :working_directory 20 | 21 | # @param [Object] raw_input 22 | # @param [String, nil] base_url 23 | # @param [String, nil] working_directory 24 | def initialize(raw_input, base_url: nil, working_directory: nil) 25 | @raw_input = raw_input 26 | @base_url = base_url 27 | working_directory ||= resolve_working_directory 28 | @working_directory = ::File.absolute_path(working_directory) 29 | super() 30 | end 31 | 32 | # @see SourceInput#resolve_next 33 | # @param [Source::Reference] reference 34 | # @return [SourceInput] 35 | def resolve_next(reference) 36 | ResolveNext.call(reference, 37 | self, 38 | base_url:, 39 | working_directory:) 40 | end 41 | 42 | # @see SourceInput#other 43 | # @param [SourceInput] other 44 | # @return [Boolean] 45 | def ==(other) 46 | return false unless other.instance_of?(self.class) 47 | 48 | raw_input == other.raw_input && 49 | base_url == other.base_url && 50 | working_directory == other.working_directory 51 | end 52 | 53 | # return [String] 54 | def inspect 55 | %{#{self.class.name}(input: #{raw_input.inspect}, base_url: } + 56 | %{#{base_url}, working_directory: #{working_directory})} 57 | end 58 | 59 | # @return [String] 60 | def to_s 61 | raw_input.to_s 62 | end 63 | 64 | private 65 | 66 | def resolve_working_directory 67 | if raw_input.respond_to?(:path) 68 | ::File.dirname(raw_input) 69 | else 70 | Dir.pwd 71 | end 72 | end 73 | 74 | def parse_contents 75 | return raw_input if raw_input.respond_to?(:keys) 76 | 77 | StringParser.call( 78 | input_to_string(raw_input), 79 | raw_input.respond_to?(:path) ? ::File.basename(raw_input.path) : nil 80 | ) 81 | end 82 | 83 | def input_to_string(input) 84 | return input.read if input.respond_to?(:read) 85 | return input.to_s if input.respond_to?(:to_s) 86 | 87 | input 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source_input/resolve_next.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | class SourceInput 5 | class ResolveNext 6 | # @param reference [Source::Reference] 7 | # @param current_source_input [SourceInput] 8 | # @param base_url [String, nil] 9 | # @param working_directory [String, nil] 10 | # @return [SourceInput] 11 | def self.call(reference, 12 | current_source_input, 13 | base_url: nil, 14 | working_directory: nil) 15 | new(reference, current_source_input, base_url, working_directory) 16 | .source_input 17 | end 18 | 19 | def initialize(reference, 20 | current_source_input, 21 | base_url, 22 | working_directory) 23 | @reference = reference 24 | @current_source_input = current_source_input 25 | @base_url = base_url 26 | @working_directory = working_directory 27 | end 28 | 29 | private_class_method :new 30 | 31 | def source_input 32 | return current_source_input if reference.only_fragment? 33 | 34 | if reference.absolute? 35 | SourceInput::Url.new(reference.resource_uri) 36 | else 37 | base_url ? url_source_input : file_source_input 38 | end 39 | end 40 | 41 | private 42 | 43 | attr_reader :reference, :current_source_input, :base_url, 44 | :working_directory 45 | 46 | def url_source_input 47 | url = URI.join(base_url, reference.resource_uri) 48 | SourceInput::Url.new(url) 49 | end 50 | 51 | def file_source_input 52 | path = reference.resource_uri.path 53 | return SourceInput::File.new(path) if path[0] == "/" 54 | 55 | expanded_path = ::File.expand_path(path, working_directory) 56 | SourceInput::File.new(expanded_path) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/openapi3_parser/source_input/string_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "psych" 4 | require "json" 5 | 6 | module Openapi3Parser 7 | class SourceInput 8 | class StringParser 9 | def self.call(input, filename = nil) 10 | new(input, filename).call 11 | end 12 | 13 | def initialize(input, filename) 14 | @input = input 15 | @filename = filename 16 | end 17 | 18 | def call 19 | json? ? parse_json : parse_yaml 20 | end 21 | 22 | private_class_method :new 23 | 24 | private 25 | 26 | attr_reader :input, :filename 27 | 28 | def json? 29 | return false if filename && ::File.extname(filename) == ".yaml" 30 | 31 | json_filename = filename && ::File.extname(filename) == ".json" 32 | json_filename || input.strip[0] == "{" 33 | end 34 | 35 | def parse_json 36 | JSON.parse(input) 37 | end 38 | 39 | def parse_yaml 40 | Psych.safe_load(input, permitted_classes: [Date, Time], aliases: true) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validation/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module Openapi3Parser 6 | module Validation 7 | # Represents a validation error for an OpenAPI document 8 | # @attr_reader [String] message The error message 9 | # @attr_reader [Context] context The context where this was 10 | # validated 11 | # @attr_reader [Class, nil] factory_class The NodeFactory that was being 12 | # created when this error was found 13 | class Error 14 | extend Forwardable 15 | 16 | attr_reader :message, :context, :factory_class 17 | 18 | # @!method source_location 19 | # The source file and pointer for where this error occurred 20 | # @return [Context::Location] 21 | def_delegator :context, :source_location 22 | 23 | alias to_s message 24 | 25 | # @param [String] message 26 | # @param [Context] context 27 | # @param [Class, nil] factory_class 28 | def initialize(message, context, factory_class = nil) 29 | @message = message 30 | @context = context 31 | @factory_class = factory_class 32 | end 33 | 34 | # @return [String, nil] 35 | def for_type 36 | return unless factory_class 37 | return "(anonymous)" unless factory_class.name 38 | 39 | factory_class.name.split("::").last 40 | end 41 | 42 | # @return [String] 43 | def inspect 44 | "#{self.class.name}(message: #{message}, context: #{context}, " \ 45 | "for_type: #{for_type})" 46 | end 47 | 48 | # @return [Boolean] 49 | def ==(other) 50 | return false unless other.instance_of?(self.class) 51 | 52 | message == other.message && 53 | context == other.context && 54 | factory_class == other.factory_class 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validation/error_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validation 5 | # An immutable collection of Validation::Error objects 6 | # @attr_reader [Array] errors 7 | class ErrorCollection 8 | include Enumerable 9 | 10 | # Combines ErrorCollection objects or arrays of Validation::Error objects 11 | # @param [ErrorCollection, Array] errors 12 | # @param [ErrorCollection, Array] other_errors 13 | # @return [ErrorCollection] 14 | def self.combine(errors, other_errors) 15 | new(errors.to_a + other_errors.to_a) 16 | end 17 | 18 | attr_reader :errors 19 | alias to_a errors 20 | 21 | # @param [Array] errors 22 | def initialize(errors = []) 23 | @errors = errors.freeze 24 | end 25 | 26 | def empty? 27 | errors.empty? 28 | end 29 | 30 | def each(&) 31 | errors.each(&) 32 | end 33 | 34 | # Group errors by those in the same location for the same node 35 | # 36 | # @return [Array 1) 59 | memo[key] = memo.fetch(key, []) + item.errors.map(&:to_s) 60 | end 61 | end 62 | end 63 | 64 | # @return [String] 65 | def inspect 66 | "#{self.class.name}(errors: #{to_h})" 67 | end 68 | 69 | LocationTypeGroup = Struct.new(:source_location, :for_type, :errors) do 70 | def location_summary(with_type: false) 71 | string = source_location.to_s 72 | string << " (as #{for_type})" if with_type && !for_type&.empty? 73 | string 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validation/input_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validation 5 | class InputValidator 6 | attr_reader :callable 7 | 8 | def initialize(callable) 9 | @callable = callable 10 | end 11 | 12 | def call(validatable) 13 | error = callable.call(validatable.input) 14 | validatable.add_error(error) if error 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validation/validatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validation 5 | class Validatable 6 | attr_reader :context, :errors, :factory 7 | 8 | UNDEFINED = Class.new 9 | 10 | def initialize(factory, context: nil) 11 | @factory = factory 12 | @context = context || factory.context 13 | @errors = [] 14 | end 15 | 16 | def input 17 | context.input 18 | end 19 | 20 | def add_error(error, given_context = nil, factory_class = UNDEFINED) 21 | return unless error 22 | return @errors << error if error.is_a?(Validation::Error) 23 | 24 | @errors << Validation::Error.new( 25 | error, 26 | given_context || context, 27 | factory_class == UNDEFINED ? factory.class : factory_class 28 | ) 29 | end 30 | 31 | def add_errors(errors) 32 | errors = errors.to_a if errors.respond_to?(:to_a) 33 | errors.each { |e| add_error(e) } 34 | end 35 | 36 | def collection 37 | ErrorCollection.new(errors) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/absolute_uri.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class AbsoluteUri 6 | def self.call(input) 7 | uri = URI.parse(input) 8 | %("#{input}" is not a absolute URI) unless uri.absolute? 9 | rescue URI::InvalidURIError 10 | %("#{input}" is not a valid URI) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/component_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | # This validates that the keys of an object match the format of those 6 | # defined for a Components node. 7 | # As defined: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#components-object 8 | class ComponentKeys 9 | REGEX = /\A[a-zA-Z0-9.\-_]+\Z/ 10 | 11 | def self.call(input) 12 | invalid = input.keys.reject { |key| REGEX.match(key) } 13 | "Contains invalid keys: #{invalid.join(', ')}" unless invalid.empty? 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/duplicate_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class DuplicateParameters 6 | def self.call(resolved_input) 7 | new.call(resolved_input) 8 | end 9 | 10 | def call(resolved_input) 11 | dupes = duplicate_names_by_in(resolved_input) 12 | message(dupes) unless dupes.empty? 13 | end 14 | 15 | private 16 | 17 | def duplicate_names_by_in(resolved_input) 18 | potential_items = resolved_input.reject do |item| 19 | next true unless item.respond_to?(:keys) 20 | 21 | item["name"].nil? || item["in"].nil? 22 | end 23 | 24 | potential_items.group_by { |item| [item["name"], item["in"]] } 25 | .delete_if { |_, group| group.size < 2 } 26 | .keys 27 | end 28 | 29 | def message(dupes) 30 | grouped = dupes.map { |d| "#{d.first} in #{d.last}" }.join(", ") 31 | "Duplicate parameters: #{grouped}" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class Email 6 | # Regex is sourced from HTML specification: 7 | # https://html.spec.whatwg.org/#e-mail-state-(type=email) 8 | REGEX = %r{ 9 | \A 10 | [a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+ 11 | @ 12 | [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? 13 | (?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)* 14 | \Z 15 | }x 16 | 17 | def self.call(input) 18 | message = %("#{input}" is not a valid email address) 19 | message unless REGEX.match(input) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/media_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class MediaType 6 | REGEX = %r{ 7 | \A 8 | (\w+|\*) # word or asterisk 9 | / # separating slash 10 | ([-+.\w]+|\*) # word (with +, - & .) or asterisk 11 | \Z 12 | }x 13 | 14 | def self.call(input) 15 | message = %("#{input}" is not a valid media type) 16 | message unless REGEX.match(input) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class Reference 6 | def initialize(given_reference) 7 | @given_reference = given_reference 8 | end 9 | 10 | def valid? 11 | errors.empty? 12 | end 13 | 14 | def errors 15 | @errors ||= Array(build_errors) 16 | end 17 | 18 | private 19 | 20 | attr_reader :given_reference 21 | 22 | def build_errors 23 | return "Expected a string" unless given_reference.is_a?(String) 24 | 25 | begin 26 | uri = URI.parse(given_reference) 27 | rescue URI::Error 28 | return "Could not parse as a URI" 29 | end 30 | check_fragment(uri) || [] 31 | end 32 | 33 | def check_fragment(uri) 34 | return if uri.fragment.nil? || uri.fragment.empty? 35 | 36 | first_char = uri.fragment[0] 37 | 38 | "Invalid JSON pointer, expected a root slash" if first_char != "/" 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/required_fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/array_sentence" 4 | 5 | module Openapi3Parser 6 | module Validators 7 | class RequiredFields 8 | using ArraySentence 9 | private_class_method :new 10 | 11 | def self.call(*args, **kwargs) 12 | new.call(*args, **kwargs) 13 | end 14 | 15 | def call(validatable, 16 | required_fields:, 17 | raise_on_invalid: true) 18 | input = validatable.input 19 | missing_fields = required_fields.select { |name| input[name].nil? } 20 | 21 | return if missing_fields.empty? 22 | 23 | if raise_on_invalid 24 | location_summary = validatable.context.location_summary 25 | raise Openapi3Parser::Error::MissingFields, 26 | "Missing required fields for " \ 27 | "#{location_summary}: #{missing_fields.sentence_join}" 28 | else 29 | validatable.add_error( 30 | "Missing required fields: #{missing_fields.sentence_join}" 31 | ) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/unexpected_fields.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openapi3_parser/array_sentence" 4 | 5 | module Openapi3Parser 6 | module Validators 7 | class UnexpectedFields 8 | using ArraySentence 9 | private_class_method :new 10 | 11 | def self.call(*args, **kwargs) 12 | new.call(*args, **kwargs) 13 | end 14 | 15 | def call(validatable, 16 | allowed_fields:, 17 | allow_extensions: true, 18 | raise_on_invalid: true) 19 | fields = unexpected_fields(validatable.input, 20 | allowed_fields, 21 | allow_extensions) 22 | return if fields.empty? 23 | 24 | if raise_on_invalid 25 | location_summary = validatable.context.location_summary 26 | raise Openapi3Parser::Error::UnexpectedFields, 27 | "Unexpected fields for #{location_summary}: " \ 28 | "#{fields.sentence_join}" 29 | else 30 | validatable.add_error( 31 | "Unexpected fields: #{fields.sentence_join}" 32 | ) 33 | end 34 | end 35 | 36 | private 37 | 38 | def unexpected_fields(input, allowed_fields, allow_extensions) 39 | extra_keys = input.keys - allowed_fields 40 | return extra_keys unless allow_extensions 41 | 42 | extra_keys.grep_v(NodeFactory::EXTENSION_REGEX) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/openapi3_parser/validators/url.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | module Validators 5 | class Url 6 | def self.call(input) 7 | URI.parse(input) && nil 8 | rescue URI::InvalidURIError 9 | %("#{input}" is not a valid URL) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/openapi3_parser/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Openapi3Parser 4 | VERSION = "0.10.1" 5 | end 6 | -------------------------------------------------------------------------------- /openapi3_parser.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require "openapi3_parser/version" 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = "openapi3_parser" 10 | spec.version = Openapi3Parser::VERSION 11 | spec.author = "Kevin Dew" 12 | spec.email = "kevindew@me.com" 13 | spec.metadata = { "rubygems_mfa_required" => "true" } 14 | 15 | spec.summary = "An OpenAPI V3 parser for Ruby" 16 | spec.description = <<-DESCRIPTION 17 | A tool to parse and validate OpenAPI V3 files. 18 | Aims to provide complete compatibility with the OpenAPI specification and 19 | to provide a natural, idiomatic way to interact with a openapi.yaml file. 20 | DESCRIPTION 21 | spec.homepage = "https://github.com/kevindew/openapi_parser" 22 | spec.license = "MIT" 23 | 24 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 25 | f.match(%r{^spec/}) 26 | end 27 | 28 | spec.required_ruby_version = ">= 3.1" 29 | 30 | spec.add_dependency "commonmarker", ">= 1.0" 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/open_a_document_with_cross_document_references_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open a document with cross document references" do 4 | let(:document) { Openapi3Parser.load(input) } 5 | 6 | let(:input) do 7 | { 8 | openapi: "3.0.0", 9 | info: { 10 | title: "Test Document", 11 | version: "1.0.0" 12 | }, 13 | paths: {}, 14 | components: { 15 | examples: { 16 | test: { "$ref": "http://example.com/#/test" } 17 | } 18 | } 19 | } 20 | end 21 | 22 | let(:remote_input) do 23 | { 24 | test: { 25 | summary: "A foo example", 26 | value: { foo: "bar" } 27 | } 28 | } 29 | end 30 | 31 | before do 32 | stub_request(:get, "http://example.com/") 33 | .to_return(body: remote_input.to_json) 34 | end 35 | 36 | it "is a valid document" do 37 | expect(document).to be_valid 38 | end 39 | 40 | it "can access the summary" do 41 | expect(document.components.examples["test"].summary).to eq "A foo example" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/integration/open_a_document_with_defaults_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open a document with defaults" do 4 | let(:document) { Openapi3Parser.load(input) } 5 | 6 | let(:input) do 7 | { 8 | openapi: "3.0.0", 9 | info: { 10 | title: "Test Document", 11 | version: "1.0.0" 12 | }, 13 | paths: { 14 | "/path": { 15 | get: { 16 | responses: { 17 | default: { 18 | description: "Get response", 19 | content: { 20 | "application/json": { 21 | example: "test" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | components: { 30 | schemas: { 31 | my_schema: { 32 | title: "My Schema" 33 | } 34 | } 35 | } 36 | } 37 | end 38 | 39 | it "is a valid document" do 40 | expect(document).to be_valid 41 | end 42 | 43 | it "has nil values for objects without defaults" do 44 | expect(document.info.contact).to be_nil 45 | end 46 | 47 | it "has nil values for arrays that don't need a value" do 48 | expect(document.components.schemas["my_schema"].required).to be_nil 49 | end 50 | 51 | it "has nil values for objects that default to nil" do 52 | media_type = document.paths["/path"] 53 | .get 54 | .responses["default"] 55 | .content["application/json"] 56 | expect(media_type.examples).to be_nil 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration/open_a_document_with_recursive_references_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open a document with recursive references" do 4 | let(:document) { Openapi3Parser.load(input) } 5 | 6 | let(:input) do 7 | { 8 | openapi: "3.0.0", 9 | info: { 10 | title: "Test Document", 11 | version: "1.0.0" 12 | }, 13 | paths: {}, 14 | components: { 15 | schemas: { 16 | RecursiveItem: { 17 | type: "object", 18 | properties: { 19 | links: { 20 | type: "array", 21 | items: { "$ref": "#/components/schemas/RecursiveItem" } 22 | }, 23 | directly_recursive: { 24 | "$ref": "#/components/schemas/RecursiveItem" 25 | }, 26 | indirectly_recursive: { 27 | "$ref": "#/components/schemas/IndirectlyRecursiveItem" 28 | } 29 | } 30 | }, 31 | IndirectlyRecursiveItem: { 32 | type: "object", 33 | properties: { 34 | recursive_item: { "$ref": "#/components/schemas/RecursiveItem" } 35 | } 36 | }, 37 | RecursiveArray: { 38 | oneOf: [ 39 | { "$ref": "#/components/schemas/RecursiveArray" }, 40 | { "$ref": "#/components/schemas/RecursiveItem" }, 41 | { "$ref": "#/components/schemas/IndirectlyRecursiveItem" } 42 | ] 43 | } 44 | } 45 | } 46 | } 47 | end 48 | 49 | it "is a valid document" do 50 | expect(document).to be_valid 51 | end 52 | 53 | it "doesn't raise an error accessing the root" do 54 | expect { document.root }.not_to raise_error 55 | end 56 | 57 | it "returns the expected node class for a recursive object" do 58 | node = document.components 59 | .schemas["RecursiveItem"] 60 | .properties["links"] 61 | .items 62 | .properties["links"] 63 | .items 64 | expect(node).to be_a(Openapi3Parser::Node::Schema) 65 | end 66 | 67 | it "returns the expected node class for a directly recursive property" do 68 | node = document.components 69 | .schemas["RecursiveItem"] 70 | .properties["directly_recursive"] 71 | .properties["directly_recursive"] 72 | expect(node).to be_a(Openapi3Parser::Node::Schema) 73 | end 74 | 75 | it "returns the expected node class for an indirectly recursive property" do 76 | node = document.components 77 | .schemas["RecursiveItem"] 78 | .properties["indirectly_recursive"] 79 | .properties["recursive_item"] 80 | expect(node).to be_a(Openapi3Parser::Node::Schema) 81 | end 82 | 83 | it "returns the expected node class for a recursive item in an array" do 84 | node = document.components 85 | .schemas["RecursiveArray"] 86 | .one_of[0] 87 | expect(node).to be_a(Openapi3Parser::Node::Schema) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/integration/open_a_yaml_document_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open a YAML Document" do 4 | let(:document) { Openapi3Parser.load_file(path) } 5 | let(:path) { File.join(__dir__, "..", "support", "examples", "uber.yaml") } 6 | 7 | it "is a valid document" do 8 | expect(document).to be_valid 9 | end 10 | 11 | it "can access the version" do 12 | expect(document.openapi).to eq "3.0.0" 13 | end 14 | 15 | it "can access the summary of the products path" do 16 | summary = document.paths["/products"].get.summary 17 | expect(summary).to eq "Product Types" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/integration/open_a_yaml_document_with_dates_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This is to test that YAML doesn't blow up when we encounter a Date or Time 4 | # (which are valid types in YAML) - these should be avoided however as these 5 | # are expected to be strings. 6 | RSpec.describe "Open a YAML Document with dates" do 7 | let(:document) { Openapi3Parser.load_url(url) } 8 | let(:url) { "http://example.com/openapi.yml" } 9 | let(:body) do 10 | <<~HEREDOC 11 | --- 12 | openapi: 3.0.1 13 | info: 14 | title: 2017-02-03T17:43:22.000Z 15 | other: 2017-02-03 16 | version: 1.0.0 17 | paths: {} 18 | HEREDOC 19 | end 20 | 21 | before do 22 | stub_request(:get, "example.com/openapi.yml") 23 | .to_return(body:) 24 | end 25 | 26 | it "is not a valid document" do 27 | expect(document).not_to be_valid 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/integration/open_a_yaml_url_document_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open a YAML Document via URL" do 4 | let(:document) { Openapi3Parser.load_url(url) } 5 | let(:url) { "http://example.com/openapi.yml" } 6 | 7 | before do 8 | path = File.join( 9 | __dir__, "..", "support", "examples", "petstore-expanded.yaml" 10 | ) 11 | stub_request(:get, "example.com/openapi.yml") 12 | .to_return(body: File.read(path)) 13 | end 14 | 15 | it "is a valid document" do 16 | expect(document).to be_valid 17 | end 18 | 19 | it "can access the version" do 20 | expect(document.openapi).to eq "3.0.0" 21 | end 22 | 23 | it "can access the summary of the products path" do 24 | operation_id = document.paths["/pets"].get.operation_id 25 | expect(operation_id).to eq "findPets" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/open_an_invalid_document_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Open an invalid document" do 4 | let(:document) { Openapi3Parser.load(input) } 5 | 6 | let(:input) do 7 | { 8 | openapi: "3.0.0", 9 | info: { 10 | title: "Test Document", 11 | version: "1.0.0" 12 | }, 13 | paths: {}, 14 | components: { 15 | examples: { 16 | test: { extra: "field" } 17 | } 18 | } 19 | } 20 | end 21 | 22 | it "isn't a valid document" do 23 | expect(document).not_to be_valid 24 | end 25 | 26 | it "raises an exception accessing the erroneous node" do 27 | expect { document.openapi }.not_to raise_error 28 | expect { document.components.examples["test"] } 29 | .to raise_error(Openapi3Parser::Error) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/cautious_dig_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::CautiousDig do 4 | describe ".call" do 5 | it "retuns the value when passed an existent segment" do 6 | expect(described_class.call({ "test" => ["value"] }, "test", 0)) 7 | .to be("value") 8 | end 9 | 10 | it "retuns nil when passed a non-existent segment" do 11 | expect(described_class.call({ "test" => ["value"] }, "not_test", 0)) 12 | .to be_nil 13 | end 14 | 15 | it "resolves symbol hash keys when passed a string" do 16 | expect(described_class.call({ symbol: "value" }, "symbol")) 17 | .to be("value") 18 | end 19 | 20 | it "resolves an array key when passed as a string" do 21 | expect(described_class.call(%w[zero one two], "1")) 22 | .to be("one") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/markdown_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Markdown do 4 | describe ".to_html" do 5 | it "converts markdown to HTML" do 6 | expect(described_class.to_html("Text")).to eq("

Text

\n") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::Array do 4 | it_behaves_like "node equality", [] 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/map_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::Map do 4 | it_behaves_like "node equality", [] 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/object_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::Object do 4 | describe "#node_at" do 5 | let(:instance) { described_class.new(data, context) } 6 | let(:data) { {} } 7 | let(:context) do 8 | create_node_context( 9 | {}, 10 | document_input: { 11 | "openapi" => "3.0.0", 12 | "info" => { 13 | "title" => "Minimal Openapi definition", 14 | "version" => "1.0.0" 15 | }, 16 | "paths" => {} 17 | }, 18 | pointer_segments: %w[info] 19 | ) 20 | end 21 | 22 | it "can find a node via an absolute path" do 23 | expect(instance.node_at("#/paths")) 24 | .to be_instance_of(Openapi3Parser::Node::Paths) 25 | end 26 | 27 | it "can find a node via a relative path" do 28 | expect(instance.node_at("#version")).to eq "1.0.0" 29 | end 30 | 31 | it "can use '..' to access the parent node" do 32 | expect(instance.node_at("#..")) 33 | .to be_instance_of(Openapi3Parser::Node::Openapi) 34 | end 35 | end 36 | 37 | it_behaves_like "node equality", {} 38 | 39 | describe "#values" do 40 | it "returns an array of values" do 41 | instance = described_class.new({ "a" => "value_a", "b" => "value_b" }, 42 | create_node_context({})) 43 | 44 | expect(instance.values).to eq %w[value_a value_b] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/operation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::Operation do 4 | describe "#alternative_servers?" do 5 | it "returns true when this node has it's own servers" do 6 | node = create_node([{ "url" => "https://example.com" }]) 7 | 8 | expect(node.alternative_servers?).to be true 9 | end 10 | 11 | it "returns false when this node hasn't got it's own servers" do 12 | node = create_node(nil) 13 | 14 | expect(node.alternative_servers?).to be false 15 | end 16 | end 17 | 18 | def create_node(servers) 19 | input = { 20 | "responses" => {}, 21 | "servers" => servers 22 | } 23 | 24 | factory_context = create_node_factory_context( 25 | input, 26 | document_input: { 27 | "openapi" => "3.0.0", 28 | "info" => { 29 | "title" => "Minimal Openapi definition", 30 | "version" => "1.0.0" 31 | }, 32 | "paths" => { "/test" => { "get" => input } } 33 | }, 34 | pointer_segments: %w[paths /test get] 35 | ) 36 | 37 | Openapi3Parser::NodeFactory::Operation 38 | .new(factory_context) 39 | .node(node_factory_context_to_node_context(factory_context)) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/path_item_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::PathItem do 4 | describe "#alternative_servers?" do 5 | it "returns true when this node has it's own servers" do 6 | node = create_node([{ "url" => "https://example.com" }]) 7 | 8 | expect(node.alternative_servers?).to be true 9 | end 10 | 11 | it "returns false when this node hasn't got it's own servers" do 12 | node = create_node(nil) 13 | 14 | expect(node.alternative_servers?).to be false 15 | end 16 | end 17 | 18 | def create_node(servers) 19 | input = { "servers" => servers } 20 | 21 | factory_context = create_node_factory_context( 22 | input, 23 | document_input: { 24 | "openapi" => "3.0.0", 25 | "info" => { 26 | "title" => "Minimal Openapi definition", 27 | "version" => "1.0.0" 28 | }, 29 | "paths" => { "/test" => { "get" => input } } 30 | }, 31 | pointer_segments: %w[paths /test get] 32 | ) 33 | 34 | Openapi3Parser::NodeFactory::PathItem 35 | .new(factory_context) 36 | .node(node_factory_context_to_node_context(factory_context)) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node/schema_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Node::Schema do 4 | describe "#name" do 5 | it "returns the key of the context when the item is defined within components/schemas" do 6 | node_context = create_node_context( 7 | {}, 8 | pointer_segments: %w[components schemas Pet] 9 | ) 10 | instance = described_class.new({}, node_context) 11 | expect(instance.name).to eq "Pet" 12 | end 13 | 14 | it "returns nil when a schema is defined outside of components/schemas" do 15 | node_context = create_node_context( 16 | {}, 17 | pointer_segments: %w[content application/json schema] 18 | ) 19 | instance = described_class.new({}, node_context) 20 | expect(instance.name).to be_nil 21 | end 22 | end 23 | 24 | describe "#requires?" do 25 | let(:node) do 26 | input = { 27 | "type" => "object", 28 | "required" => %w[field_a], 29 | "properties" => { 30 | "field_a" => { "type" => "string" }, 31 | "field_b" => { "type" => "string" } 32 | } 33 | } 34 | 35 | factory_context = create_node_factory_context(input) 36 | Openapi3Parser::NodeFactory::Schema 37 | .new(factory_context) 38 | .node(node_factory_context_to_node_context(factory_context)) 39 | end 40 | 41 | context "when enquiring with a field name" do 42 | it "returns true when a field name is required" do 43 | expect(node.requires?("field_a")).to be true 44 | end 45 | 46 | it "returns false when a field name is not required" do 47 | expect(node.requires?("field_b")).to be false 48 | end 49 | end 50 | 51 | context "when enquiring with a schema object" do 52 | it "returns true when the schema is required" do 53 | expect(node.requires?(node.properties["field_a"])).to be true 54 | end 55 | 56 | it "returns false when the schema is not required" do 57 | expect(node.requires?(node.properties["field_b"])).to be false 58 | end 59 | end 60 | 61 | context "when comparing referenced schemas" do 62 | let(:node) do 63 | input = { 64 | "type" => "object", 65 | "required" => %w[field_a], 66 | "properties" => { 67 | "field_a" => { "$ref" => "#/referenced_item" }, 68 | "field_b" => { "$ref" => "#/referenced_item" } 69 | } 70 | } 71 | 72 | document_input = { 73 | "referenced_item" => { "type" => "string" } 74 | } 75 | 76 | factory_context = create_node_factory_context(input, document_input:) 77 | Openapi3Parser::NodeFactory::Schema 78 | .new(factory_context) 79 | .node(node_factory_context_to_node_context(factory_context)) 80 | end 81 | 82 | it "returns true for the required reference field" do 83 | expect(node.requires?(node.properties["field_a"])).to be true 84 | end 85 | 86 | it "returns false for the reference field that isn't required" do 87 | expect(node.requires?(node.properties["field_b"])).to be false 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Array do 4 | it_behaves_like "node factory", Array do 5 | let(:node_factory_context) { create_node_factory_context([]) } 6 | end 7 | 8 | describe "validating input" do 9 | it "is not valid when given a non-array input" do 10 | instance = described_class.new(create_node_factory_context("a string")) 11 | expect(instance).not_to be_valid 12 | end 13 | end 14 | 15 | describe "#node" do 16 | it "returns a type of Openapi3Parser::Node::Array" do 17 | expect(create_node([])).to be_a(Openapi3Parser::Node::Array) 18 | end 19 | 20 | context "when input is nil" do 21 | it "returns a Openapi3Parser::Node::Array when an array is specified as the default" do 22 | expect(create_node(nil, default: [])).to be_a(Openapi3Parser::Node::Array) 23 | end 24 | 25 | it "returns nil when nil is specified as the default" do 26 | expect(create_node(nil, default: nil)).to be_nil 27 | end 28 | end 29 | 30 | it "can return the default when given an empty array as input" do 31 | node = create_node([], default: [1], use_default_on_empty: true) 32 | expect(node.first).to be(1) 33 | end 34 | 35 | it "can build the items based on a value factory" do 36 | node = create_node([{ "name" => "Kenneth" }], 37 | value_factory: Openapi3Parser::NodeFactory::Contact) 38 | 39 | expect(node.first).to be_a(Openapi3Parser::Node::Contact) 40 | end 41 | 42 | it "raises an error when the array values are the wrong type" do 43 | expect { create_node([1], value_input_type: Hash) } 44 | .to raise_error(Openapi3Parser::Error::InvalidType, 45 | "Invalid type for #/0: Expected Object") 46 | end 47 | 48 | it "raises an error when input fails a passed validation constraint" do 49 | validation_rule = ->(validatable) { validatable.add_error("Fail") } 50 | expect { create_node([], validate: validation_rule) } 51 | .to raise_error(Openapi3Parser::Error::InvalidData) 52 | end 53 | 54 | def create_node(input, **options) 55 | node_factory_context = create_node_factory_context(input) 56 | instance = described_class.new(node_factory_context, **options) 57 | node_context = node_factory_context_to_node_context(node_factory_context) 58 | instance.node(node_context) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/callback_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Callback do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Callback do 5 | let(:callback_expression) do 6 | "http://notificationServer.com?transactionId={$request.body#/id}" \ 7 | "&email={$request.body#/email}" 8 | end 9 | 10 | let(:input) do 11 | { 12 | callback_expression => { 13 | "post" => { 14 | "requestBody" => { 15 | "description" => "Callback payload", 16 | "content" => { 17 | "application/json" => { 18 | "schema" => { "type" => "string" } 19 | } 20 | } 21 | }, 22 | "responses" => { 23 | "200" => { 24 | "description" => "webhook successfully processed and no" \ 25 | "retries will be performed" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/contact_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Contact do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Contact do 5 | let(:input) do 6 | { 7 | "name" => "Contact" 8 | } 9 | end 10 | end 11 | 12 | describe "validating URL" do 13 | it "is valid for an actual URL" do 14 | factory_context = create_node_factory_context( 15 | { "url" => "https://example.com/path" } 16 | ) 17 | expect(described_class.new(factory_context)).to be_valid 18 | end 19 | 20 | it "is invalid for an incorrect URL" do 21 | factory_context = create_node_factory_context( 22 | { "url" => "not a url" } 23 | ) 24 | instance = described_class.new(factory_context) 25 | expect(instance).not_to be_valid 26 | expect(instance).to have_validation_error("#/url") 27 | end 28 | end 29 | 30 | describe "validating email" do 31 | it "is valid for an email address" do 32 | factory_context = create_node_factory_context( 33 | { "email" => "kevin@example.com" } 34 | ) 35 | expect(described_class.new(factory_context)).to be_valid 36 | end 37 | 38 | it "is invalid for an incorrect email address" do 39 | factory_context = create_node_factory_context( 40 | { "email" => "not a url" } 41 | ) 42 | instance = described_class.new(factory_context) 43 | expect(instance).not_to be_valid 44 | expect(instance).to have_validation_error("#/email") 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/discriminator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Discriminator do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Discriminator do 5 | let(:input) do 6 | { 7 | "propertyName" => "test", 8 | "mapping" => { "key" => "value" } 9 | } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/encoding_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Encoding do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Encoding do 5 | let(:input) do 6 | { 7 | "contentType" => "image/png, image/jpeg", 8 | "headers" => { 9 | "X-Rate-Limit-Limit" => { 10 | "description" => "The number of allowed requests in the current " \ 11 | "period", 12 | "schema" => { "type" => "integer" } 13 | } 14 | } 15 | } 16 | end 17 | end 18 | 19 | describe "default value for explode" do 20 | it "sets explode to true when style is 'form'" do 21 | factory_context = create_node_factory_context({ "style" => "form" }) 22 | node = described_class.new(factory_context).node( 23 | node_factory_context_to_node_context(factory_context) 24 | ) 25 | expect(node["explode"]).to be(true) 26 | end 27 | 28 | it "sets explode to false when style is not 'form'" do 29 | factory_context = create_node_factory_context({ "style" => "simple" }) 30 | node = described_class.new(factory_context).node( 31 | node_factory_context_to_node_context(factory_context) 32 | ) 33 | expect(node["explode"]).to be(false) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/example_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Example do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Example do 5 | let(:input) do 6 | { 7 | "summary" => "Summary", 8 | "value" => [1, 2, 3], 9 | "x-otherField" => "Extension value" 10 | } 11 | end 12 | end 13 | 14 | describe "validating externalValue formatting" do 15 | it "is valid for an actual URL" do 16 | factory_context = create_node_factory_context( 17 | { "externalValue" => "https://example.com/path" } 18 | ) 19 | expect(described_class.new(factory_context)).to be_valid 20 | end 21 | 22 | it "is invalid for an incorrect URL" do 23 | factory_context = create_node_factory_context( 24 | { "externalValue" => "not a url" } 25 | ) 26 | instance = described_class.new(factory_context) 27 | expect(instance).not_to be_valid 28 | expect(instance).to have_validation_error("#/externalValue") 29 | end 30 | end 31 | 32 | describe "validating value and externalValue are mutually exclusive" do 33 | it "is valid when neither are provided" do 34 | expect(described_class.new(create_node_factory_context({}))) 35 | .to be_valid 36 | end 37 | 38 | it "is valid when one of them is provided" do 39 | factory_context = create_node_factory_context({ "value" => "anything" }) 40 | expect(described_class.new(factory_context)).to be_valid 41 | 42 | factory_context = create_node_factory_context({ "externalValue" => "/" }) 43 | expect(described_class.new(factory_context)).to be_valid 44 | end 45 | 46 | it "is invalid when both of them are provided" do 47 | factory_context = create_node_factory_context({ "value" => "anything", 48 | "externalValue" => "/" }) 49 | instance = described_class.new(factory_context) 50 | expect(instance).not_to be_valid 51 | expect(instance) 52 | .to have_validation_error("#/") 53 | .with_message("value and externalValue are mutually exclusive fields") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/external_documentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::ExternalDocumentation do 4 | it_behaves_like "node object factory", 5 | Openapi3Parser::Node::ExternalDocumentation do 6 | let(:input) do 7 | { 8 | "description" => "Test", 9 | "url" => "http://www.yahoo.com" 10 | } 11 | end 12 | end 13 | 14 | describe "validating URL" do 15 | it "is valid for an actual URL" do 16 | factory_context = create_node_factory_context( 17 | { "url" => "https://example.com/path" } 18 | ) 19 | expect(described_class.new(factory_context)).to be_valid 20 | end 21 | 22 | it "is invalid for an incorrect URL" do 23 | factory_context = create_node_factory_context( 24 | { "url" => "not a url" } 25 | ) 26 | instance = described_class.new(factory_context) 27 | expect(instance).not_to be_valid 28 | expect(instance).to have_validation_error("#/url") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/field_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Field do 4 | it_behaves_like "node factory", Integer do 5 | let(:node_factory_context) { create_node_factory_context(1) } 6 | end 7 | 8 | describe "#node" do 9 | it "returns the input" do 10 | expect(create_node(1)).to be(1) 11 | end 12 | 13 | it "raises an error when an input type is specified and doesn't match" do 14 | expect { create_node("string", input_type: Integer) } 15 | .to raise_error(Openapi3Parser::Error::InvalidType, 16 | "Invalid type for #/: Expected Integer") 17 | end 18 | 19 | it "doesn't raise an error when an input type is specified and the input is nil" do 20 | expect { create_node(nil, input_type: Integer) }.not_to raise_error 21 | end 22 | 23 | it "raises an error when input fails a passed validation constraint" do 24 | validation_rule = ->(validatable) { validatable.add_error("Fail") } 25 | expect { create_node(1, validate: validation_rule) } 26 | .to raise_error(Openapi3Parser::Error::InvalidData) 27 | end 28 | 29 | def create_node(input, **options) 30 | node_factory_context = create_node_factory_context(input) 31 | instance = described_class.new(node_factory_context, **options) 32 | node_context = node_factory_context_to_node_context(node_factory_context) 33 | instance.node(node_context) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/fields/reference_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Fields::Reference do 4 | let(:factory_class) { Openapi3Parser::NodeFactory::Contact } 5 | let(:factory_context) do 6 | create_node_factory_context( 7 | "#/reference", 8 | document_input:, 9 | pointer_segments: %w[field $ref] 10 | ) 11 | end 12 | 13 | describe "#resolved_input" do 14 | let(:instance) { described_class.new(factory_context, factory_class) } 15 | 16 | context "when reference can be resolved" do 17 | let(:document_input) do 18 | { "reference" => { "name" => "Joe" } } 19 | end 20 | 21 | it "returns the resolved input" do 22 | expect(instance.resolved_input) 23 | .to match(hash_including({ "name" => "Joe" })) 24 | end 25 | end 26 | 27 | context "when reference can't be resolved" do 28 | let(:document_input) do 29 | { "not_reference" => {} } 30 | end 31 | 32 | it "returns nil" do 33 | expect(instance.resolved_input).to be_nil 34 | end 35 | end 36 | end 37 | 38 | describe "#node" do 39 | let(:instance) { described_class.new(factory_context, factory_class) } 40 | let(:node_context) { node_factory_context_to_node_context(factory_context) } 41 | 42 | context "when the reference can be resolved" do 43 | let(:document_input) do 44 | { "reference" => { "name" => "Joe" } } 45 | end 46 | 47 | it "returns an instance of the referenced node" do 48 | expect(instance.node(node_context)) 49 | .to be_a(Openapi3Parser::Node::Contact) 50 | end 51 | end 52 | 53 | context "when the reference can't be resolved" do 54 | let(:document_input) do 55 | { "reference" => { "url" => "invalid url" } } 56 | end 57 | 58 | it "raises an error" do 59 | expect { instance.node(node_context) } 60 | .to raise_error(Openapi3Parser::Error::InvalidData) 61 | end 62 | end 63 | end 64 | 65 | describe "validations" do 66 | let(:instance) { described_class.new(factory_context, factory_class) } 67 | 68 | context "when the reference can be resolved" do 69 | let(:document_input) do 70 | { "reference" => { "name" => "Joe" } } 71 | end 72 | 73 | it "is valid" do 74 | expect(instance).to be_valid 75 | end 76 | end 77 | 78 | context "when the reference can't be resolved" do 79 | let(:document_input) do 80 | { "reference" => { "url" => "invalid url" } } 81 | end 82 | 83 | it "is invalid" do 84 | expect(instance).not_to be_valid 85 | expect(instance).to have_validation_error("#/field/%24ref") 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Header do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Header do 5 | let(:input) do 6 | { 7 | "description" => "token to be passed as a header", 8 | "required" => true, 9 | "schema" => { 10 | "type" => "array", 11 | "items" => { 12 | "type" => "integer", 13 | "format" => "int64" 14 | } 15 | }, 16 | "style" => "simple", 17 | "x-additional" => "test" 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/info_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Info do 4 | let(:minimal_info_definition) do 5 | { 6 | "title" => "Info", 7 | "version" => "1.0" 8 | } 9 | end 10 | 11 | it_behaves_like "node object factory", Openapi3Parser::Node::Info do 12 | let(:input) do 13 | minimal_info_definition.merge( 14 | "license" => { "name" => "License" }, 15 | "contact" => { "name" => "Contact" } 16 | ) 17 | end 18 | end 19 | 20 | describe "validating terms of service URL" do 21 | it "is valid for an actual URL" do 22 | factory_context = create_node_factory_context( 23 | minimal_info_definition.merge({ "termsOfService" => "https://example.com/path" }) 24 | ) 25 | expect(described_class.new(factory_context)).to be_valid 26 | end 27 | 28 | it "is invalid for an incorrect URL" do 29 | factory_context = create_node_factory_context( 30 | minimal_info_definition.merge({ "termsOfService" => "not a url" }) 31 | ) 32 | instance = described_class.new(factory_context) 33 | expect(instance).not_to be_valid 34 | expect(instance).to have_validation_error("#/termsOfService") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/license_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::License do 4 | let(:minimal_license_definition) { { "name" => "License" } } 5 | 6 | it_behaves_like "node object factory", Openapi3Parser::Node::License do 7 | let(:input) { minimal_license_definition } 8 | end 9 | 10 | describe "validating URL" do 11 | it "is valid for an actual URL" do 12 | factory_context = create_node_factory_context( 13 | minimal_license_definition.merge({ "url" => "https://example.com/path" }) 14 | ) 15 | expect(described_class.new(factory_context)).to be_valid 16 | end 17 | 18 | it "is invalid for an incorrect URL" do 19 | factory_context = create_node_factory_context( 20 | minimal_license_definition.merge({ "url" => "not a url" }) 21 | ) 22 | instance = described_class.new(factory_context) 23 | expect(instance).not_to be_valid 24 | expect(instance).to have_validation_error("#/url") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/link_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Link do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Link do 5 | let(:input) do 6 | { 7 | "operationRef" => "#/paths/~12.0~1repositories~1{username}/get", 8 | "parameters" => { "username" => "$response.body#/username" } 9 | } 10 | end 11 | end 12 | 13 | describe "validating operationRef and operationId are mutually exclusive" do 14 | it "is invalid when neither are provided" do 15 | instance = described_class.new(create_node_factory_context({})) 16 | expect(instance).not_to be_valid 17 | expect(instance) 18 | .to have_validation_error("#/") 19 | .with_message("One of operationRef and operationId is required") 20 | end 21 | 22 | it "is valid when one of them is provided" do 23 | factory_context = create_node_factory_context({ "operationRef" => "#/test" }) 24 | expect(described_class.new(factory_context)).to be_valid 25 | 26 | factory_context = create_node_factory_context({ "operationId" => "getOperation" }) 27 | expect(described_class.new(factory_context)).to be_valid 28 | end 29 | 30 | it "is invalid when both of them are provided" do 31 | factory_context = create_node_factory_context({ "operationRef" => "#/test", 32 | "operationId" => "getOperation" }) 33 | instance = described_class.new(factory_context) 34 | expect(instance).not_to be_valid 35 | expect(instance) 36 | .to have_validation_error("#/") 37 | .with_message("operationRef and operationId are mutually exclusive fields") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/map_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Map do 4 | it_behaves_like "node factory", Hash do 5 | let(:node_factory_context) { create_node_factory_context({}) } 6 | end 7 | 8 | describe "validating input" do 9 | it "is not valid when given a non-hash input" do 10 | instance = described_class.new(create_node_factory_context("a string")) 11 | expect(instance).not_to be_valid 12 | end 13 | end 14 | 15 | describe "#node" do 16 | it "returns a type of Openapi3Parser::Node::Map" do 17 | expect(create_node({})).to be_a(Openapi3Parser::Node::Map) 18 | end 19 | 20 | context "when input is nil" do 21 | it "returns a Openapi3Parser::Node::Map when a hash is specified as the default" do 22 | expect(create_node(nil, default: {})).to be_a(Openapi3Parser::Node::Map) 23 | end 24 | 25 | it "returns nil when nil is specified as the default" do 26 | expect(create_node(nil, default: nil)).to be_nil 27 | end 28 | end 29 | 30 | it "can build the items based on a value factory" do 31 | node = create_node({ "item" => { "name" => "Kenneth" } }, 32 | value_factory: Openapi3Parser::NodeFactory::Contact) 33 | 34 | expect(node["item"]).to be_a(Openapi3Parser::Node::Contact) 35 | end 36 | 37 | it "allows extensions to be a different input type to valid_input_type" do 38 | input = { "real" => 1, "x-item" => "string" } 39 | expect { create_node(input, allow_extensions: true, value_input_type: Integer) } 40 | .not_to raise_error 41 | end 42 | 43 | it "raises an error when a type other than string is given as an input key" do 44 | expect { create_node({ 1 => "Test" }) } 45 | .to raise_error(Openapi3Parser::Error::InvalidType, 46 | "Invalid keys for #/: Expected keys to be of type String") 47 | end 48 | 49 | it "raises an error when the hash values are the wrong type" do 50 | expect { create_node({ "a" => "Test" }, value_input_type: Integer) } 51 | .to raise_error(Openapi3Parser::Error::InvalidType, 52 | "Invalid type for #/a: Expected Integer") 53 | end 54 | 55 | it "raises an error when input fails a passed validation constraint" do 56 | validation_rule = ->(validatable) { validatable.add_error("Fail") } 57 | expect { create_node({}, validate: validation_rule) } 58 | .to raise_error(Openapi3Parser::Error::InvalidData) 59 | end 60 | 61 | def create_node(input, **options) 62 | node_factory_context = create_node_factory_context(input) 63 | instance = described_class.new(node_factory_context, **options) 64 | node_context = node_factory_context_to_node_context(node_factory_context) 65 | instance.node(node_context) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/oauth_flow_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::OauthFlow do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::OauthFlow do 5 | let(:input) do 6 | { 7 | "authorizationUrl" => "https://example.com/api/oauth/dialog", 8 | "tokenUrl" => "https://example.com/api/oauth/token", 9 | "scopes" => { 10 | "write:pets" => "modify pets in your account", 11 | "read:pets" => "read your pets" 12 | } 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/oauth_flows_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::OauthFlows do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::OauthFlows do 5 | let(:input) do 6 | { 7 | "authorizationCode" => { 8 | "$ref" => "#/myReference" 9 | } 10 | } 11 | end 12 | 13 | let(:document_input) do 14 | { 15 | "myReference" => { 16 | "authorizationUrl" => "https://example.com/api/oauth/dialog", 17 | "tokenUrl" => "https://example.com/api/oauth/token", 18 | "scopes" => { 19 | "write:pets" => "modify pets in your account", 20 | "read:pets" => "read your pets" 21 | } 22 | } 23 | } 24 | end 25 | 26 | let(:node_factory_context) do 27 | create_node_factory_context(input, document_input:) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/object_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Object do 4 | let(:node_factory_context) { create_node_factory_context({}) } 5 | let(:instance) { described_class.new(node_factory_context) } 6 | 7 | it_behaves_like "node factory", Hash 8 | end 9 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/paths_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Paths do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Paths do 5 | let(:input) do 6 | { 7 | "/pets" => { 8 | "get" => { 9 | "description" => "Returns all pets that the user has access to", 10 | "responses" => { 11 | "200" => { 12 | "description" => "A list of pets.", 13 | "content" => { 14 | "application/json" => { 15 | "schema" => { 16 | "type" => "array", 17 | "items" => { "type" => "string" } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | end 27 | end 28 | 29 | describe "validating path keys" do 30 | let(:path) do 31 | { 32 | "get" => { 33 | "description" => "Description", 34 | "responses" => { 35 | "200" => { "description" => "Description" } 36 | } 37 | } 38 | } 39 | end 40 | 41 | it "is valid when the path key is a valid path" do 42 | instance = described_class.new(create_node_factory_context({ "/path" => path })) 43 | expect(instance).to be_valid 44 | end 45 | 46 | it "is valid when the path key has template parameters" do 47 | instance = described_class.new( 48 | create_node_factory_context({ "/path/{test}" => path }) 49 | ) 50 | expect(instance).to be_valid 51 | end 52 | 53 | it "is invalid when the path isn't prefixed with a slash" do 54 | instance = described_class.new( 55 | create_node_factory_context({ "path" => path }) 56 | ) 57 | expect(instance).not_to be_valid 58 | end 59 | 60 | it "is invalid when the path isn't a valid path" do 61 | instance = described_class.new( 62 | create_node_factory_context({ "invalid path" => path }) 63 | ) 64 | expect(instance).not_to be_valid 65 | end 66 | 67 | it "is invalid when there are two paths with same hiearchy but different templated names" do 68 | factory_context = create_node_factory_context({ "/path/{param_a}/test" => path, 69 | "/path/{param_b}/test" => path }) 70 | expect(described_class.new(factory_context)).not_to be_valid 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/request_body_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::RequestBody do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::RequestBody do 5 | let(:input) do 6 | { 7 | "description" => "user to add to the system", 8 | "content" => { 9 | "text/plain" => { 10 | "schema" => { 11 | "type" => "array", 12 | "items" => { "type" => "string" } 13 | } 14 | } 15 | } 16 | } 17 | end 18 | end 19 | 20 | describe "validating content" do 21 | it "is valid when content has a valid media type" do 22 | instance = described_class.new( 23 | create_node_factory_context({ "content" => { "application/json" => {} } }) 24 | ) 25 | expect(instance).to be_valid 26 | end 27 | 28 | it "is valid when content has a valid media type range" do 29 | instance = described_class.new( 30 | create_node_factory_context({ "content" => { "text/*" => {} } }) 31 | ) 32 | expect(instance).to be_valid 33 | end 34 | 35 | it "is invalid when content has an invalid media type" do 36 | instance = described_class.new( 37 | create_node_factory_context({ "content" => { "bad-media-type" => {} } }) 38 | ) 39 | expect(instance).not_to be_valid 40 | expect(instance) 41 | .to have_validation_error("#/content/bad-media-type") 42 | .with_message(%("bad-media-type" is not a valid media type)) 43 | end 44 | 45 | it "is invalid when content is an empty hash" do 46 | instance = described_class.new(create_node_factory_context({ "content" => {} })) 47 | expect(instance).not_to be_valid 48 | expect(instance) 49 | .to have_validation_error("#/content") 50 | .with_message("Expected to have at least 1 item") 51 | end 52 | end 53 | 54 | describe "required field" do 55 | it "defaults to false" do 56 | factory_context = create_node_factory_context( 57 | { "content" => { "application/json" => {} } } 58 | ) 59 | node = described_class.new(factory_context).node( 60 | node_factory_context_to_node_context(factory_context) 61 | ) 62 | expect(node["required"]).to be(false) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Response do 4 | let(:minimal_definition) do 5 | { "description" => "Description" } 6 | end 7 | 8 | it_behaves_like "node object factory", Openapi3Parser::Node::Response do 9 | let(:input) do 10 | { 11 | "description" => "A simple string response", 12 | "content" => { 13 | "text/plain" => { 14 | "schema" => { 15 | "type" => "string" 16 | } 17 | } 18 | }, 19 | "headers" => { 20 | "X-Rate-Limit-Limit" => { 21 | "description" => "The number of allowed requests in the current period", 22 | "schema" => { "type" => "integer" } 23 | }, 24 | "X-Rate-Limit-Remaining" => { 25 | "description" => "The number of remaining requests in the current period", 26 | "schema" => { "type" => "integer" } 27 | }, 28 | "X-Rate-Limit-Reset" => { 29 | "description" => "The number of seconds left in the current period", 30 | "schema" => { "type" => "integer" } 31 | } 32 | } 33 | } 34 | end 35 | end 36 | 37 | describe "validating content" do 38 | it "is valid when content is an empty hash" do 39 | instance = described_class.new( 40 | create_node_factory_context( 41 | minimal_definition.merge({ "content" => {} }) 42 | ) 43 | ) 44 | expect(instance).to be_valid 45 | end 46 | 47 | it "is valid when content has a valid media type" do 48 | instance = described_class.new( 49 | create_node_factory_context( 50 | minimal_definition.merge({ "content" => { "application/json" => {} } }) 51 | ) 52 | ) 53 | expect(instance).to be_valid 54 | end 55 | 56 | it "is invalid when content has an invalid media type" do 57 | instance = described_class.new( 58 | create_node_factory_context( 59 | minimal_definition.merge({ "content" => { "bad-media-type" => {} } }) 60 | ) 61 | ) 62 | expect(instance).not_to be_valid 63 | expect(instance) 64 | .to have_validation_error("#/content/bad-media-type") 65 | .with_message(%("bad-media-type" is not a valid media type)) 66 | end 67 | end 68 | 69 | describe "validating links keys" do 70 | let(:link) { { "operationRef" => "#/test" } } 71 | 72 | it "is valid for a key that matches the expected formatting" do 73 | instance = described_class.new( 74 | create_node_factory_context( 75 | minimal_definition.merge( 76 | { "links" => { "valid.key" => link } } 77 | ) 78 | ) 79 | ) 80 | expect(instance).to be_valid 81 | end 82 | 83 | it "is invalid for a key that doesn't match the expected formatting" do 84 | instance = described_class.new( 85 | create_node_factory_context( 86 | minimal_definition.merge( 87 | { "links" => { "Invalid Key" => link } } 88 | ) 89 | ) 90 | ) 91 | expect(instance).not_to be_valid 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/responses_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Responses do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Responses do 5 | let(:input) do 6 | { 7 | "200" => { 8 | "description" => "a pet to be returned", 9 | "content" => { 10 | "application/json" => { 11 | "schema" => { "type" => "string" } 12 | } 13 | } 14 | }, 15 | "default" => { 16 | "description" => "Unexpected error", 17 | "content" => { 18 | "application/json" => { 19 | "schema" => { "type" => "string" } 20 | } 21 | } 22 | } 23 | } 24 | end 25 | end 26 | 27 | describe "validating keys" do 28 | let(:response) do 29 | { 30 | "description" => "A response", 31 | "content" => { 32 | "application/json" => { 33 | "schema" => { "type" => "string" } 34 | } 35 | } 36 | } 37 | end 38 | 39 | it "is valid when the key is 'default'" do 40 | instance = described_class.new( 41 | create_node_factory_context({ "default" => response }) 42 | ) 43 | expect(instance).to be_valid 44 | end 45 | 46 | it "is valid when the key is a status code range" do 47 | instance = described_class.new( 48 | create_node_factory_context({ "2XX" => response }) 49 | ) 50 | expect(instance).to be_valid 51 | end 52 | 53 | it "is valid when the key is a valid status code" do 54 | instance = described_class.new( 55 | create_node_factory_context({ "503" => response }) 56 | ) 57 | expect(instance).to be_valid 58 | end 59 | 60 | it "is invalid when the key is an invalid status code" do 61 | instance = described_class.new( 62 | create_node_factory_context({ "999" => response }) 63 | ) 64 | expect(instance).not_to be_valid 65 | expect(instance) 66 | .to have_validation_error("#/") 67 | .with_message( 68 | "Invalid responses keys: '999' - default, status codes and status code ranges allowed" 69 | ) 70 | end 71 | 72 | it "is invalid when the key is not a status code od default" do 73 | instance = described_class.new( 74 | create_node_factory_context({ "any string" => response }) 75 | ) 76 | expect(instance).not_to be_valid 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/security_requirement_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::SecurityRequirement do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::SecurityRequirement do 5 | let(:input) do 6 | { 7 | "petstore_auth" => %w[write:pets read:pets] 8 | } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/security_scheme_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::SecurityScheme do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::SecurityScheme do 5 | let(:input) do 6 | { 7 | "type" => "oauth2", 8 | "flows" => { 9 | "implicit" => { 10 | "authorizationUrl" => "https://example.com/api/oauth/dialog", 11 | "scopes" => { 12 | "write =>pets": "modify pets in your account", 13 | "read =>pets": "read your pets" 14 | } 15 | } 16 | } 17 | } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Server do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Server do 5 | let(:input) do 6 | { 7 | "url" => "https://{username}.gigantic-server.com:{port}/{basePath}", 8 | "description" => "The production API server", 9 | "variables" => { 10 | "username" => { 11 | "default" => "demo", 12 | "description" => "this value is assigned by the service provider," \ 13 | "in this example `gigantic-server.com`" 14 | }, 15 | "port" => { 16 | "enum" => %w[8443 443], 17 | "default" => "8443" 18 | }, 19 | "basePath" => { 20 | "default" => "v2" 21 | } 22 | } 23 | } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/server_variable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::ServerVariable do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::ServerVariable do 5 | let(:input) do 6 | { 7 | "enum" => %w[8443 443], 8 | "default" => "8443" 9 | } 10 | end 11 | end 12 | 13 | describe "validating enum" do 14 | it "is valid when enum is not empty" do 15 | instance = described_class.new( 16 | create_node_factory_context({ "enum" => %w[test], "default" => "test" }) 17 | ) 18 | expect(instance).to be_valid 19 | end 20 | 21 | it "is valid when enum is empty" do 22 | instance = described_class.new( 23 | create_node_factory_context({ "enum" => [], "default" => "test" }) 24 | ) 25 | expect(instance).not_to be_valid 26 | expect(instance).to have_validation_error("#/enum") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/tag_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Tag do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Tag do 5 | let(:input) do 6 | { 7 | "name" => "pet", 8 | "description" => "Pets operations", 9 | "externalDocs" => { 10 | "description" => "Find more info here", 11 | "url" => "https://example.com" 12 | } 13 | } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/node_factory/xml_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::NodeFactory::Xml do 4 | it_behaves_like "node object factory", Openapi3Parser::Node::Xml do 5 | let(:input) do 6 | { 7 | "namespace" => "http://example.com/schema/sample", 8 | "prefix" => "sample" 9 | } 10 | end 11 | end 12 | 13 | describe "validating namespace" do 14 | it "is valid when the namespace is a uri" do 15 | factory_context = create_node_factory_context( 16 | { "namespace" => "https://example.com/path", "prefix" => "sample" } 17 | ) 18 | 19 | expect(described_class.new(factory_context)).to be_valid 20 | end 21 | 22 | it "is invalid when the namespace is not a uri" do 23 | factory_context = create_node_factory_context( 24 | { "namespace" => "not a url", "prefix" => "sample" } 25 | ) 26 | 27 | instance = described_class.new(factory_context) 28 | expect(instance).not_to be_valid 29 | expect(instance) 30 | .to have_validation_error("#/namespace") 31 | .with_message(%("not a url" is not a valid URI)) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/source/reference_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Source::Reference do 4 | describe ".only_fragment?" do 5 | it "returns true when reference is only a fragment" do 6 | expect(described_class.new("#/test").only_fragment?).to be true 7 | end 8 | 9 | it "returns false when reference includes a filename" do 10 | expect(described_class.new("test.yaml").only_fragment?).to be false 11 | end 12 | end 13 | 14 | describe ".fragment" do 15 | it "returns the fragment for a reference with a fragment" do 16 | expect(described_class.new("test.yaml#/test").fragment).to eq "/test" 17 | end 18 | 19 | it "returns nil for a reference without a fragment" do 20 | expect(described_class.new("test.yaml").fragment).to be_nil 21 | end 22 | end 23 | 24 | describe ".resource_uri" do 25 | it "returns a URI object for the non fragment portion of the reference" do 26 | expect(described_class.new("test.yaml#/test").resource_uri) 27 | .to eq URI.parse("test.yaml") 28 | end 29 | 30 | it "returns an empty URI object when the reference is only a fragment" do 31 | expect(described_class.new("#/test").resource_uri) 32 | .to eq URI.parse("") 33 | end 34 | end 35 | 36 | describe ".absolute?" do 37 | it "returns true when reference is an absolute URL" do 38 | expect(described_class.new("https://example.org").absolute?).to be true 39 | end 40 | 41 | it "returns false when reference is a relative URL" do 42 | expect(described_class.new("test.yaml").absolute?).to be false 43 | end 44 | 45 | it "returns false when reference is to a root file in a file system" do 46 | expect(described_class.new("/path/to/file.yaml").absolute?).to be false 47 | end 48 | end 49 | 50 | describe ".json_pointer" do 51 | it "returns an array of reference segments" do 52 | expect(described_class.new("test.yaml#/path/to/field").json_pointer) 53 | .to eq %w[path to field] 54 | end 55 | 56 | it "decodes URL encoded segments" do 57 | instance = described_class.new("test.yaml#/two%20words/comma%2C%20seperated") 58 | expect(instance.json_pointer).to eq ["two words", "comma, seperated"] 59 | end 60 | 61 | it "returns an empty array for a reference without a fragment" do 62 | expect(described_class.new("test.yaml").json_pointer).to eq [] 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/source_input/raw_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::SourceInput::Raw do 4 | let(:unparsable_input) { "*invalid: yaml" } 5 | 6 | describe "#available?" do 7 | it "returns true when input is valid" do 8 | expect(described_class.new({}).available?).to be true 9 | end 10 | 11 | it "returns false when input is unparsable" do 12 | expect(described_class.new(unparsable_input).available?).to be false 13 | end 14 | end 15 | 16 | describe "#parse_error" do 17 | it "returns nil when input is valid" do 18 | expect(described_class.new({}).parse_error).to be_nil 19 | end 20 | 21 | it "returns an error object when input is invalid" do 22 | expect(described_class.new(unparsable_input).parse_error) 23 | .to be_a(Openapi3Parser::Error::UnparsableInput) 24 | end 25 | end 26 | 27 | describe "#contents" do 28 | it "returns the input" do 29 | expect(described_class.new({}).contents).to eq({}) 30 | end 31 | 32 | it "raises an error when input is unparsable" do 33 | expect { described_class.new(unparsable_input).contents } 34 | .to raise_error(Openapi3Parser::Error::UnparsableInput) 35 | end 36 | end 37 | 38 | describe "#resolve_next" do 39 | it "returns a new source input based on the given reference" do 40 | url = "https://example.com/openapi" 41 | reference = Openapi3Parser::Source::Reference.new(url) 42 | stub_request(:get, url) 43 | source_input = described_class.new({}).resolve_next(reference) 44 | expect(source_input).to eq Openapi3Parser::SourceInput::Url.new(url) 45 | end 46 | end 47 | 48 | describe "#==" do 49 | it "returns true for the same class and same input" do 50 | other = described_class.new({ field: "value" }) 51 | 52 | expect(described_class.new({ field: "value" })).to eq other 53 | end 54 | 55 | it "returns false for the same class and different input" do 56 | other = described_class.new({ field: "value" }) 57 | 58 | expect(described_class.new({})).not_to eq other 59 | end 60 | 61 | it "returns false for a different class" do 62 | other = Openapi3Parser::SourceInput::File.new("test.yml") 63 | 64 | expect(described_class.new({})).not_to eq other 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/source_input/resolve_next_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::SourceInput::ResolveNext do 4 | before do 5 | allow(File).to receive(:read).and_return("") 6 | stub_request(:get, /example/) 7 | end 8 | 9 | describe "#call" do 10 | it "can return a file source input based on the working directory" do 11 | source_input = described_class.call( 12 | Openapi3Parser::Source::Reference.new("other.yaml#/test"), 13 | Openapi3Parser::SourceInput::Raw.new({}), 14 | working_directory: "/file" 15 | ) 16 | 17 | expect(source_input) 18 | .to eq Openapi3Parser::SourceInput::File.new("/file/other.yaml") 19 | end 20 | 21 | it "can return a URL source input based on the base URL" do 22 | source_input = described_class.call( 23 | Openapi3Parser::Source::Reference.new("other.yaml#/test"), 24 | Openapi3Parser::SourceInput::Raw.new({}), 25 | base_url: "https://example.org/path/to/file.yaml" 26 | ) 27 | 28 | expect(source_input) 29 | .to eq Openapi3Parser::SourceInput::Url.new("https://example.org/path/to/other.yaml") 30 | end 31 | 32 | it "returns an unchanged URL for an absolute reference" do 33 | source_input = described_class.call( 34 | Openapi3Parser::Source::Reference.new("https://example.org/file.yaml"), 35 | Openapi3Parser::SourceInput::Raw.new({}), 36 | base_url: "https://example.org/path/to/file.yaml" 37 | ) 38 | 39 | expect(source_input) 40 | .to eq Openapi3Parser::SourceInput::Url.new("https://example.org/file.yaml") 41 | end 42 | 43 | it "returns the current source input when the reference is just a fragment" do 44 | current_source_input = Openapi3Parser::SourceInput::Raw.new({}) 45 | source_input = described_class.call( 46 | Openapi3Parser::Source::Reference.new("#/test"), 47 | current_source_input 48 | ) 49 | 50 | expect(source_input).to eq current_source_input 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/source_input/string_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::SourceInput::StringParser do 4 | describe "#call" do 5 | it "can parse a YAML string" do 6 | expect(described_class.call("key: value\n")) 7 | .to match("key" => "value") 8 | end 9 | 10 | it "can parse a JSON string" do 11 | expect(described_class.call(%({ "key": "value" }))) 12 | .to match("key" => "value") 13 | end 14 | 15 | it "raises an error when the input is invalid YAML" do 16 | input = "*test: test\n" 17 | expect { described_class.call(input) } 18 | .to raise_error(Psych::Exception) 19 | end 20 | 21 | it "raises an error when the input is invalid JSON" do 22 | input = %({ "key" "value" }) 23 | expect { described_class.call(input) } 24 | .to raise_error(JSON::JSONError) 25 | end 26 | 27 | it "treats a file as YAML if the filename ends in .yaml" do 28 | json_input = %({ "key" "value" }) 29 | expect { described_class.call(json_input, "file.yaml") } 30 | .to raise_error(Psych::Exception) 31 | end 32 | 33 | it "treats a file as JSON if the filename ends in .json" do 34 | yaml_input = "key: value\n" 35 | expect { described_class.call(yaml_input, "file.json") } 36 | .to raise_error(JSON::JSONError) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validation/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validation::Error do 4 | describe ".for_type" do 5 | let(:node_factory_context) { create_node_factory_context({}) } 6 | 7 | it "returns nil when there isn't a factory class" do 8 | instance = described_class.new("", node_factory_context) 9 | expect(instance.for_type).to be_nil 10 | end 11 | 12 | it "returns the last class for a nested class" do 13 | instance = described_class.new("", 14 | node_factory_context, 15 | Openapi3Parser::NodeFactory::Contact) 16 | expect(instance.for_type).to eq "Contact" 17 | end 18 | 19 | it "returns '(anonymous)' when there isn't a factory name" do 20 | factory_class = Class.new 21 | instance = described_class.new("", node_factory_context, factory_class) 22 | expect(instance.for_type).to eq "(anonymous)" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/absolute_uri_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::AbsoluteUri do 4 | describe ".call" do 5 | it "returns nil for a valid URI" do 6 | expect(described_class.call("http://example.com/test?query=blah#anchor")) 7 | .to be_nil 8 | end 9 | 10 | it "returns an error for a relative url" do 11 | expect(described_class.call("test?query=blah#anchor")) 12 | .to eq %("test?query=blah#anchor" is not a absolute URI) 13 | end 14 | 15 | it "returns an error for an invalid URI" do 16 | expect(described_class.call("not a URI")) 17 | .to eq %("not a URI" is not a valid URI) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/component_keys_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::ComponentKeys do 4 | describe ".call" do 5 | it "returns nil for a hash with a valid component key" do 6 | expect(described_class.call({ "valid.key" => {} })) 7 | .to be_nil 8 | end 9 | 10 | it "returns an error for a hash with an invalid component key" do 11 | expect(described_class.call({ "Invalid Key" => {} })) 12 | .to eq "Contains invalid keys: Invalid Key" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/duplicate_parameters_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::DuplicateParameters do 4 | describe ".call" do 5 | it "returns nil when there aren't any duplicate parameters" do 6 | parameters = [ 7 | { "name" => "id", "in" => "path" }, 8 | { "name" => "id", "in" => "query" } 9 | ] 10 | 11 | expect(described_class.call(parameters)).to be_nil 12 | end 13 | 14 | it "copes with parameters that are in an unexpected type" do 15 | parameters = [1, "string", [1, 2, 3], {}] 16 | expect(described_class.call(parameters)).to be_nil 17 | end 18 | 19 | it "returns an error for dupliate parameters" do 20 | parameters = [ 21 | { "name" => "id", "in" => "path" }, 22 | { "name" => "id", "in" => "path" }, 23 | { "name" => "field", "in" => "path" }, 24 | { "name" => "field", "in" => "path" }, 25 | { "name" => "address", "in" => "query" }, 26 | { "name" => "address", "in" => "query" } 27 | ] 28 | 29 | expect(described_class.call(parameters)) 30 | .to eq "Duplicate parameters: id in path, field in path, address in query" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::Email do 4 | describe ".call" do 5 | it "returns a message when an email is invalid" do 6 | expect(described_class.call("not an email")) 7 | .to eq %("not an email" is not a valid email address) 8 | end 9 | 10 | it "returns nil for a valid email" do 11 | expect(described_class.call("kevin@example.com")).to be_nil 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/media_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::MediaType do 4 | describe ".call" do 5 | it "returns a message when a media type is invalid" do 6 | expect(described_class.call("not a media type")) 7 | .to eq %("not a media type" is not a valid media type) 8 | end 9 | 10 | it "returns nil for valid media types and ranges" do 11 | %w[ 12 | */* 13 | text/* 14 | text/plain 15 | application/atom+xml 16 | application/EDI-X12 17 | application/xml-dtd 18 | application/zip 19 | application/vnd.openxmlformats-officedocument.presentationml 20 | video/quicktime 21 | ].each do |media_type| 22 | expect(described_class.call(media_type)).to be_nil 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/reference_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::Reference do 4 | describe "#errors" do 5 | it "returns an empty array when input is valid" do 6 | expect(described_class.new("#/test").errors).to be_empty 7 | end 8 | 9 | it "has an error when input is not a string" do 10 | expect(described_class.new(12).errors) 11 | .to contain_exactly("Expected a string") 12 | end 13 | 14 | it "has an error when input is an invalid URI" do 15 | expect(described_class.new("not a uri").errors) 16 | .to contain_exactly("Could not parse as a URI") 17 | end 18 | 19 | it "has an error when input is an invalid JSON pointer" do 20 | expect(described_class.new("./test#any-old-fragment").errors) 21 | .to contain_exactly("Invalid JSON pointer, expected a root slash") 22 | end 23 | end 24 | 25 | describe "#valid?" do 26 | it "returns true for valid input" do 27 | expect(described_class.new("#/test")).to be_valid 28 | end 29 | 30 | it "returns false for invalid input" do 31 | expect(described_class.new(12)).not_to be_valid 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/required_fields_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::RequiredFields do 4 | describe ".call" do 5 | it "doesn't raise an error for valid input" do 6 | validatable = create_validatable({}) 7 | expect { described_class.call(validatable, required_fields: []) } 8 | .not_to raise_error 9 | end 10 | 11 | describe "required_fields option" do 12 | it "doesn't raise an error when a required field is present" do 13 | validatable = create_validatable({ "fieldA" => "value" }) 14 | expect { described_class.call(validatable, required_fields: ["fieldA"]) } 15 | .not_to raise_error 16 | end 17 | 18 | it "raises an error when a required field is missing" do 19 | validatable = create_validatable({}) 20 | expect { described_class.call(validatable, required_fields: ["fieldA"]) } 21 | .to raise_error( 22 | Openapi3Parser::Error::MissingFields, 23 | "Missing required fields for #/: fieldA" 24 | ) 25 | end 26 | end 27 | 28 | describe "raise_on_invalid option" do 29 | let(:validatable) do 30 | create_validatable({ "fieldA" => "My field" }) 31 | end 32 | 33 | it "sets errors on the validatable when invalid and raise_on_invalid is false" do 34 | described_class.call(validatable, 35 | required_fields: ["fieldC"], 36 | raise_on_invalid: false) 37 | 38 | expect(validatable.errors.length).to eq 1 39 | expect(validatable.errors.first.message).to eq "Missing required fields: fieldC" 40 | end 41 | 42 | it "doesn't set errors on the validatable when valid" do 43 | described_class.call(validatable, 44 | required_fields: ["fieldA"], 45 | raise_on_invalid: false) 46 | 47 | expect(validatable.errors).to be_empty 48 | end 49 | end 50 | end 51 | 52 | def create_validatable(input) 53 | node_factory_context = create_node_factory_context(input) 54 | Openapi3Parser::Validation::Validatable.new( 55 | Openapi3Parser::NodeFactory::Map.new(node_factory_context) 56 | ) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/unexpected_fields_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::UnexpectedFields do 4 | describe ".call" do 5 | it "doesn't raise an error for valid input" do 6 | validatable = create_validatable({}) 7 | expect { described_class.call(validatable, allowed_fields: []) } 8 | .not_to raise_error 9 | end 10 | 11 | describe "allowed_fields option" do 12 | let(:validatable) do 13 | create_validatable({ "fieldA" => "My field" }) 14 | end 15 | 16 | it "doesn't raise an error for an allowed field" do 17 | expect { described_class.call(validatable, allowed_fields: ["fieldA"]) } 18 | .not_to raise_error 19 | end 20 | 21 | it "raises an error when a field isn't allowed" do 22 | expect { described_class.call(validatable, allowed_fields: ["fieldC"]) } 23 | .to raise_error( 24 | Openapi3Parser::Error::UnexpectedFields, 25 | "Unexpected fields for #/: fieldA" 26 | ) 27 | end 28 | end 29 | 30 | describe "allow_extensions option" do 31 | let(:validatable) do 32 | create_validatable({ "x-extension" => "my extension", 33 | "x-extension-2" => "my other extension" }) 34 | end 35 | 36 | it "defaults to allowing extensions" do 37 | expect { described_class.call(validatable, allowed_fields: []) } 38 | .not_to raise_error 39 | end 40 | 41 | it "raises an error when allow_extensions is false" do 42 | expect { described_class.call(validatable, allowed_fields: [], allow_extensions: false) } 43 | .to raise_error( 44 | Openapi3Parser::Error::UnexpectedFields, 45 | "Unexpected fields for #/: x-extension and x-extension-2" 46 | ) 47 | end 48 | end 49 | 50 | describe "raise_on_invalid option" do 51 | let(:validatable) do 52 | create_validatable({ "fieldA" => "My field" }) 53 | end 54 | 55 | it "sets errors on the validatable when invalid and raise_on_invalid is false" do 56 | described_class.call(validatable, 57 | allowed_fields: ["fieldC"], 58 | raise_on_invalid: false) 59 | 60 | expect(validatable.errors.length).to eq 1 61 | expect(validatable.errors.first.message).to eq "Unexpected fields: fieldA" 62 | end 63 | 64 | it "doesn't set errors on the validatable when valid" do 65 | described_class.call(validatable, 66 | allowed_fields: ["fieldA"], 67 | raise_on_invalid: false) 68 | 69 | expect(validatable.errors).to be_empty 70 | end 71 | end 72 | end 73 | 74 | def create_validatable(input) 75 | node_factory_context = create_node_factory_context(input) 76 | Openapi3Parser::Validation::Validatable.new( 77 | Openapi3Parser::NodeFactory::Map.new(node_factory_context) 78 | ) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/openapi3_parser/validators/url_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Openapi3Parser::Validators::Url do 4 | describe ".call" do 5 | it "returns nil for a valid URL" do 6 | expect(described_class.call("https://example.org/resource")) 7 | .to be_nil 8 | end 9 | 10 | it "returns an error for an invalid URL" do 11 | expect(described_class.call("not a URL")) 12 | .to eq %("not a URL" is not a valid URL) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | SimpleCov.start 5 | 6 | require "openapi3_parser" 7 | require "webmock/rspec" 8 | 9 | files = Dir.glob(File.join(__dir__, "support", "**", "*.rb")) 10 | files.each { |file| require file } 11 | 12 | RSpec.configure do |config| 13 | include Helpers::Context 14 | include Helpers::Source 15 | 16 | config.disable_monkey_patching! 17 | 18 | config.order = :random 19 | 20 | Kernel.srand config.seed 21 | WebMock.disable_net_connect! 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/default_field.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "default field" do |field:, defaults_to:, var_name: nil| 4 | var_name ||= field.to_sym 5 | 6 | context "when #{field} is not set" do 7 | let(var_name) { nil } 8 | 9 | it "has a default value of #{defaults_to}" do 10 | node = described_class.new(node_factory_context).node(node_context) 11 | expect(node[field]).to eq defaults_to 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/helpers/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Helpers 4 | module Context 5 | def create_node_factory_context(input, 6 | document_input: {}, 7 | document: nil, 8 | pointer_segments: [], 9 | reference_pointer_fragments: []) 10 | source_input = Openapi3Parser::SourceInput::Raw.new(document_input) 11 | document ||= Openapi3Parser::Document.new(source_input) 12 | location = Openapi3Parser::Source::Location.new( 13 | document.root_source, 14 | pointer_segments 15 | ) 16 | 17 | reference_locations = reference_pointer_fragments.map do |fragment| 18 | Openapi3Parser::Source::Location.new( 19 | document.root_source, 20 | Openapi3Parser::Source::Pointer.from_fragment(fragment).segments 21 | ) 22 | end 23 | 24 | Openapi3Parser::NodeFactory::Context.new( 25 | input, 26 | source_location: location, 27 | reference_locations: 28 | ) 29 | end 30 | 31 | def node_factory_context_to_node_context(node_factory_context) 32 | Openapi3Parser::Node::Context.new( 33 | node_factory_context.input, 34 | document_location: node_factory_context.source_location, 35 | source_location: node_factory_context.source_location 36 | ) 37 | end 38 | 39 | def create_node_context(input, document_input: {}, pointer_segments: []) 40 | source_input = Openapi3Parser::SourceInput::Raw.new(document_input) 41 | document = Openapi3Parser::Document.new(source_input) 42 | location = Openapi3Parser::Source::Location.new( 43 | document.root_source, 44 | pointer_segments 45 | ) 46 | Openapi3Parser::Node::Context.new(input, 47 | document_location: location, 48 | source_location: location) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/support/helpers/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Helpers 4 | module Source 5 | def create_source_location(source_input, 6 | document: nil, 7 | pointer_segments: []) 8 | source = create_source(source_input, document:) 9 | Openapi3Parser::Source::Location.new(source, pointer_segments) 10 | end 11 | 12 | def create_source(source_input, document: nil) 13 | unless source_input.is_a?(Openapi3Parser::SourceInput) 14 | source_input = Openapi3Parser::SourceInput::Raw.new(source_input) 15 | end 16 | 17 | if document 18 | registry = Openapi3Parser::Document::ReferenceRegistry.new 19 | Openapi3Parser::Source.new(source_input, document, registry) 20 | else 21 | Openapi3Parser::Document.new(source_input).root_source 22 | end 23 | end 24 | 25 | def create_file_source_input(data: {}, 26 | path: "/path/to/openapi.yaml", 27 | working_directory: nil) 28 | allow(File) 29 | .to receive(:read) 30 | .with(path) 31 | .and_return(data.to_yaml) 32 | Openapi3Parser::SourceInput::File 33 | .new(path, working_directory:) 34 | end 35 | 36 | def create_raw_source_input(data: {}, 37 | base_url: nil, 38 | working_directory: nil) 39 | Openapi3Parser::SourceInput::Raw 40 | .new(data, 41 | base_url:, 42 | working_directory:) 43 | end 44 | 45 | def create_url_source_input(data: {}, 46 | url: "https://example.com/openapi.yaml") 47 | stub_request(:get, url) 48 | .to_return(body: data.to_yaml, status: 200) 49 | Openapi3Parser::SourceInput::Url.new(url) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/support/matchers/have_validation_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec::Matchers.define :have_validation_error do |expected_location| 4 | match do |actual| 5 | errors_hash = actual.errors.to_h 6 | locations = errors_hash.keys 7 | 8 | return false unless locations.include?(expected_location) 9 | return true unless expected_message 10 | 11 | errors_hash[expected_location].any? do |message| 12 | if expected_message.is_a?(Regexp) 13 | expected_message.match(message) 14 | else 15 | message == expected_message 16 | end 17 | end 18 | end 19 | 20 | chain :with_message, :expected_message 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/mutually_exclusive_example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "mutually exclusive example" do 4 | let(:input) { {} } 5 | 6 | it "is valid when neither example or examples are provided" do 7 | instance = described_class.new(create_node_factory_context(input)) 8 | expect(instance).to be_valid 9 | end 10 | 11 | it "is valid when one of them is provided" do 12 | factory_context = create_node_factory_context( 13 | input.merge({ "example" => "anything" }) 14 | ) 15 | expect(described_class.new(factory_context)).to be_valid 16 | 17 | factory_context = create_node_factory_context( 18 | input.merge({ "examples" => {} }) 19 | ) 20 | expect(described_class.new(factory_context)).to be_valid 21 | end 22 | 23 | it "is invalid when both of them are provided" do 24 | factory_context = create_node_factory_context({ "example" => "anything", 25 | "examples" => {} }) 26 | instance = described_class.new(factory_context) 27 | expect(instance).not_to be_valid 28 | expect(instance) 29 | .to have_validation_error("#/") 30 | .with_message("example and examples are mutually exclusive fields") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/support/node_equality.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "node equality" do |input| 4 | describe "#==" do 5 | let(:context) { create_node_context({}) } 6 | 7 | it "is equal when context and input are the same" do 8 | instance = described_class.new(input, context) 9 | other = described_class.new(input, context) 10 | expect(instance).to eq(other) 11 | end 12 | 13 | it "is equal when class, input and source locations match but document location doesn't" do 14 | instance = described_class.new(input, context) 15 | other_context = Openapi3Parser::Node::Context.new( 16 | {}, 17 | document_location: Openapi3Parser::Source::Location.new( 18 | context.document_location.source, 19 | %w[different] 20 | ), 21 | source_location: context.source_location 22 | ) 23 | other = described_class.new(input, other_context) 24 | expect(instance).to eq(other) 25 | end 26 | 27 | it "isn't equal when the class differs" do 28 | instance = described_class.new(input, context) 29 | other = Openapi3Parser::Node::Contact.new({}, context) 30 | expect(instance).not_to eq(other) 31 | end 32 | 33 | it "isn't equal when source is different" do 34 | instance = described_class.new(input, context) 35 | other_context = Openapi3Parser::Node::Context.new( 36 | {}, 37 | document_location: Openapi3Parser::Source::Location.new( 38 | context.document_location.source, 39 | %w[different] 40 | ), 41 | source_location: Openapi3Parser::Source::Location.new( 42 | context.document_location.source, 43 | %w[different] 44 | ) 45 | ) 46 | 47 | other = described_class.new(input, other_context) 48 | expect(instance).not_to eq(other) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/support/node_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "node factory" do |data_type| 4 | it "responds to #node" do 5 | instance = described_class.new(node_factory_context) 6 | expect(instance).to respond_to(:node) 7 | end 8 | 9 | it "responds to #default" do 10 | instance = described_class.new(node_factory_context) 11 | expect(instance).to respond_to(:default) 12 | end 13 | 14 | it "defaults to being valid" do 15 | expect(described_class.new(node_factory_context)).to be_valid 16 | end 17 | 18 | it "defaults to having an empty error collection" do 19 | errors = described_class.new(node_factory_context).errors 20 | 21 | expect(errors).to be_a(Openapi3Parser::Validation::ErrorCollection) 22 | expect(errors).to be_empty 23 | end 24 | 25 | it "has #data in the expected type" do 26 | expect(described_class.new(node_factory_context).data) 27 | .to be_a(data_type) 28 | end 29 | 30 | it "has #resolved_input in the expected type" do 31 | expect(described_class.new(node_factory_context).resolved_input) 32 | .to be_a(data_type) 33 | end 34 | 35 | it "has #raw_input in the expected type" do 36 | expect(described_class.new(node_factory_context).raw_input) 37 | .to be_a(data_type) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/node_object_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "node object factory" do |klass| 4 | let(:node_factory_context) { create_node_factory_context(input) } 5 | let(:node_context) do 6 | node_factory_context_to_node_context(node_factory_context) 7 | end 8 | 9 | describe "#node" do 10 | it "returns an instance of #{klass}" do 11 | expect(described_class.new(node_factory_context).node(node_context)) 12 | .to be_a(klass) 13 | end 14 | end 15 | 16 | describe "#valid?" do 17 | it "is valid" do 18 | expect(described_class.new(node_factory_context).valid?).to be(true) 19 | end 20 | end 21 | 22 | describe "#errors" do 23 | it "has no errors" do 24 | expect(described_class.new(node_factory_context).errors).to be_empty 25 | end 26 | end 27 | end 28 | --------------------------------------------------------------------------------