├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_ignore.yml ├── .ruby-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── Steepfile ├── bin ├── console └── setup ├── lib ├── openapi_parser.rb └── openapi_parser │ ├── concern.rb │ ├── concerns │ ├── expandable.rb │ ├── findable.rb │ ├── media_type_selectable.rb │ ├── parameter_validatable.rb │ ├── parser.rb │ ├── parser │ │ ├── core.rb │ │ ├── hash.rb │ │ ├── hash_body.rb │ │ ├── list.rb │ │ ├── object.rb │ │ └── value.rb │ ├── schema_loader.rb │ └── schema_loader │ │ ├── base.rb │ │ ├── creator.rb │ │ ├── hash_body_loader.rb │ │ ├── hash_objects_loader.rb │ │ ├── list_loader.rb │ │ ├── objects_loader.rb │ │ └── values_loader.rb │ ├── config.rb │ ├── errors.rb │ ├── parameter_validator.rb │ ├── path_item_finder.rb │ ├── reference_expander.rb │ ├── request_operation.rb │ ├── schema_validator.rb │ ├── schema_validator │ ├── all_of_validator.rb │ ├── any_of_validator.rb │ ├── array_validator.rb │ ├── base.rb │ ├── boolean_validator.rb │ ├── enumable.rb │ ├── float_validator.rb │ ├── integer_validator.rb │ ├── minimum_maximum.rb │ ├── nil_validator.rb │ ├── object_validator.rb │ ├── one_of_validator.rb │ ├── options.rb │ ├── properties_number.rb │ ├── string_validator.rb │ └── unspecified_type_validator.rb │ ├── schemas.rb │ ├── schemas │ ├── base.rb │ ├── classes.rb │ ├── components.rb │ ├── discriminator.rb │ ├── header.rb │ ├── info.rb │ ├── media_type.rb │ ├── openapi.rb │ ├── operation.rb │ ├── parameter.rb │ ├── path_item.rb │ ├── paths.rb │ ├── reference.rb │ ├── request_body.rb │ ├── response.rb │ ├── responses.rb │ └── schema.rb │ └── version.rb ├── openapi_parser.gemspec ├── sig ├── openapi_parser.rbs ├── openapi_parser │ ├── config.rbs │ ├── errors.rbs │ ├── reference_expander.rbs │ ├── schema_validator.rbs │ ├── schema_validators │ │ ├── base.rbs │ │ └── options.rbs │ ├── schemas │ │ └── base.rbs │ └── version.rbs ├── types.rbs └── wip_types.rbs └── spec ├── data ├── cyclic-remote-ref1.yaml ├── cyclic-remote-ref2.yaml ├── normal.yml ├── path-item-ref-relative.yaml ├── path-item-ref.yaml ├── petstore-expanded.yaml ├── petstore-with-discriminator.yaml ├── petstore-with-mapped-polymorphism.yaml ├── petstore-with-polymorphism.yaml ├── petstore.json ├── petstore.json.unsupported_extension ├── petstore.yaml.unsupported_extension ├── reference-broken.yaml ├── reference_in_responses.yaml ├── remote-file-ref.yaml ├── remote-http-ref.yaml └── validate_test.yaml ├── openapi_parser ├── concerns │ ├── expandable_spec.rb │ ├── findable_spec.rb │ ├── media_type_selectable_spec.rb │ └── schema_loader │ │ └── base_spec.rb ├── parameter_validator_spec.rb ├── path_item_finder_spec.rb ├── path_item_ref_spec.rb ├── request_operation_spec.rb ├── schema_validator │ ├── all_of_validator_spec.rb │ ├── array_validator_spec.rb │ ├── base_spec.rb │ ├── integer_validator_spec.rb │ ├── object_validator_spec.rb │ └── string_validator_spec.rb ├── schema_validator_spec.rb └── schemas │ ├── base_spec.rb │ ├── components_spec.rb │ ├── discriminator_spec.rb │ ├── header_spec.rb │ ├── media_type_spec.rb │ ├── open_api_spec.rb │ ├── operation_spec.rb │ ├── parameter_spec.rb │ ├── path_item_spec.rb │ ├── paths_spec.rb │ ├── polymorphism_spec.rb │ ├── reference_spec.rb │ ├── request_body_spec.rb │ ├── response_spec.rb │ ├── responses_spec.rb │ └── schema_spec.rb ├── openapi_parser_spec.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: 9 | - ubuntu-latest 10 | - macos-latest 11 | ruby: 12 | - "2.7" 13 | - "3.0" 14 | - "3.1" 15 | - "3.2" 16 | - "3.3" 17 | - "3.4" 18 | - ruby-head 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle exec rake 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # RubyMine 11 | .idea 12 | 13 | # rspec failure tracking 14 | .rspec_status 15 | Gemfile.lock 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | fincop: 3 | # - 'config/rails.yml' 4 | - 'config/rspec.yml' 5 | - 'config/rubocop.yml' 6 | # If you are using Rails 4, activate this cop. 7 | # - 'config/disabled_for_rails_4.yml' 8 | 9 | inherit_from: 10 | - '.rubocop_ignore.yml' 11 | 12 | AllCops: 13 | TargetRubyVersion: 2.5 14 | -------------------------------------------------------------------------------- /.rubocop_ignore.yml: -------------------------------------------------------------------------------- 1 | Metrics/ParameterLists: 2 | Exclude: 3 | - lib/openapi_parser/concerns/parser.rb 4 | 5 | RSpec/FilePath: 6 | Enabled: false 7 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 2.2.6 (2025-04-07) 4 | * support validator options and allow_empty_date_and_datetime for response validate options #183 5 | 6 | ## 2.2.5 (2025-04-05) 7 | * add allow_empty_date_and_datetime option for more lenient format parsing #182 8 | 9 | ## 2.2.4 (2025-02-27) 10 | * skip validation if format is set to `binary` #180 11 | 12 | ## 2.2.3 (2024-12-12) 13 | * fix parent_discriminator_schema nil from all_of_validator #179 14 | 15 | ## 2.2.2 (2024-11-21) 16 | * make content-type lookup case-insensitive #178 17 | 18 | ## 2.2.1 (2024-10-23) 19 | * revert schemas validation in additionalProperties and remove additionalProperties logic from allof (breaking things for me, will revisit) 20 | 21 | ## 2.2.0 (2024-10-23) 22 | ### Fixed 23 | * support schemas validation in additionalProperties and remove additionalProperties logic from allof 24 | * suppress method redefined warning 25 | * add base64 dev dependency for ruby-head compatibility 26 | * remove no longer needed activesupport dev dep thanks to a fix upstream 27 | * add dependabot config 28 | * remove no longer used code for old rubies 29 | * bump actions/checkout 30 | * add ruby 3.3 to test matrix 31 | * fix uuid validation logic (make it more strict) 32 | 33 | ## 2.1.0 (2024-04-03) 34 | ### Fixed 35 | * Add full-date compliant date format validation #164 36 | 37 | ## 2.0.0 (2023-10-15) 38 | 39 | ### Added 40 | 41 | * Support for uniqueItems in array #154 42 | * Fix nullable field does not work with allOf, anyOf and oneOf keyword #128 43 | 44 | ### Other 45 | 46 | * Drop Ruby 2.6 Support #158 47 | 48 | ## 1.0.0 (2021-02-03) 49 | ### Added 50 | * Add date-time format validation #126 51 | 52 | ## 1.0.0.beta1 (2021-12-15) 53 | ### Added 54 | * Add strict_reference_validation config and implementation to address/implement #29 #123 55 | 56 | ## 0.15.0 (2021-09-27) 57 | ### Added 58 | * support: relative file path escape. #117 59 | 60 | ## 0.14.1 (2021-07-9) 61 | ### Fixed 62 | * Fix bug for using path parameter and coerce option #115 63 | 64 | ## 0.14.0 (2021-05-24) 65 | 66 | ### Added 67 | * Add basic polymorphism handling #103 68 | * Support empty schema as any type #109 69 | * Add date format validation for string #102 70 | 71 | ### Fixed 72 | * Fix anyOf coercion to float and integer when value is a non-string type #110 73 | 74 | ## 0.13.0 (2021-05-01) 75 | * Fix a problem with remote reference to path items which have path parameters #95 76 | * Support enum for booleans. #104 77 | 78 | ## 0.12.1 (2020-08-27) 79 | * Use CGI.unescape (warning fix) #92 80 | 81 | ## 0.12.0 (2020-08-26) 82 | * Find path by extracted params than path length #84 83 | * Unescape ref URI before lookup in OpenAPIParser::Findable #85 84 | * Improved path parameter matching code to allow file extensions, multiple parameters inside one path element, etc #90 85 | 86 | ## 0.11.2 (2020-05-23) 87 | * Allow date and time content in YAML #81 88 | 89 | ## 0.11.1 (2020-05-09) 90 | * fix too many warning 91 | 92 | ## 0.11.0 (2020-05-09) 93 | * Add committee friendly interface to use remote references. #74 94 | * Prevent SystemStackError on recursive schema reference #76 95 | * support newest ruby versions #78 96 | 97 | ## 0.10.0 (2020-04-01) 98 | * Support $ref to objects in other OpenAPI yaml files #66 99 | * Allow $ref for path item objects #71 100 | 101 | ## 0.9.0 (2020-03-22) 102 | * Added support for validating UUID formatted strings #67 103 | 104 | ## 0.8.0 (2020-01-21) 105 | * Append the example to the Pattern validator error message #64 106 | 107 | ## 0.7.0 (2020-01-15) 108 | * Avoid potential `.send(:exit)` #58 109 | * Improve PathItemFinder #44 110 | 111 | ## 0.6.1 (2019-10-12) 112 | * Bugfix: validate non-nullable response header #54 113 | * Improve grammar in error messages #55 114 | * fix object validator in case of properties missing #56 115 | 116 | ## 0.6.0 (2019-10-05) 117 | * add email format validation on string #51 118 | 119 | ## 0.5.0 (2019-09-28) 120 | * Add max and min length validators for string. #45 121 | * Support for minItems and maxItems in array #49 122 | 123 | ## 0.4.1 (2019-07-27) 124 | * release missed 125 | 126 | ## 0.4.0 (2019-07-27) 127 | * Add minimum and maximum checks for `integer` and `number` data types (#43) 128 | 129 | ## 0.3.1 (2019-06-04) 130 | * Add additionalProperties default value (#40) 131 | 132 | ## 0.3.0 (2019-06-01) 133 | 134 | ### features 135 | * Perform a strict check on object properties (#33) 136 | 137 | ### Bugfix 138 | * Support discriminator without mapping (#35) 139 | * Fix upper case request param validation (#38) 140 | 141 | ## 0.2.7 (2019-05-20) 142 | * Fix for release miss 143 | 144 | ## 0.2.6 (2019-05-20) 145 | * Add support for discriminator (#32) 146 | 147 | ## 0.2.5 (2019-04-12) 148 | * Support one of validator (#26) 149 | 150 | ## 0.2.3 (2019-03-18) 151 | * validate_header_parameter support case incentive (#25) 152 | 153 | ## 0.2.2 (2019-01-06) 154 | * bugfix for datetime validate (#20) 155 | 156 | ## 0.2.1 (2019-01-03) 157 | * raise error when invalid datetime format (#19) 158 | 159 | ## 0.2.0 (2019-01-02) 160 | * support header validate (#18) 161 | 162 | ## 0.1.9 (2019-01-01) 163 | * add strict option (#17) 164 | 165 | ## 0.1.8 (2018-12-31) 166 | * add select_media_type method(#16) 167 | 168 | ## 0.1.7 (2018-12-30) 169 | * Float value validate bugfix (#15) 170 | 171 | ## 0.1.6 (2018-12-29) 172 | * Support allOf definition (#11) 173 | * Support wildcard status code (#12) 174 | * Support wildcard content type (#13) 175 | 176 | ## 0.1.5 (2018-12-23) 177 | * First release for production usage 178 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ota42y@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in openapi_parser.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 ota42y 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Parser 2 | [![ci](https://github.com/ota42y/openapi_parser/actions/workflows/ci.yaml/badge.svg)](https://github.com/ota42y/openapi_parser/actions/workflows/ci.yaml) 3 | [![Gem Version](https://badge.fury.io/rb/openapi_parser.svg)](https://badge.fury.io/rb/openapi_parser) 4 | [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/gems/openapi_parser) 5 | [![Inch CI](https://inch-ci.org/github/ota42y/openapi_parser.svg?branch=master)](https://inch-ci.org/github/ota42y/openapi_parser) 6 | 7 | This is OpenAPI3 parser and validator. 8 | 9 | ## Usage 10 | 11 | ```ruby 12 | root = OpenAPIParser.parse(YAML.load_file('open_api_3/schema.yml')) 13 | 14 | # request operation combine path parameters and OpenAPI3's Operation Object 15 | request_operation = root.request_operation(:post, '/validate') 16 | 17 | ret = request_operation.validate_request_body('application/json', {"integer" => 1}) 18 | # => {"integer" => 1} 19 | 20 | # invalid parameter 21 | request_operation.validate_request_body('application/json', {"integer" => '1'}) 22 | # => OpenAPIParser::ValidateError: 1 class is String but it's not valid integer in #/paths/~1validate/post/requestBody/content/application~1json/schema/properties/integer 23 | 24 | # path parameter 25 | request_operation = root.request_operation(:get, '/path_template_test/1') 26 | request_operation.path_params 27 | # => {"template_name"=>"1"} 28 | 29 | # coerce parameter 30 | root = OpenAPIParser.parse(YAML.load_file('open_api_3/schema.yml'), {coerce_value: true, datetime_coerce_class: DateTime}) 31 | request_operation = root.request_operation(:get, '/string_params_coercer') 32 | request_operation.validate_request_parameter({'integer_1' => '1', 'datetime_string' => '2016-04-01T16:00:00+09:00'}) 33 | # => {"integer_1"=>1, "datetime_string"=># 34 | # convert number string to Integer and datetime string to DateTime class 35 | 36 | ``` 37 | 38 | ## Installation 39 | 40 | Add this line to your application's Gemfile: 41 | 42 | ```ruby 43 | gem 'openapi_parser' 44 | ``` 45 | 46 | And then execute: 47 | 48 | $ bundle 49 | 50 | Or install it yourself as: 51 | 52 | $ gem install openapi_parser 53 | 54 | ## Additional features 55 | OpenAPI Parser's validation based on [OpenAPI spec](https://github.com/OAI/OpenAPI-Specification) 56 | But we support few useful features. 57 | 58 | ### type validation 59 | We support additional type validation. 60 | 61 | |type|format|description| 62 | |---|---|---| 63 | |string|uuid|validate uuid string. But we don't check uuid layout| 64 | 65 | ### Reference Validation on Schema Load 66 | Invalid references (missing definitions, typos, etc.) can cause validation to fail in runtime, 67 | and these errors can be difficult to debug (see: https://github.com/ota42y/openapi_parser/issues/29). 68 | 69 | Pass the `strict_reference_validation: true` option to detect invalid references. 70 | An `OpenAPIError::MissingReferenceError` exception will be raised when a reference cannot be resolved. 71 | 72 | If the `expand_reference` configuration is explicitly `false` (default is `true`), then 73 | this configuration has no effect. 74 | 75 | DEPRECATION NOTICE: To maintain compatibility with the previous behavior, this version sets `false` as a default. 76 | This behavior will be changed to `true` in a later version, so you should explicitly pass `strict_reference_validation: false` 77 | if you wish to keep the old behavior (and please let the maintainers know your use-case for this configuration!). 78 | 79 | ```ruby 80 | yaml_file = YAML.load_file('open_api_3/schema_with_broken_references.yml') 81 | options = { 82 | coerce_value: true, 83 | datetime_coerce_class: DateTime, 84 | # This defaults to false (for now) - passing `true` provides load-time validation of refs 85 | strict_reference_validation: true 86 | } 87 | 88 | # Will raise with OpenAPIParser::MissingReferenceError 89 | OpenAPIParser.parse(yaml_file, options) 90 | ``` 91 | 92 | ## ToDo 93 | - correct schema checker 94 | - more detailed validator 95 | 96 | ## Development 97 | 98 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 99 | 100 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 101 | 102 | ## Contributing 103 | 104 | Bug reports and pull requests are welcome on GitHub at https://github.com/ota42y/openapi_parser. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 105 | 106 | ## License 107 | 108 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 109 | 110 | ## Code of Conduct 111 | 112 | Everyone interacting in the OpenAPIParser project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ota42y/openapi_parser/blob/master/CODE_OF_CONDUCT.md). 113 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :steep do 7 | sh 'steep check' 8 | end 9 | 10 | task :default => [:steep, :spec] 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please contact 4 | [Tidelift security](https://tidelift.com/security). 5 | 6 | Tidelift will coordinate the fix and disclosure. 7 | -------------------------------------------------------------------------------- /Steepfile: -------------------------------------------------------------------------------- 1 | target :lib do 2 | signature "sig" 3 | #check "lib" 4 | 5 | check "lib/openapi_parser.rb" 6 | check "lib/openapi_parser/config.rb" 7 | check "lib/openapi_parser/schema_validator/options.rb" 8 | check "lib/openapi_parser/schema_validator/base.rb" 9 | 10 | library 'uri', 'json', 'pathname' 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "openapi_parser" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/openapi_parser.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'time' 3 | require 'json' 4 | require 'psych' 5 | require 'pathname' 6 | require 'open-uri' 7 | 8 | require 'openapi_parser/version' 9 | require 'openapi_parser/config' 10 | require 'openapi_parser/errors' 11 | require 'openapi_parser/concern' 12 | require 'openapi_parser/schemas' 13 | require 'openapi_parser/path_item_finder' 14 | require 'openapi_parser/request_operation' 15 | require 'openapi_parser/schema_validator' 16 | require 'openapi_parser/parameter_validator' 17 | require 'openapi_parser/reference_expander' 18 | 19 | module OpenAPIParser 20 | class << self 21 | # Load schema hash object. Uri is not set for returned schema. 22 | # @return [OpenAPIParser::Schemas::OpenAPI] 23 | def parse(schema, config = {}) 24 | load_hash(schema, config: Config.new(config), uri: nil, schema_registry: {}) 25 | end 26 | 27 | # @param filepath [String] Path of the file containing the passed schema. 28 | # Used for resolving remote $ref if provided. 29 | # If file path is relative, it is resolved using working directory. 30 | # @return [OpenAPIParser::Schemas::OpenAPI] 31 | def parse_with_filepath(schema, filepath, config = {}) 32 | load_hash(schema, config: Config.new(config), uri: filepath && file_uri(filepath), schema_registry: {}) 33 | end 34 | 35 | # Load schema in specified filepath. If file path is relative, it is resolved using working directory. 36 | # @return [OpenAPIParser::Schemas::OpenAPI] 37 | def load(filepath, config = {}) 38 | load_uri(file_uri(filepath), config: Config.new(config), schema_registry: {}) 39 | end 40 | 41 | # Load schema located by the passed uri. Uri must be absolute. 42 | # @return [OpenAPIParser::Schemas::OpenAPI] 43 | def load_uri(uri, config:, schema_registry:) 44 | # Open-uri doesn't open file scheme uri, so we try to open file path directly 45 | # File scheme uri which points to a remote file is not supported. 46 | uri_path = uri.path 47 | raise "file not found" if uri_path.nil? 48 | 49 | content = if uri.scheme == 'file' 50 | open(uri_path)&.read 51 | elsif uri.is_a?(OpenURI::OpenRead) 52 | uri.open()&.read 53 | end 54 | 55 | extension = Pathname.new(uri_path).extname 56 | load_hash(parse_file(content, extension), config: config, uri: uri, schema_registry: schema_registry) 57 | end 58 | 59 | private 60 | 61 | def file_uri(filepath) 62 | path = Pathname.new(filepath) 63 | path = Pathname.getwd + path if path.relative? 64 | URI.join("file:///", path.to_s) 65 | end 66 | 67 | def parse_file(content, ext) 68 | case ext.downcase 69 | when '.yaml', '.yml' 70 | parse_yaml(content) 71 | when '.json' 72 | parse_json(content) 73 | else 74 | # When extension is something we don't know, try to parse as json first. If it fails, parse as yaml 75 | begin 76 | parse_json(content) 77 | rescue JSON::ParserError 78 | parse_yaml(content) 79 | end 80 | end 81 | end 82 | 83 | def parse_yaml(content) 84 | Psych.safe_load(content, permitted_classes: [Date, Time]) 85 | end 86 | 87 | def parse_json(content) 88 | raise "json content is nil" unless content 89 | JSON.parse(content) 90 | end 91 | 92 | def load_hash(hash, config:, uri:, schema_registry:) 93 | root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry) 94 | 95 | OpenAPIParser::ReferenceExpander.expand(root, config.strict_reference_validation) if config.expand_reference 96 | 97 | # TODO: use callbacks 98 | root.paths&.path&.values&.each do | path_item | 99 | path_item.set_path_item_to_operation 100 | end 101 | 102 | root 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/openapi_parser/concern.rb: -------------------------------------------------------------------------------- 1 | require_relative 'concerns/parser' 2 | require_relative 'concerns/findable' 3 | require_relative 'concerns/expandable' 4 | require_relative 'concerns/media_type_selectable' 5 | require_relative 'concerns/parameter_validatable' 6 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/expandable.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Expandable 2 | # expand refs 3 | # @param [OpenAPIParser::Schemas::Base] root 4 | # @return nil 5 | def expand_reference(root, validate_references) 6 | expand_list_objects(root, self.class._openapi_attr_list_objects.keys, validate_references) 7 | expand_objects(root, self.class._openapi_attr_objects.keys, validate_references) 8 | expand_hash_objects(root, self.class._openapi_attr_hash_objects.keys, validate_references) 9 | expand_hash_objects(root, self.class._openapi_attr_hash_body_objects.keys, validate_references) 10 | nil 11 | end 12 | 13 | private 14 | 15 | def expand_hash_objects(root, attribute_names, validate_references) 16 | return unless attribute_names 17 | 18 | attribute_names.each { |name| expand_hash_attribute(root, name, validate_references) } 19 | end 20 | 21 | def expand_hash_attribute(root, name, validate_references) 22 | h = send(name) 23 | return if h.nil? 24 | 25 | update_values = h.map do |k, v| 26 | new_object = expand_object(root, v, validate_references) 27 | new_object.nil? ? nil : [k, new_object] 28 | end 29 | 30 | update_values.compact.each do |k, v| 31 | _update_child_object(h[k], v) 32 | h[k] = v 33 | end 34 | end 35 | 36 | def expand_objects(root, attribute_names, validate_references) 37 | return unless attribute_names 38 | 39 | attribute_names.each do |name| 40 | v = send(name) 41 | next if v.nil? 42 | 43 | new_object = expand_object(root, v, validate_references) 44 | next if new_object.nil? 45 | 46 | _update_child_object(v, new_object) 47 | self.instance_variable_set("@#{name}", new_object) 48 | end 49 | end 50 | 51 | def expand_list_objects(root, attribute_names, validate_references) 52 | return unless attribute_names 53 | 54 | attribute_names.each do |name| 55 | l = send(name) 56 | next if l.nil? 57 | 58 | l.each_with_index do |v, idx| 59 | new_object = expand_object(root, v, validate_references) 60 | next if new_object.nil? 61 | 62 | _update_child_object(v, new_object) 63 | l[idx] = new_object 64 | end 65 | end 66 | end 67 | 68 | def expand_object(root, object, validate_references) 69 | if object.kind_of?(OpenAPIParser::Schemas::Reference) 70 | ref_object = referenced_object(root, object) 71 | raise OpenAPIParser::MissingReferenceError.new(object.ref) if ref_object.nil? && validate_references 72 | 73 | return ref_object 74 | end 75 | 76 | object.expand_reference(root, validate_references) if object.kind_of?(OpenAPIParser::Expandable) 77 | nil 78 | end 79 | 80 | # @param [OpenAPIParser::Schemas::OpenAPI] root 81 | # @param [OpenAPIParser::Schemas::Reference] reference 82 | def referenced_object(root, reference) 83 | obj = root.find_object(reference.ref) 84 | 85 | obj.kind_of?(OpenAPIParser::Schemas::Reference) ? referenced_object(root, obj) : obj 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/findable.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'uri' 3 | 4 | module OpenAPIParser::Findable 5 | # @param [String] reference 6 | # @return [OpenAPIParser::Findable] 7 | def find_object(reference) 8 | return self if object_reference == reference 9 | remote_reference = !reference.start_with?('#') 10 | return find_remote_object(reference) if remote_reference 11 | return nil unless reference.start_with?(object_reference) 12 | 13 | unescaped_reference = CGI.unescape(reference) 14 | 15 | @find_object_cache = {} unless defined? @find_object_cache 16 | if (obj = @find_object_cache[unescaped_reference]) 17 | return obj 18 | end 19 | 20 | if (child = _openapi_all_child_objects[unescaped_reference]) 21 | @find_object_cache[unescaped_reference] = child 22 | return child 23 | end 24 | 25 | _openapi_all_child_objects.values.each do |c| 26 | if (obj = c.find_object(unescaped_reference)) 27 | @find_object_cache[unescaped_reference] = obj 28 | return obj 29 | end 30 | end 31 | 32 | nil 33 | end 34 | 35 | def purge_object_cache 36 | @purged = false unless defined? @purged 37 | 38 | return if @purged 39 | 40 | @find_object_cache = {} 41 | @purged = true 42 | 43 | _openapi_all_child_objects.values.each(&:purge_object_cache) 44 | end 45 | 46 | private 47 | 48 | def find_remote_object(reference) 49 | uri, fragment = reference.split("#", 2) 50 | reference_uri = URI(uri) 51 | reference_uri.fragment = nil 52 | root.load_another_schema(reference_uri)&.find_object("##{fragment}") 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/media_type_selectable.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::MediaTypeSelectable 2 | private 3 | 4 | # select media type by content_type (consider wild card definition) 5 | # @param [String] content_type 6 | # @param [Hash{String => OpenAPIParser::Schemas::MediaType}] content 7 | # @return [OpenAPIParser::Schemas::MediaType, nil] 8 | def select_media_type_from_content(content_type, content) 9 | return nil unless content_type 10 | return nil unless content 11 | 12 | if (media_type = content[content_type]) 13 | return media_type 14 | end 15 | 16 | # application/json => [application, json] 17 | splited = content_type.split('/') 18 | 19 | if (media_type = content["#{splited.first}/*"]) 20 | return media_type 21 | end 22 | 23 | if (media_type = content['*/*']) 24 | return media_type 25 | end 26 | 27 | nil 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parameter_validatable.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::ParameterValidatable 2 | # @param [Hash] path_params path parameters 3 | # @param [OpenAPIParser::SchemaValidator::Options] options request validator options 4 | def validate_path_params(path_params, options) 5 | OpenAPIParser::ParameterValidator.validate_parameter(path_parameter_hash, path_params, object_reference, options) 6 | end 7 | 8 | # @param [Hash] params parameter hash 9 | # @param [Hash] headers headers hash 10 | # @param [OpenAPIParser::SchemaValidator::Options] options request validator options 11 | def validate_request_parameter(params, headers, options) 12 | validate_header_parameter(headers, object_reference, options) if options.validate_header 13 | validate_query_parameter(params, object_reference, options) 14 | end 15 | 16 | # @param [PathItem] path_item parent 17 | def set_parent_path_item(path_item) 18 | @merged_parameter = (parameters || []) + (path_item.parameters || []) 19 | nil 20 | end 21 | 22 | private 23 | 24 | # @param [Hash] params query parameter hash 25 | # @param [String] object_reference 26 | # @param [OpenAPIParser::SchemaValidator::Options] options request validator options 27 | def validate_query_parameter(params, object_reference, options) 28 | OpenAPIParser::ParameterValidator.validate_parameter(query_parameter_hash, params, object_reference, options) 29 | end 30 | 31 | # @param [Hash] headers header hash 32 | # @param [String] object_reference 33 | # @param [OpenAPIParser::SchemaValidator::Options] options request validator options 34 | def validate_header_parameter(headers, object_reference, options) 35 | OpenAPIParser::ParameterValidator.validate_parameter(header_parameter_hash, headers, object_reference, options, true) 36 | end 37 | 38 | def header_parameter_hash 39 | divided_parameter_hash['header'] || [] 40 | end 41 | 42 | def path_parameter_hash 43 | divided_parameter_hash['path'] || [] 44 | end 45 | 46 | def query_parameter_hash 47 | divided_parameter_hash['query'] || [] 48 | end 49 | 50 | # @return [Hash{String => Hash{String => Parameter}}] hash[in][name] => Parameter 51 | def divided_parameter_hash 52 | @divided_parameter_hash ||= 53 | (@merged_parameter || []). 54 | group_by(&:in). 55 | map { |in_type, params| # rubocop:disable Style/BlockDelimiters 56 | [ 57 | in_type, 58 | params.map { |param| [param.name, param] }.to_h, 59 | ] 60 | }.to_h 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser 2 | end 3 | 4 | require 'forwardable' 5 | 6 | require_relative './parser/core' 7 | require_relative './schema_loader' 8 | 9 | module OpenAPIParser::Parser 10 | def self.included(base) 11 | base.extend(ClassMethods) 12 | end 13 | module ClassMethods 14 | extend Forwardable 15 | 16 | def_delegators :_parser_core, :_openapi_attr_values, :openapi_attr_value, :openapi_attr_values 17 | def_delegators :_parser_core, :_openapi_attr_objects, :openapi_attr_objects, :openapi_attr_object 18 | def_delegators :_parser_core, :_openapi_attr_list_objects, :openapi_attr_list_object 19 | def_delegators :_parser_core, :_openapi_attr_hash_objects, :openapi_attr_hash_object 20 | def_delegators :_parser_core, :_openapi_attr_hash_body_objects, :openapi_attr_hash_body_objects 21 | 22 | def _parser_core 23 | @_parser_core ||= OpenAPIParser::Parser::Core.new(self) 24 | end 25 | end 26 | 27 | # @param [OpenAPIParser::Schemas::Base] old 28 | # @param [OpenAPIParser::Schemas::Base] new 29 | def _update_child_object(old, new) 30 | _openapi_all_child_objects[old.object_reference] = new 31 | end 32 | 33 | # @return [Hash{String => OpenAPIParser::Schemas::Base}] 34 | def _openapi_all_child_objects 35 | @_openapi_all_child_objects ||= {} 36 | end 37 | 38 | # load data by schema definition in core and set children to _openapi_all_child_objects 39 | # @return nil 40 | def load_data 41 | loader = ::OpenAPIParser::SchemaLoader.new(self, self.class._parser_core) 42 | @_openapi_all_child_objects = loader.load_data 43 | nil 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/core.rb: -------------------------------------------------------------------------------- 1 | require_relative './value' 2 | require_relative './object' 3 | require_relative './list' 4 | require_relative './hash' 5 | require_relative './hash_body' 6 | 7 | class OpenAPIParser::Parser::Core 8 | include OpenAPIParser::Parser::Value 9 | include OpenAPIParser::Parser::Object 10 | include OpenAPIParser::Parser::List 11 | include OpenAPIParser::Parser::Hash 12 | include OpenAPIParser::Parser::HashBody 13 | 14 | def initialize(target_klass) 15 | @target_klass = target_klass 16 | end 17 | 18 | private 19 | 20 | attr_reader :target_klass 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/hash.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser::Hash 2 | def _openapi_attr_hash_objects 3 | @_openapi_attr_hash_objects ||= {} 4 | end 5 | 6 | def openapi_attr_hash_object(name, klass, options = {}) 7 | target_klass.send(:attr_reader, name) 8 | _openapi_attr_hash_objects[name] = ::OpenAPIParser::SchemaLoader::HashObjectsLoader.new(name, options.merge(klass: klass)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/hash_body.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser::HashBody 2 | def _openapi_attr_hash_body_objects 3 | @_openapi_attr_hash_body_objects ||= {} 4 | end 5 | 6 | def openapi_attr_hash_body_objects(name, klass, options = {}) 7 | # options[:reject_keys] = options[:reject_keys] ? options[:reject_keys].map(&:to_s) : [] 8 | 9 | target_klass.send(:attr_reader, name) 10 | _openapi_attr_hash_body_objects[name] = ::OpenAPIParser::SchemaLoader::HashBodyLoader.new(name, options.merge(klass: klass)) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/list.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser::List 2 | def _openapi_attr_list_objects 3 | @_openapi_attr_list_objects ||= {} 4 | end 5 | 6 | def openapi_attr_list_object(name, klass, options = {}) 7 | target_klass.send(:attr_reader, name) 8 | _openapi_attr_list_objects[name] = OpenAPIParser::SchemaLoader::ListLoader.new(name, options.merge(klass: klass)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/object.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser::Object 2 | def _openapi_attr_objects 3 | @_openapi_attr_objects ||= {} 4 | end 5 | 6 | def openapi_attr_objects(*names, klass) 7 | names.each { |name| openapi_attr_object(name, klass) } 8 | end 9 | 10 | def openapi_attr_object(name, klass, options = {}) 11 | target_klass.send(:attr_reader, name) 12 | _openapi_attr_objects[name] = OpenAPIParser::SchemaLoader::ObjectsLoader.new(name, options.merge(klass: klass)) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/parser/value.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Parser::Value 2 | def _openapi_attr_values 3 | @_openapi_attr_values ||= {} 4 | end 5 | 6 | def openapi_attr_values(*names) 7 | names.each { |name| openapi_attr_value(name) } 8 | end 9 | 10 | def openapi_attr_value(name, options = {}) 11 | target_klass.send(:attr_reader, name) 12 | _openapi_attr_values[name] = OpenAPIParser::SchemaLoader::ValuesLoader.new(name, options) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaLoader 2 | end 3 | 4 | require_relative './schema_loader/base' 5 | require_relative './schema_loader/creator' 6 | require_relative './schema_loader/values_loader' 7 | require_relative './schema_loader/list_loader' 8 | require_relative './schema_loader/objects_loader' 9 | require_relative './schema_loader/hash_objects_loader' 10 | require_relative './schema_loader/hash_body_loader' 11 | 12 | # load data to target_object by schema definition in core 13 | class OpenAPIParser::SchemaLoader 14 | # @param [OpenAPIParser::Schemas::Base] target_object 15 | # @param [OpenAPIParser::Parser::Core] core 16 | def initialize(target_object, core) 17 | @target_object = target_object 18 | @core = core 19 | @children = {} 20 | end 21 | 22 | # @!attribute [r] children 23 | # @return [Array] 24 | attr_reader :children 25 | 26 | # execute load data 27 | # return data is equal to :children 28 | # @return [Array] 29 | def load_data 30 | all_loader.each { |l| load_data_by_schema_loader(l) } 31 | children 32 | end 33 | 34 | private 35 | 36 | attr_reader :core, :target_object 37 | 38 | # @param [OpenAPIParser::SchemaLoader::Base] schema_loader 39 | def load_data_by_schema_loader(schema_loader) 40 | children = schema_loader.load_data(target_object, target_object.raw_schema) 41 | 42 | children.each { |c| register_child(c) } if children 43 | end 44 | 45 | def register_child(object) 46 | return unless object.kind_of?(OpenAPIParser::Parser) 47 | 48 | @children[object.object_reference] = object 49 | end 50 | 51 | def all_loader 52 | core._openapi_attr_values.values + 53 | core._openapi_attr_objects.values + 54 | core._openapi_attr_list_objects.values + 55 | core._openapi_attr_hash_objects.values + 56 | core._openapi_attr_hash_body_objects.values 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/base.rb: -------------------------------------------------------------------------------- 1 | # loader base class 2 | class OpenAPIParser::SchemaLoader::Base 3 | # @param [String] variable_name 4 | # @param [Hash] options 5 | def initialize(variable_name, options) 6 | @variable_name = variable_name 7 | @schema_key = options[:schema_key] || variable_name 8 | end 9 | 10 | # @param [OpenAPIParser::Schemas::Base] _target_object 11 | # @param [Hash] _raw_schema 12 | # @return [Array, nil] 13 | def load_data(_target_object, _raw_schema) 14 | raise 'need implement' 15 | end 16 | 17 | private 18 | 19 | attr_reader :variable_name, :schema_key 20 | 21 | # create instance variable @variable_name using data 22 | # @param [OpenAPIParser::Schemas::Base] target 23 | # @param [String] variable_name 24 | # @param [Object] data 25 | def variable_set(target, variable_name, data) 26 | target.instance_variable_set("@#{variable_name}", data) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/creator.rb: -------------------------------------------------------------------------------- 1 | # loader base class for create OpenAPI::Schemas::Base object 2 | class OpenAPIParser::SchemaLoader::Creator < OpenAPIParser::SchemaLoader::Base 3 | # @param [String] variable_name 4 | # @param [Hash] options 5 | def initialize(variable_name, options) 6 | super(variable_name, options) 7 | 8 | @klass = options[:klass] 9 | @allow_reference = options[:reference] || false 10 | @allow_data_type = options[:allow_data_type] 11 | end 12 | 13 | private 14 | 15 | attr_reader :klass, :allow_reference, :allow_data_type 16 | 17 | def build_object_reference_from_base(base, names) 18 | names = [names] unless names.kind_of?(Array) 19 | ref = names.map { |n| escape_reference(n) }.join('/') 20 | 21 | "#{base}/#{ref}" 22 | end 23 | 24 | # @return Boolean 25 | def check_reference_schema?(check_schema) 26 | check_object_schema?(check_schema) && !check_schema['$ref'].nil? 27 | end 28 | 29 | def check_object_schema?(check_schema) 30 | check_schema.kind_of?(::Hash) 31 | end 32 | 33 | def escape_reference(str) 34 | str.to_s.gsub('/', '~1') 35 | end 36 | 37 | def build_openapi_object_from_option(target_object, ref, schema) 38 | return nil if schema.nil? 39 | 40 | if @allow_data_type && !check_object_schema?(schema) 41 | schema 42 | elsif @allow_reference && check_reference_schema?(schema) 43 | OpenAPIParser::Schemas::Reference.new(ref, target_object, target_object.root, schema) 44 | else 45 | @klass.new(ref, target_object, target_object.root, schema) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/hash_body_loader.rb: -------------------------------------------------------------------------------- 1 | # hash body object loader 2 | class OpenAPIParser::SchemaLoader::HashBodyLoader < OpenAPIParser::SchemaLoader::Creator 3 | # @param [String] variable_name 4 | # @param [Hash] options 5 | def initialize(variable_name, options) 6 | super(variable_name, options) 7 | 8 | @reject_keys = options[:reject_keys] ? options[:reject_keys].map(&:to_s) : [] 9 | end 10 | 11 | # @param [OpenAPIParser::Schemas::Base] target_object 12 | # @param [Hash] raw_schema 13 | # @return [Array, nil] 14 | def load_data(target_object, raw_schema) 15 | # raw schema always exist because if not exist' this object don't create 16 | create_hash_body_objects(target_object, raw_schema) 17 | end 18 | 19 | private 20 | 21 | # for responses and paths object 22 | def create_hash_body_objects(target_object, raw_schema) 23 | object_list = raw_schema.reject { |k, _| reject_keys.include?(k) }.map do |child_name, child_schema| 24 | ref = build_object_reference_from_base(target_object.object_reference, escape_reference(child_name)) 25 | [ 26 | child_name.to_s, # support string key only in OpenAPI3 27 | build_openapi_object_from_option(target_object, ref, child_schema), 28 | ] 29 | end 30 | 31 | objects = object_list.to_h 32 | variable_set(target_object, variable_name, objects) 33 | objects.values 34 | end 35 | 36 | attr_reader :reject_keys 37 | end 38 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/hash_objects_loader.rb: -------------------------------------------------------------------------------- 1 | # hash object loader 2 | class OpenAPIParser::SchemaLoader::HashObjectsLoader < OpenAPIParser::SchemaLoader::Creator 3 | # @param [OpenAPIParser::Schemas::Base] target_object 4 | # @param [Hash] raw_schema 5 | # @return [Array, nil] 6 | def load_data(target_object, raw_schema) 7 | create_attr_hash_object(target_object, raw_schema[ref_name_base.to_s]) 8 | end 9 | 10 | private 11 | 12 | def create_attr_hash_object(target_object, hash_schema) 13 | unless hash_schema 14 | variable_set(target_object, variable_name, nil) 15 | return 16 | end 17 | 18 | data_list = hash_schema.map do |key, s| 19 | ref = build_object_reference_from_base(target_object.object_reference, [ref_name_base, key]) 20 | [key, build_openapi_object_from_option(target_object, ref, s)] 21 | end 22 | 23 | data = data_list.to_h 24 | variable_set(target_object, variable_name, data) 25 | data.values 26 | end 27 | 28 | alias_method :ref_name_base, :schema_key 29 | end 30 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/list_loader.rb: -------------------------------------------------------------------------------- 1 | # list object loader 2 | class OpenAPIParser::SchemaLoader::ListLoader < OpenAPIParser::SchemaLoader::Creator 3 | # @param [OpenAPIParser::Schemas::Base] target_object 4 | # @param [Hash] raw_schema 5 | # @return [Array, nil] 6 | def load_data(target_object, raw_schema) 7 | create_attr_list_object(target_object, raw_schema[ref_name_base.to_s]) 8 | end 9 | 10 | private 11 | 12 | def create_attr_list_object(target_object, array_schema) 13 | unless array_schema 14 | variable_set(target_object, variable_name, nil) 15 | return 16 | end 17 | 18 | data = array_schema.map.with_index do |s, idx| 19 | ref = build_object_reference_from_base(target_object.object_reference, [ref_name_base, idx]) 20 | build_openapi_object_from_option(target_object, ref, s) 21 | end 22 | 23 | variable_set(target_object, variable_name, data) 24 | data 25 | end 26 | 27 | alias_method :ref_name_base, :schema_key 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/objects_loader.rb: -------------------------------------------------------------------------------- 1 | # Specific Object loader (defined by klass option) 2 | class OpenAPIParser::SchemaLoader::ObjectsLoader < OpenAPIParser::SchemaLoader::Creator 3 | # @param [OpenAPIParser::Schemas::Base] target_object 4 | # @param [Hash] raw_schema 5 | # @return [Array, nil] 6 | def load_data(target_object, raw_schema) 7 | obj = create_attr_object(target_object, raw_schema[schema_key.to_s]) 8 | [obj] 9 | end 10 | 11 | private 12 | 13 | # @return [OpenAPIParser::Schemas::Base] 14 | def create_attr_object(target_object, schema) 15 | ref = build_object_reference_from_base(target_object.object_reference, schema_key) 16 | 17 | data = build_openapi_object_from_option(target_object, ref, schema) 18 | variable_set(target_object, variable_name, data) 19 | data 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi_parser/concerns/schema_loader/values_loader.rb: -------------------------------------------------------------------------------- 1 | # data type values loader 2 | class OpenAPIParser::SchemaLoader::ValuesLoader < OpenAPIParser::SchemaLoader::Base 3 | # @param [OpenAPIParser::Schemas::Base] target_object 4 | # @param [Hash] raw_schema 5 | # @return [Array, nil] 6 | def load_data(target_object, raw_schema) 7 | variable_set(target_object, variable_name, raw_schema[schema_key.to_s]) 8 | nil # this loader not return schema object 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/openapi_parser/config.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::Config 2 | def initialize(config) 3 | # TODO: This deprecation warning can be removed after we set the default to `true` 4 | # in a later (major?) version update. 5 | unless config.key?(:strict_reference_validation) 6 | msg = "[DEPRECATION] strict_reference_validation config is not set. It defaults to `false` now, " + 7 | "but will be `true` in a future version. Please explicitly set to `false` " + 8 | "if you want to skip reference validation on schema load." 9 | warn(msg) 10 | end 11 | @config = config 12 | end 13 | 14 | def allow_empty_date_and_datetime 15 | @config.fetch(:allow_empty_date_and_datetime, false) 16 | end 17 | 18 | def datetime_coerce_class 19 | @config[:datetime_coerce_class] 20 | end 21 | 22 | def coerce_value 23 | @config[:coerce_value] 24 | end 25 | 26 | def expand_reference 27 | @config.fetch(:expand_reference, true) 28 | end 29 | 30 | def strict_response_validation 31 | # TODO: in a major version update, change this to default to `true`. 32 | # https://github.com/ota42y/openapi_parser/pull/123/files#r767142217 33 | @config.fetch(:strict_response_validation, false) 34 | end 35 | 36 | def strict_reference_validation 37 | @config.fetch(:strict_reference_validation, false) 38 | end 39 | 40 | def validate_header 41 | @config.fetch(:validate_header, true) 42 | end 43 | 44 | # @return [OpenAPIParser::SchemaValidator::Options] 45 | def request_validator_options 46 | @request_validator_options ||= OpenAPIParser::SchemaValidator::Options.new(allow_empty_date_and_datetime: allow_empty_date_and_datetime, 47 | coerce_value: coerce_value, 48 | datetime_coerce_class: datetime_coerce_class, 49 | validate_header: validate_header) 50 | end 51 | 52 | alias_method :request_body_options, :request_validator_options 53 | alias_method :path_params_options, :request_validator_options 54 | 55 | # @return [OpenAPIParser::SchemaValidator::ResponseValidateOptions] 56 | def response_validate_options 57 | @response_validate_options ||= OpenAPIParser::SchemaValidator::ResponseValidateOptions. 58 | new(strict: strict_response_validation, validate_header: validate_header) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/openapi_parser/parameter_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::ParameterValidator 2 | class << self 3 | # @param [Hash{String => Parameter}] parameters_hash 4 | # @param [Hash] params 5 | # @param [String] object_reference 6 | # @param [OpenAPIParser::SchemaValidator::Options] options 7 | # @param [Boolean] is_header is header or not (ignore params key case) 8 | def validate_parameter(parameters_hash, params, object_reference, options, is_header = false) 9 | no_exist_required_key = [] 10 | 11 | params_key_converted = params.keys.map { |k| [convert_key(k, is_header), k] }.to_h 12 | parameters_hash.each do |k, v| 13 | key = params_key_converted[convert_key(k, is_header)] 14 | if params.include?(key) 15 | coerced = v.validate_params(params[key], options) 16 | params[key] = coerced if options.coerce_value 17 | elsif v.required 18 | no_exist_required_key << k 19 | end 20 | end 21 | 22 | raise OpenAPIParser::NotExistRequiredKey.new(no_exist_required_key, object_reference) unless no_exist_required_key.empty? 23 | 24 | params 25 | end 26 | 27 | private 28 | 29 | def convert_key(k, is_header) 30 | is_header ? k&.downcase : k 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/openapi_parser/path_item_finder.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::PathItemFinder 2 | # @param [OpenAPIParser::Schemas::Paths] paths 3 | def initialize(paths) 4 | @paths = paths 5 | end 6 | 7 | # find operation object and if not exist return nil 8 | # @param [String, Symbol] http_method like (get, post .... allow symbol) 9 | # @param [String] request_path 10 | # @return [Result, nil] 11 | def operation_object(http_method, request_path) 12 | if (path_item_object = @paths.path[request_path]) 13 | if (op = path_item_object.operation(http_method)) 14 | return Result.new(path_item_object, op, request_path, {}) # find no path_params path 15 | end 16 | end 17 | 18 | # check with path_params 19 | parse_request_path(http_method, request_path) 20 | end 21 | 22 | class Result 23 | attr_reader :path_item_object, :operation_object, :original_path, :path_params 24 | # @!attribute [r] path_item_object 25 | # @return [OpenAPIParser::Schemas::PathItem] 26 | # @!attribute [r] operation_object 27 | # @return [OpenAPIParser::Schemas::Operation] 28 | # @!attribute [r] path_params 29 | # @return [Hash{String => String}] 30 | # @!attribute [r] original_path 31 | # @return [String] 32 | 33 | def initialize(path_item_object, operation_object, original_path, path_params) 34 | @path_item_object = path_item_object 35 | @operation_object = operation_object 36 | @original_path = original_path 37 | @path_params = path_params 38 | end 39 | end 40 | 41 | def parse_path_parameters(schema_path, request_path) 42 | parameters = path_parameters(schema_path) 43 | return nil if parameters.empty? 44 | 45 | # If there are regex special characters in the path, the regex will 46 | # be too permissive, so escape the non-parameter parts. 47 | components = [] 48 | unprocessed = schema_path.dup 49 | parameters.each do |parameter| 50 | parts = unprocessed.partition(parameter) 51 | components << Regexp.escape(parts[0]) unless parts[0] == '' 52 | components << "(?<#{param_name(parameter)}>.+)" 53 | unprocessed = parts[2] 54 | end 55 | components << Regexp.escape(unprocessed) unless unprocessed == '' 56 | 57 | regex = components.join('') 58 | matches = request_path.match(regex) 59 | return nil unless matches 60 | 61 | # Match up the captured names with the captured values as a hash 62 | matches.names.zip(matches.captures).to_h 63 | end 64 | 65 | private 66 | def path_parameters(schema_path) 67 | # OAS3 follows a RFC6570 subset for URL templates 68 | # https://swagger.io/docs/specification/serialization/#uri-templates 69 | # A URL template param can be preceded optionally by a "." or ";", and can be succeeded optionally by a "*"; 70 | # this regex returns a match of the full parameter name with all of these modifiers. Ex: {;id*} 71 | parameters = schema_path.scan(/(\{[\.;]*[^\{\*\}]+\**\})/) 72 | # The `String#scan` method returns an array of arrays; we want an array of strings 73 | parameters.collect { |param| param[0] } 74 | end 75 | 76 | # check if there is a identical path in the schema (without any param) 77 | def matches_directly?(request_path, http_method) 78 | @paths.path[request_path]&.operation(http_method) 79 | end 80 | 81 | # used to filter paths with different depth or without given http method 82 | def different_depth_or_method?(splitted_schema_path, splitted_request_path, path_item, http_method) 83 | splitted_schema_path.size != splitted_request_path.size || !path_item.operation(http_method) 84 | end 85 | 86 | # check if the path item is a template 87 | # EXAMPLE: path_template?('{id}') => true 88 | def path_template?(schema_path_item) 89 | schema_path_item.start_with?('{') && schema_path_item.end_with?('}') 90 | end 91 | 92 | # get the parameter name from the schema path item 93 | # EXAMPLE: param_name('{id}') => 'id' 94 | def param_name(schema_path_item) 95 | schema_path_item[1..(schema_path_item.length - 2)] 96 | end 97 | 98 | # extract params by comparing the request path and the path from schema 99 | # EXAMPLE: 100 | # extract_params(['org', '1', 'user', '2', 'edit'], ['org', '{org_id}', 'user', '{user_id}']) 101 | # => { 'org_id' => 1, 'user_id' => 2 } 102 | # return nil if the schema does not match 103 | def extract_params(splitted_request_path, splitted_schema_path) 104 | splitted_request_path.zip(splitted_schema_path).reduce({}) do |result, zip_item| 105 | request_path_item, schema_path_item = zip_item 106 | 107 | params = parse_path_parameters(schema_path_item, request_path_item) 108 | if params 109 | result.merge!(params) 110 | else 111 | return if schema_path_item != request_path_item 112 | end 113 | 114 | result 115 | end 116 | end 117 | 118 | # find all matching paths with parameters extracted 119 | # EXAMPLE: 120 | # [ 121 | # ['/user/{id}/edit', { 'id' => 1 }], 122 | # ['/user/{id}/{action}', { 'id' => 1, 'action' => 'edit' }], 123 | # ] 124 | def matching_paths_with_params(request_path, http_method) 125 | splitted_request_path = request_path.split('/') 126 | 127 | @paths.path.reduce([]) do |result, item| 128 | path, path_item = item 129 | splitted_schema_path = path.split('/') 130 | 131 | next result if different_depth_or_method?(splitted_schema_path, splitted_request_path, path_item, http_method) 132 | 133 | extracted_params = extract_params(splitted_request_path, splitted_schema_path) 134 | result << [path, extracted_params] if extracted_params 135 | result 136 | end 137 | end 138 | 139 | # find matching path and extract params 140 | # EXAMPLE: find_path_and_params('get', '/user/1') => ['/user/{id}', { 'id' => 1 }] 141 | def find_path_and_params(http_method, request_path) 142 | return [request_path, {}] if matches_directly?(request_path, http_method) 143 | 144 | matching = matching_paths_with_params(request_path, http_method) 145 | 146 | # if there are many matching paths, return the one with the smallest number of params 147 | # (prefer /user/{id}/action over /user/{param_1}/{param_2} ) 148 | matching.min_by { |match| match[1].size } 149 | end 150 | 151 | def parse_request_path(http_method, request_path) 152 | original_path, path_params = find_path_and_params(http_method, request_path) 153 | return nil unless original_path # # can't find 154 | 155 | path_item_object = @paths.path[original_path] 156 | obj = path_item_object.operation(http_method.to_s) 157 | return nil unless obj 158 | 159 | Result.new(path_item_object, obj, original_path, path_params) 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/openapi_parser/reference_expander.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::ReferenceExpander 2 | class << self 3 | # @param [OpenAPIParser::Schemas::OpenAPI] openapi 4 | def expand(openapi, validate_references) 5 | openapi.expand_reference(openapi, validate_references) 6 | openapi.purge_object_cache 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/openapi_parser/request_operation.rb: -------------------------------------------------------------------------------- 1 | # binding request data and operation object 2 | 3 | class OpenAPIParser::RequestOperation 4 | class << self 5 | # @param [OpenAPIParser::Config] config 6 | # @param [OpenAPIParser::PathItemFinder] path_item_finder 7 | # @return [OpenAPIParser::RequestOperation, nil] 8 | def create(http_method, request_path, path_item_finder, config) 9 | result = path_item_finder.operation_object(http_method, request_path) 10 | return nil unless result 11 | 12 | self.new(http_method, result, config) 13 | end 14 | end 15 | 16 | # @!attribute [r] operation_object 17 | # @return [OpenAPIParser::Schemas::Operation] 18 | # @!attribute [r] path_params 19 | # @return [Hash{String => String}] 20 | # @!attribute [r] config 21 | # @return [OpenAPIParser::Config] 22 | # @!attribute [r] http_method 23 | # @return [String] 24 | # @!attribute [r] original_path 25 | # @return [String] 26 | # @!attribute [r] path_item 27 | # @return [OpenAPIParser::Schemas::PathItem] 28 | attr_reader :operation_object, :path_params, :config, :http_method, :original_path, :path_item 29 | 30 | # @param [String] http_method 31 | # @param [OpenAPIParser::PathItemFinder::Result] result 32 | # @param [OpenAPIParser::Config] config 33 | def initialize(http_method, result, config) 34 | @http_method = http_method.to_s 35 | @original_path = result.original_path 36 | @operation_object = result.operation_object 37 | @path_params = result.path_params || {} 38 | @path_item = result.path_item_object 39 | @config = config 40 | end 41 | 42 | def validate_path_params(options = nil) 43 | options ||= config.path_params_options 44 | operation_object&.validate_path_params(path_params, options) 45 | end 46 | 47 | # @param [String] content_type 48 | # @param [Hash] params 49 | # @param [OpenAPIParser::SchemaValidator::Options] options 50 | def validate_request_body(content_type, params, options = nil) 51 | options ||= config.request_body_options 52 | operation_object&.validate_request_body(content_type, params, options) 53 | end 54 | 55 | # @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] response_body 56 | # @param [OpenAPIParser::SchemaValidator::ResponseValidateOptions] response_validate_options 57 | def validate_response_body(response_body, response_validate_options = nil) 58 | response_validate_options ||= config.response_validate_options 59 | 60 | operation_object&.validate_response(response_body, response_validate_options) 61 | end 62 | 63 | # @param [Hash] params parameter hash 64 | # @param [Hash] headers headers hash 65 | # @param [OpenAPIParser::SchemaValidator::Options] options request validator options 66 | def validate_request_parameter(params, headers, options = nil) 67 | options ||= config.request_validator_options 68 | operation_object&.validate_request_parameter(params, headers, options) 69 | end 70 | 71 | class ValidatableResponseBody 72 | attr_reader :status_code, :response_data, :headers 73 | 74 | def initialize(status_code, response_data, headers) 75 | @status_code = status_code 76 | @response_data = response_data 77 | @headers = headers 78 | end 79 | 80 | def content_type 81 | content_type_key = headers.keys.detect { |k| k.casecmp?('Content-Type') } 82 | headers[content_type_key].to_s.split(';').first.to_s 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator.rb: -------------------------------------------------------------------------------- 1 | require_relative 'schema_validator/options' 2 | require_relative 'schema_validator/enumable' 3 | require_relative 'schema_validator/minimum_maximum' 4 | require_relative 'schema_validator/properties_number' 5 | require_relative 'schema_validator/base' 6 | require_relative 'schema_validator/string_validator' 7 | require_relative 'schema_validator/integer_validator' 8 | require_relative 'schema_validator/float_validator' 9 | require_relative 'schema_validator/boolean_validator' 10 | require_relative 'schema_validator/object_validator' 11 | require_relative 'schema_validator/array_validator' 12 | require_relative 'schema_validator/any_of_validator' 13 | require_relative 'schema_validator/all_of_validator' 14 | require_relative 'schema_validator/one_of_validator' 15 | require_relative 'schema_validator/nil_validator' 16 | require_relative 'schema_validator/unspecified_type_validator' 17 | 18 | class OpenAPIParser::SchemaValidator 19 | # validate value by schema 20 | # this module for SchemaValidators::Base 21 | # @param [Object] value 22 | # @param [OpenAPIParser::Schemas::Schema] schema 23 | module Validatable 24 | def validate_schema(value, schema, **keyword_args) 25 | raise 'implement' 26 | end 27 | 28 | # validate integer value by schema 29 | # this method use from float_validator because number allow float and integer 30 | # @param [Object] _value 31 | # @param [OpenAPIParser::Schemas::Schema] _schema 32 | def validate_integer(_value, _schema) 33 | raise 'implement' 34 | end 35 | end 36 | 37 | include Validatable 38 | 39 | class << self 40 | # validate schema data 41 | # @param [Hash] value 42 | # @param [OpenAPIParser::Schemas:v:Schema] 43 | # @param [OpenAPIParser::SchemaValidator::Options] options 44 | # @return [Object] coerced or original params 45 | def validate(value, schema, options) 46 | new(value, schema, options).validate_data 47 | end 48 | end 49 | 50 | # @param [Hash] value 51 | # @param [OpenAPIParser::Schemas::Schema] schema 52 | # @param [OpenAPIParser::SchemaValidator::Options] options 53 | def initialize(value, schema, options) 54 | @value = value 55 | @schema = schema 56 | @allow_empty_date_and_datetime = options.allow_empty_date_and_datetime 57 | @coerce_value = options.coerce_value 58 | @datetime_coerce_class = options.datetime_coerce_class 59 | end 60 | 61 | # execute validate data 62 | # @return [Object] coerced or original params 63 | def validate_data 64 | coerced, err = validate_schema(@value, @schema) 65 | raise err if err 66 | 67 | coerced 68 | end 69 | 70 | # validate value eby schema 71 | # @param [Object] value 72 | # @param [OpenAPIParser::Schemas::Schema] schema 73 | def validate_schema(value, schema, **keyword_args) 74 | return [value, nil] unless schema 75 | 76 | if (v = validator(value, schema)) 77 | if keyword_args.empty? 78 | return v.coerce_and_validate(value, schema) 79 | else 80 | return v.coerce_and_validate(value, schema, **keyword_args) 81 | end 82 | end 83 | 84 | # unknown return error 85 | OpenAPIParser::ValidateError.build_error_result(value, schema) 86 | end 87 | 88 | # validate integer value by schema 89 | # this method use from float_validator because number allow float and integer 90 | # @param [Object] value 91 | # @param [OpenAPIParser::Schemas::Schema] schema 92 | def validate_integer(value, schema) 93 | integer_validator.coerce_and_validate(value, schema) 94 | end 95 | 96 | private 97 | 98 | # @return [OpenAPIParser::SchemaValidator::Base, nil] 99 | def validator(value, schema) 100 | return any_of_validator if schema.any_of 101 | return all_of_validator if schema.all_of 102 | return one_of_validator if schema.one_of 103 | return nil_validator if value.nil? 104 | 105 | case schema.type 106 | when 'string' 107 | string_validator 108 | when 'integer' 109 | integer_validator 110 | when 'boolean' 111 | boolean_validator 112 | when 'number' 113 | float_validator 114 | when 'object' 115 | object_validator 116 | when 'array' 117 | array_validator 118 | else 119 | unspecified_type_validator 120 | end 121 | end 122 | 123 | def string_validator 124 | @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @allow_empty_date_and_datetime, @coerce_value, @datetime_coerce_class) 125 | end 126 | 127 | def integer_validator 128 | @integer_validator ||= OpenAPIParser::SchemaValidator::IntegerValidator.new(self, @coerce_value) 129 | end 130 | 131 | def float_validator 132 | @float_validator ||= OpenAPIParser::SchemaValidator::FloatValidator.new(self, @coerce_value) 133 | end 134 | 135 | def boolean_validator 136 | @boolean_validator ||= OpenAPIParser::SchemaValidator::BooleanValidator.new(self, @coerce_value) 137 | end 138 | 139 | def object_validator 140 | @object_validator ||= OpenAPIParser::SchemaValidator::ObjectValidator.new(self, @coerce_value) 141 | end 142 | 143 | def array_validator 144 | @array_validator ||= OpenAPIParser::SchemaValidator::ArrayValidator.new(self, @coerce_value) 145 | end 146 | 147 | def any_of_validator 148 | @any_of_validator ||= OpenAPIParser::SchemaValidator::AnyOfValidator.new(self, @coerce_value) 149 | end 150 | 151 | def all_of_validator 152 | @all_of_validator ||= OpenAPIParser::SchemaValidator::AllOfValidator.new(self, @coerce_value) 153 | end 154 | 155 | def one_of_validator 156 | @one_of_validator ||= OpenAPIParser::SchemaValidator::OneOfValidator.new(self, @coerce_value) 157 | end 158 | 159 | def nil_validator 160 | @nil_validator ||= OpenAPIParser::SchemaValidator::NilValidator.new(self, @coerce_value) 161 | end 162 | 163 | def unspecified_type_validator 164 | @unspecified_type_validator ||= OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator.new(self, @coerce_value) 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/all_of_validator.rb: -------------------------------------------------------------------------------- 1 | # validate AllOf schema 2 | class OpenAPIParser::SchemaValidator 3 | class AllOfValidator < Base 4 | # coerce and validate value 5 | # @param [Object] value 6 | # @param [OpenAPIParser::Schemas::Schema] schema 7 | def coerce_and_validate(value, schema, **keyword_args) 8 | if value.nil? && schema.nullable 9 | return [value, nil] 10 | end 11 | 12 | # if any schema return error, it's not valida all of value 13 | remaining_keys = value.kind_of?(Hash) ? value.keys : [] 14 | nested_additional_properties = false 15 | schema.all_of.each do |s| 16 | # We need to store the reference to all of, so we can perform strict check on allowed properties 17 | _coerced, err = validatable.validate_schema( 18 | value, 19 | s, 20 | :parent_all_of => true, 21 | parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas] || [] 22 | ) 23 | 24 | if s.type == "object" 25 | remaining_keys -= (s.properties || {}).keys 26 | nested_additional_properties = true if s.additional_properties 27 | else 28 | # If this is not allOf having array of objects inside, but e.g. having another anyOf/oneOf nested 29 | remaining_keys.clear 30 | end 31 | 32 | return [nil, err] if err 33 | end 34 | 35 | # If there are nested additionalProperites, we allow not defined extra properties and lean on the specific 36 | # additionalProperties validation 37 | if !nested_additional_properties && !remaining_keys.empty? 38 | return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)] 39 | end 40 | 41 | [value, nil] 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/any_of_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class AnyOfValidator < Base 3 | # @param [Object] value 4 | # @param [OpenAPIParser::Schemas::Schema] schema 5 | def coerce_and_validate(value, schema, **_keyword_args) 6 | if value.nil? && schema.nullable 7 | return [value, nil] 8 | end 9 | if schema.discriminator 10 | return validate_discriminator_schema(schema.discriminator, value) 11 | end 12 | 13 | # in all schema return error (=true) not any of data 14 | schema.any_of.each do |s| 15 | coerced, err = validatable.validate_schema(value, s) 16 | return [coerced, nil] if err.nil? 17 | end 18 | [nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference)] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/array_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class ArrayValidator < Base 3 | # @param [Array] value 4 | # @param [OpenAPIParser::Schemas::Schema] schema 5 | def coerce_and_validate(value, schema, **_keyword_args) 6 | return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Array) 7 | 8 | value, err = validate_max_min_items(value, schema) 9 | return [nil, err] if err 10 | 11 | value, err = validate_unique_items(value, schema) 12 | return [nil, err] if err 13 | 14 | # array type have an schema in items property 15 | items_schema = schema.items 16 | coerced_values = value.map do |v| 17 | coerced, err = validatable.validate_schema(v, items_schema) 18 | return [nil, err] if err 19 | 20 | coerced 21 | end 22 | 23 | value.each_index { |idx| value[idx] = coerced_values[idx] } if @coerce_value 24 | 25 | [value, nil] 26 | end 27 | 28 | def validate_max_min_items(value, schema) 29 | return [nil, OpenAPIParser::MoreThanMaxItems.new(value, schema.object_reference)] if schema.maxItems && value.length > schema.maxItems 30 | return [nil, OpenAPIParser::LessThanMinItems.new(value, schema.object_reference)] if schema.minItems && value.length < schema.minItems 31 | 32 | [value, nil] 33 | end 34 | 35 | def validate_unique_items(value, schema) 36 | return [nil, OpenAPIParser::NotUniqueItems.new(value, schema.object_reference)] if schema.uniqueItems && value.length != value.uniq.length 37 | 38 | [value, nil] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/base.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class Base 3 | def initialize(validatable, coerce_value) 4 | @validatable = validatable 5 | @coerce_value = coerce_value 6 | end 7 | 8 | attr_reader :validatable 9 | 10 | # need override 11 | def coerce_and_validate(_value, _schema, **_keyword_args) 12 | raise 'need implement' 13 | end 14 | 15 | def validate_discriminator_schema(discriminator, value, parent_discriminator_schemas: []) 16 | property_name = discriminator.property_name 17 | if property_name.nil? || !value.key?(property_name) 18 | return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)] 19 | end 20 | mapping_key = value[property_name] 21 | 22 | # it's allowed to have discriminator without mapping, then we need to lookup discriminator.property_name 23 | # but the format is not the full path, just model name in the components 24 | mapping_target = discriminator.mapping&.[](mapping_key) || "#/components/schemas/#{mapping_key}" 25 | 26 | # Find object does O(n) search at worst, then caches the result, so this is ok for repeated search 27 | resolved_schema = discriminator.root.find_object(mapping_target) 28 | 29 | unless resolved_schema 30 | return [nil, OpenAPIParser::NotExistDiscriminatorMappedSchema.new(mapping_target, discriminator.object_reference)] 31 | end 32 | validatable.validate_schema( 33 | value, 34 | resolved_schema, 35 | **{discriminator_property_name: discriminator.property_name, parent_discriminator_schemas: parent_discriminator_schemas} 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/boolean_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class BooleanValidator < Base 3 | include ::OpenAPIParser::SchemaValidator::Enumable 4 | 5 | TRUE_VALUES = ['true', '1'].freeze 6 | FALSE_VALUES = ['false', '0'].freeze 7 | 8 | def coerce_and_validate(value, schema, **_keyword_args) 9 | value = coerce(value) if @coerce_value 10 | 11 | return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(TrueClass) || value.kind_of?(FalseClass) 12 | 13 | value, err = check_enum_include(value, schema) 14 | return [nil, err] if err 15 | 16 | [value, nil] 17 | end 18 | 19 | private 20 | 21 | def coerce(value) 22 | return true if TRUE_VALUES.include?(value) 23 | 24 | return false if FALSE_VALUES.include?(value) 25 | 26 | value 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/enumable.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | module Enumable 3 | # check enum value by schema 4 | # @param [Object] value 5 | # @param [OpenAPIParser::Schemas::Schema] schema 6 | def check_enum_include(value, schema) 7 | return [value, nil] unless schema.enum 8 | return [value, nil] if schema.enum.include?(value) 9 | 10 | [nil, OpenAPIParser::NotEnumInclude.new(value, schema.object_reference)] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/float_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class FloatValidator < Base 3 | include ::OpenAPIParser::SchemaValidator::Enumable 4 | include ::OpenAPIParser::SchemaValidator::MinimumMaximum 5 | 6 | # validate float value by schema 7 | # @param [Object] value 8 | # @param [OpenAPIParser::Schemas::Schema] schema 9 | def coerce_and_validate(value, schema, **_keyword_args) 10 | value = coerce(value) if @coerce_value 11 | 12 | return validatable.validate_integer(value, schema) if value.kind_of?(Integer) 13 | 14 | coercer_and_validate_numeric(value, schema) 15 | end 16 | 17 | private 18 | 19 | def coercer_and_validate_numeric(value, schema) 20 | return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Numeric) 21 | 22 | value, err = check_enum_include(value, schema) 23 | return [nil, err] if err 24 | 25 | check_minimum_maximum(value, schema) 26 | end 27 | 28 | def coerce(value) 29 | Float(value) 30 | rescue ArgumentError, TypeError 31 | value 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/integer_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class IntegerValidator < Base 3 | include ::OpenAPIParser::SchemaValidator::Enumable 4 | include ::OpenAPIParser::SchemaValidator::MinimumMaximum 5 | 6 | # validate integer value by schema 7 | # @param [Object] value 8 | # @param [OpenAPIParser::Schemas::Schema] schema 9 | def coerce_and_validate(value, schema, **_keyword_args) 10 | value = coerce(value) if @coerce_value 11 | 12 | return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Integer) 13 | 14 | value, err = check_enum_include(value, schema) 15 | return [nil, err] if err 16 | 17 | check_minimum_maximum(value, schema) 18 | end 19 | 20 | private 21 | 22 | def coerce(value) 23 | return value if value.kind_of?(Integer) 24 | 25 | begin 26 | Integer(value) 27 | rescue ArgumentError, TypeError 28 | value 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/minimum_maximum.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | module MinimumMaximum 3 | # check minimum and maximum value by schema 4 | # @param [Object] value 5 | # @param [OpenAPIParser::Schemas::Schema] schema 6 | def check_minimum_maximum(value, schema) 7 | include_min_max = schema.minimum || schema.maximum 8 | return [value, nil] unless include_min_max 9 | 10 | validate(value, schema) 11 | [value, nil] 12 | rescue OpenAPIParser::OpenAPIError => e 13 | return [nil, e] 14 | end 15 | 16 | private 17 | 18 | def validate(value, schema) 19 | reference = schema.object_reference 20 | 21 | if schema.minimum 22 | if schema.exclusiveMinimum && value <= schema.minimum 23 | raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference) 24 | elsif value < schema.minimum 25 | raise OpenAPIParser::LessThanMinimum.new(value, reference) 26 | end 27 | end 28 | 29 | if schema.maximum 30 | if schema.exclusiveMaximum && value >= schema.maximum 31 | raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference) 32 | elsif value > schema.maximum 33 | raise OpenAPIParser::MoreThanMaximum.new(value, reference) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/nil_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class NilValidator < Base 3 | # @param [Object] value 4 | # @param [OpenAPIParser::Schemas::Schema] schema 5 | def coerce_and_validate(value, schema, **_keyword_args) 6 | return [value, nil] if schema.nullable 7 | 8 | [nil, OpenAPIParser::NotNullError.new(schema.object_reference)] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/object_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class ObjectValidator < Base 3 | include ::OpenAPIParser::SchemaValidator::PropertiesNumber 4 | 5 | # @param [Hash] value 6 | # @param [OpenAPIParser::Schemas::Schema] schema 7 | # @param [Boolean] parent_all_of true if component is nested under allOf 8 | # @param [String, nil] discriminator_property_name discriminator.property_name to ignore checking additional_properties 9 | def coerce_and_validate(value, schema, parent_all_of: false, parent_discriminator_schemas: [], discriminator_property_name: nil) 10 | return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Hash) 11 | 12 | properties = schema.properties || {} 13 | 14 | required_set = schema.required ? schema.required.to_set : Set.new 15 | remaining_keys = value.keys 16 | 17 | if schema.discriminator && !parent_discriminator_schemas.include?(schema) 18 | return validate_discriminator_schema( 19 | schema.discriminator, 20 | value, 21 | parent_discriminator_schemas: parent_discriminator_schemas + [schema] 22 | ) 23 | else 24 | remaining_keys.delete('discriminator') 25 | end 26 | 27 | coerced_values = value.map do |name, v| 28 | s = properties[name] 29 | coerced, err = if s 30 | remaining_keys.delete(name) 31 | validatable.validate_schema(v, s) 32 | else 33 | # TODO: we need to perform a validation based on schema.additional_properties here, if 34 | # additionalProperties are defined 35 | [v, nil] 36 | end 37 | 38 | return [nil, err] if err 39 | 40 | required_set.delete(name) 41 | [name, coerced] 42 | end 43 | 44 | remaining_keys.delete(discriminator_property_name) if discriminator_property_name 45 | 46 | if !remaining_keys.empty? && !parent_all_of && !schema.additional_properties 47 | # If object is nested in all of, the validation is already done in allOf validator. Or if 48 | # additionalProperties are defined, we will validate using that 49 | return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)] 50 | end 51 | return [nil, OpenAPIParser::NotExistRequiredKey.new(required_set.to_a, schema.object_reference)] unless required_set.empty? 52 | 53 | value.merge!(coerced_values.to_h) if @coerce_value 54 | 55 | check_properties_number(value, schema) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/one_of_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class OneOfValidator < Base 3 | # @param [Object] value 4 | # @param [OpenAPIParser::Schemas::Schema] schema 5 | def coerce_and_validate(value, schema, **_keyword_args) 6 | if value.nil? && schema.nullable 7 | return [value, nil] 8 | end 9 | if schema.discriminator 10 | return validate_discriminator_schema(schema.discriminator, value) 11 | end 12 | 13 | # if multiple schemas are satisfied, it's not valid 14 | result = schema.one_of.one? do |s| 15 | _coerced, err = validatable.validate_schema(value, s) 16 | err.nil? 17 | end 18 | if result 19 | [value, nil] 20 | else 21 | [nil, OpenAPIParser::NotOneOf.new(value, schema.object_reference)] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/options.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class Options 3 | # @!attribute [r] allow_empty_date_and_datetime 4 | # @return [Boolean] allow empty date and datetime values option on/off 5 | # @!attribute [r] coerce_value 6 | # @return [Boolean] coerce value option on/off 7 | # @!attribute [r] datetime_coerce_class 8 | # @return [Object, nil] coerce datetime string by this Object class 9 | # @!attribute [r] validate_header 10 | # @return [Boolean] validate header or not 11 | attr_reader :allow_empty_date_and_datetime, :coerce_value, :datetime_coerce_class, :validate_header 12 | 13 | def initialize(allow_empty_date_and_datetime: false, coerce_value: nil, datetime_coerce_class: nil, validate_header: true) 14 | @allow_empty_date_and_datetime = allow_empty_date_and_datetime 15 | @coerce_value = coerce_value 16 | @datetime_coerce_class = datetime_coerce_class 17 | @validate_header = validate_header 18 | end 19 | end 20 | 21 | # response body validation option 22 | class ResponseValidateOptions 23 | # @!attribute [r] strict 24 | # @return [Boolean] validate by strict (when not exist definition, raise error) 25 | attr_reader :strict, :validate_header, :validator_options 26 | 27 | def initialize(strict: false, validate_header: true, **validator_options) 28 | @validator_options = validator_options 29 | @strict = strict 30 | @validate_header = validate_header 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/properties_number.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | module PropertiesNumber 3 | # check minProperties and manProperties value by schema 4 | # @param [Object] value 5 | # @param [OpenAPIParser::Schemas::Schema] schema 6 | def check_properties_number(value, schema) 7 | include_properties_num = schema.minProperties || schema.maxProperties 8 | return [value, nil] unless include_properties_num 9 | 10 | validate(value, schema) 11 | [value, nil] 12 | rescue OpenAPIParser::OpenAPIError => e 13 | return [nil, e] 14 | end 15 | 16 | private 17 | 18 | def validate(value, schema) 19 | reference = schema.object_reference 20 | 21 | if schema.minProperties && (value.size < schema.minProperties) 22 | raise OpenAPIParser::LessThanMinProperties.new(value, reference) 23 | end 24 | 25 | if schema.maxProperties && (value.size > schema.maxProperties) 26 | raise OpenAPIParser::MoreThanMaxProperties.new(value, reference) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/string_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class StringValidator < Base 3 | include ::OpenAPIParser::SchemaValidator::Enumable 4 | 5 | def initialize(validator, allow_empty_date_and_datetime, coerce_value, datetime_coerce_class) 6 | super(validator, coerce_value) 7 | @allow_empty_date_and_datetime = allow_empty_date_and_datetime 8 | @datetime_coerce_class = datetime_coerce_class 9 | end 10 | 11 | def coerce_and_validate(value, schema, **_keyword_args) 12 | unless value.kind_of?(String) 13 | # Skip validation if the format is `binary`, even if the value is not an actual string. 14 | # ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types 15 | if schema.format == 'binary' 16 | # TODO: 17 | # It would be better to check whether the value is an instance of `Rack::Multipart::UploadFile`, 18 | # `ActionDispatch::Http::UploadedFile`, or another similar class. 19 | return [value, nil] 20 | end 21 | 22 | return OpenAPIParser::ValidateError.build_error_result(value, schema) 23 | end 24 | 25 | value, err = check_enum_include(value, schema) 26 | return [nil, err] if err 27 | 28 | value, err = pattern_validate(value, schema) 29 | return [nil, err] if err 30 | 31 | value, err = validate_max_min_length(value, schema) 32 | return [nil, err] if err 33 | 34 | value, err = validate_email_format(value, schema) 35 | return [nil, err] if err 36 | 37 | value, err = validate_uuid_format(value, schema) 38 | return [nil, err] if err 39 | 40 | value, err = validate_date_format(value, schema) 41 | return [nil, err] if err 42 | 43 | value, err = validate_datetime_format(value, schema) 44 | return [nil, err] if err 45 | 46 | [value, nil] 47 | end 48 | 49 | private 50 | 51 | # @param [OpenAPIParser::Schemas::Schema] schema 52 | def pattern_validate(value, schema) 53 | # pattern support string only so put this 54 | return [value, nil] unless schema.pattern 55 | return [value, nil] if value =~ /#{schema.pattern}/ 56 | 57 | [nil, OpenAPIParser::InvalidPattern.new(value, schema.pattern, schema.object_reference, schema.example)] 58 | end 59 | 60 | def validate_max_min_length(value, schema) 61 | return [nil, OpenAPIParser::MoreThanMaxLength.new(value, schema.object_reference)] if schema.maxLength && value.size > schema.maxLength 62 | return [nil, OpenAPIParser::LessThanMinLength.new(value, schema.object_reference)] if schema.minLength && value.size < schema.minLength 63 | 64 | [value, nil] 65 | end 66 | 67 | def validate_email_format(value, schema) 68 | return [value, nil] unless schema.format == 'email' 69 | 70 | return [value, nil] if value.match?(URI::MailTo::EMAIL_REGEXP) 71 | 72 | return [nil, OpenAPIParser::InvalidEmailFormat.new(value, schema.object_reference)] 73 | end 74 | 75 | def validate_uuid_format(value, schema) 76 | return [value, nil] unless schema.format == 'uuid' 77 | 78 | return [value, nil] if value.match(/^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$/) 79 | 80 | return [nil, OpenAPIParser::InvalidUUIDFormat.new(value, schema.object_reference)] 81 | end 82 | 83 | def validate_date_format(value, schema) 84 | if @allow_empty_date_and_datetime && value.to_s.empty? 85 | return [value, nil] if schema.format == 'date' 86 | end 87 | 88 | return [value, nil] unless schema.format == 'date' 89 | 90 | return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] unless value =~ /^\d{4}-\d{2}-\d{2}$/ 91 | 92 | begin 93 | parsed_date = Date.iso8601(value) 94 | rescue ArgumentError 95 | return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] 96 | end 97 | 98 | unless parsed_date.strftime('%Y-%m-%d') == value 99 | return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] 100 | end 101 | 102 | return [value, nil] 103 | end 104 | 105 | def validate_datetime_format(value, schema) 106 | if @allow_empty_date_and_datetime && value.to_s.empty? 107 | return [value, nil] if schema.format == 'date-time' 108 | end 109 | 110 | return [value, nil] unless schema.format == 'date-time' 111 | 112 | begin 113 | if @datetime_coerce_class.nil? 114 | # validate only 115 | DateTime.rfc3339(value) 116 | [value, nil] 117 | else 118 | # validate and coerce 119 | if @datetime_coerce_class == Time 120 | [DateTime.rfc3339(value).to_time, nil] 121 | else 122 | [@datetime_coerce_class.rfc3339(value), nil] 123 | end 124 | end 125 | rescue ArgumentError 126 | # when rfc3339(value) failed 127 | [nil, OpenAPIParser::InvalidDateTimeFormat.new(value, schema.object_reference)] 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/openapi_parser/schema_validator/unspecified_type_validator.rb: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::SchemaValidator 2 | class UnspecifiedTypeValidator < Base 3 | # @param [Object] value 4 | def coerce_and_validate(value, _schema, **_keyword_args) 5 | [value, nil] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas.rb: -------------------------------------------------------------------------------- 1 | require_relative 'schemas/classes' 2 | 3 | require_relative 'schemas/base' 4 | require_relative 'schemas/discriminator' 5 | require_relative 'schemas/openapi' 6 | require_relative 'schemas/paths' 7 | require_relative 'schemas/path_item' 8 | require_relative 'schemas/operation' 9 | require_relative 'schemas/parameter' 10 | require_relative 'schemas/reference' 11 | require_relative 'schemas/request_body' 12 | require_relative 'schemas/response' 13 | require_relative 'schemas/responses' 14 | require_relative 'schemas/components' 15 | require_relative 'schemas/media_type' 16 | require_relative 'schemas/schema' 17 | require_relative 'schemas/header' 18 | require_relative 'schemas/info' 19 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/base.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Base 3 | include OpenAPIParser::Parser 4 | include OpenAPIParser::Findable 5 | include OpenAPIParser::Expandable 6 | 7 | attr_reader :parent, :raw_schema, :object_reference, :root 8 | 9 | # @param [OpenAPIParser::Schemas::Base] 10 | def initialize(object_reference, parent, root, raw_schema) 11 | @raw_schema = raw_schema 12 | @parent = parent 13 | @root = root 14 | @object_reference = object_reference 15 | 16 | load_data 17 | after_init 18 | end 19 | 20 | # override 21 | def after_init 22 | end 23 | 24 | def inspect 25 | @object_reference 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/classes.rb: -------------------------------------------------------------------------------- 1 | # We want to use class name for DSL in class definition so we should define first... 2 | 3 | module OpenAPIParser::Schemas 4 | class Base; end 5 | class Discriminator < Base; end 6 | class OpenAPI < Base; end 7 | class Operation < Base; end 8 | class Parameter < Base; end 9 | class PathItem < Base; end 10 | class Paths < Base; end 11 | class Reference < Base; end 12 | class RequestBody < Base; end 13 | class Responses < Base; end 14 | class Response < Base; end 15 | class MediaType < Base; end 16 | class Schema < Base; end 17 | class Components < Base; end 18 | class Header < Base; end 19 | class Info < Base; end 20 | end 21 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/components.rb: -------------------------------------------------------------------------------- 1 | # TODO: examples 2 | # TODO: securitySchemes 3 | # TODO: links 4 | # TODO: callbacks 5 | 6 | module OpenAPIParser::Schemas 7 | class Components < Base 8 | # @!attribute [r] parameters 9 | # @return [Hash{String => Parameter}, nil] 10 | openapi_attr_hash_object :parameters, Parameter, reference: true 11 | 12 | # @!attribute [r] parameters 13 | # @return [Hash{String => Parameter}, nil] 14 | openapi_attr_hash_object :schemas, Schema, reference: true 15 | 16 | # @!attribute [r] responses 17 | # @return [Hash{String => Response}, nil] 18 | openapi_attr_hash_object :responses, Response, reference: true 19 | 20 | # @!attribute [r] request_bodies 21 | # @return [Hash{String => RequestBody}, nil] 22 | openapi_attr_hash_object :request_bodies, RequestBody, reference: true, schema_key: :requestBodies 23 | 24 | # @!attribute [r] headers 25 | # @return [Hash{String => Header}, nil] header objects 26 | openapi_attr_hash_object :headers, Header, reference: true 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/discriminator.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Discriminator < Base 3 | # @!attribute [r] property_name 4 | # @return [String, nil] 5 | openapi_attr_value :property_name, schema_key: :propertyName 6 | 7 | # @!attribute [r] mapping 8 | # @return [Hash{String => String] 9 | openapi_attr_value :mapping 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/header.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Header < Base 3 | openapi_attr_values :description, :required, :deprecated, :style, :explode, :example 4 | 5 | openapi_attr_value :allow_empty_value, schema_key: :allowEmptyValue 6 | openapi_attr_value :allow_reserved, schema_key: :allowReserved 7 | 8 | # @!attribute [r] schema 9 | # @return [Schema, Reference, nil] 10 | openapi_attr_object :schema, Schema, reference: true 11 | 12 | # validate by schema 13 | # @param [Object] value 14 | def validate(value) 15 | OpenAPIParser::SchemaValidator.validate(value, schema, OpenAPIParser::SchemaValidator::Options.new) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/info.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Info < Base 3 | 4 | openapi_attr_values :title, :version 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/media_type.rb: -------------------------------------------------------------------------------- 1 | # TODO: example 2 | # TODO: examples 3 | # TODO: encoding 4 | 5 | module OpenAPIParser::Schemas 6 | class MediaType < Base 7 | # @!attribute [r] schema 8 | # @return [Schema, nil] OpenAPI3 Schema object 9 | openapi_attr_object :schema, Schema, reference: true 10 | 11 | # validate params by schema definitions 12 | # @param [Hash] params 13 | # @param [OpenAPIParser::SchemaValidator::Options] options 14 | def validate_parameter(params, options) 15 | OpenAPIParser::SchemaValidator.validate(params, schema, options) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/openapi.rb: -------------------------------------------------------------------------------- 1 | # TODO: info object 2 | # TODO: servers object 3 | # TODO: tags object 4 | # TODO: externalDocs object 5 | 6 | module OpenAPIParser::Schemas 7 | class OpenAPI < Base 8 | def initialize(raw_schema, config, uri: nil, schema_registry: {}) 9 | super('#', nil, self, raw_schema) 10 | @find_object_cache = {} 11 | @path_item_finder = OpenAPIParser::PathItemFinder.new(paths) if paths # invalid definition 12 | @config = config 13 | @uri = uri 14 | @schema_registry = schema_registry 15 | 16 | # schema_registery is shared among schemas, and prevents a schema from being loaded multiple times 17 | schema_registry[uri] = self if uri 18 | end 19 | 20 | # @!attribute [r] openapi 21 | # @return [String, nil] 22 | openapi_attr_values :openapi 23 | 24 | # @!attribute [r] paths 25 | # @return [Paths, nil] 26 | openapi_attr_object :paths, Paths, reference: false 27 | 28 | # @!attribute [r] components 29 | # @return [Components, nil] 30 | openapi_attr_object :components, Components, reference: false 31 | 32 | # @!attribute [r] info 33 | # @return [Info, nil] 34 | openapi_attr_object :info, Info, reference: false 35 | 36 | # @return [OpenAPIParser::RequestOperation, nil] 37 | def request_operation(http_method, request_path) 38 | OpenAPIParser::RequestOperation.create(http_method, request_path, @path_item_finder, @config) 39 | end 40 | 41 | # load another schema with shared config and schema_registry 42 | # @return [OpenAPIParser::Schemas::OpenAPI] 43 | def load_another_schema(uri) 44 | resolved_uri = resolve_uri(uri) 45 | return if resolved_uri.nil? 46 | 47 | loaded = @schema_registry[resolved_uri] 48 | return loaded if loaded 49 | 50 | OpenAPIParser.load_uri(resolved_uri, config: @config, schema_registry: @schema_registry) 51 | end 52 | 53 | private 54 | 55 | def resolve_uri(uri) 56 | if uri.absolute? 57 | uri 58 | else 59 | @uri&.merge(uri) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/operation.rb: -------------------------------------------------------------------------------- 1 | # TODO: externalDocs 2 | # TODO: callbacks 3 | # TODO: security 4 | # TODO: servers 5 | 6 | module OpenAPIParser::Schemas 7 | class Operation < Base 8 | include OpenAPIParser::ParameterValidatable 9 | 10 | openapi_attr_values :tags, :summary, :description, :deprecated 11 | 12 | openapi_attr_value :operation_id, schema_key: :operationId 13 | 14 | openapi_attr_list_object :parameters, Parameter, reference: true 15 | 16 | # @!attribute [r] request_body 17 | # @return [OpenAPIParser::Schemas::RequestBody, nil] return OpenAPI3 object 18 | openapi_attr_object :request_body, RequestBody, reference: true, schema_key: :requestBody 19 | 20 | # @!attribute [r] responses 21 | # @return [OpenAPIParser::Schemas::Responses, nil] return OpenAPI3 object 22 | openapi_attr_object :responses, Responses, reference: false 23 | 24 | def validate_request_body(content_type, params, options) 25 | request_body&.validate_request_body(content_type, params, options) 26 | end 27 | 28 | # @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] response_body 29 | # @param [OpenAPIParser::SchemaValidator::ResponseValidateOptions] response_validate_options 30 | def validate_response(response_body, response_validate_options) 31 | responses&.validate(response_body, response_validate_options) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/parameter.rb: -------------------------------------------------------------------------------- 1 | # TODO: support examples 2 | 3 | module OpenAPIParser::Schemas 4 | class Parameter < Base 5 | openapi_attr_values :name, :in, :description, :required, :deprecated, :style, :explode, :example 6 | 7 | openapi_attr_value :allow_empty_value, schema_key: :allowEmptyValue 8 | openapi_attr_value :allow_reserved, schema_key: :allowReserved 9 | 10 | # @!attribute [r] schema 11 | # @return [Schema, Reference, nil] 12 | openapi_attr_object :schema, Schema, reference: true 13 | 14 | # @return [Object] coerced or original params 15 | # @param [OpenAPIParser::SchemaValidator::Options] options 16 | def validate_params(params, options) 17 | ::OpenAPIParser::SchemaValidator.validate(params, schema, options) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/path_item.rb: -------------------------------------------------------------------------------- 1 | # TODO: support servers 2 | # TODO: support reference 3 | 4 | module OpenAPIParser::Schemas 5 | class PathItem < Base 6 | openapi_attr_values :summary, :description 7 | 8 | openapi_attr_objects :get, :put, :post, :delete, :options, :head, :patch, :trace, Operation 9 | openapi_attr_list_object :parameters, Parameter, reference: true 10 | 11 | # @return [Operation] 12 | def operation(method) 13 | public_send(method) 14 | rescue NoMethodError 15 | nil 16 | end 17 | 18 | def set_path_item_to_operation 19 | [:get, :put, :post, :delete, :options, :head, :patch, :trace].each{ |method| operation(method)&.set_parent_path_item(self)} 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/paths.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Paths < Base 3 | # @!attribute [r] path 4 | # @return [Hash{String => PathItem, Reference}, nil] 5 | openapi_attr_hash_body_objects 'path', PathItem, reference: true, allow_data_type: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/reference.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser::Schemas 2 | class Reference < Base 3 | # @!attribute [r] ref 4 | # @return [Base] 5 | openapi_attr_value :ref, schema_key: '$ref' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/request_body.rb: -------------------------------------------------------------------------------- 1 | # TODO: support extended property 2 | 3 | module OpenAPIParser::Schemas 4 | class RequestBody < Base 5 | include OpenAPIParser::MediaTypeSelectable 6 | 7 | # @!attribute [r] description 8 | # @return [String] description data 9 | # @!attribute [r] required 10 | # @return [Boolean] required bool data 11 | openapi_attr_values :description, :required 12 | 13 | # @!attribute [r] content 14 | # @return [Hash{String => MediaType}, nil] content type to MediaType object 15 | openapi_attr_hash_object :content, MediaType, reference: false 16 | 17 | # @param [String] content_type 18 | # @param [Hash] params 19 | # @param [OpenAPIParser::SchemaValidator::Options] options 20 | def validate_request_body(content_type, params, options) 21 | media_type = select_media_type(content_type) 22 | return params unless media_type 23 | 24 | media_type.validate_parameter(params, options) 25 | end 26 | 27 | # select media type by content_type (consider wild card definition) 28 | # @param [String] content_type 29 | # @return [OpenAPIParser::Schemas::MediaType, nil] 30 | def select_media_type(content_type) 31 | select_media_type_from_content(content_type, content) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/response.rb: -------------------------------------------------------------------------------- 1 | # TODO: links 2 | 3 | module OpenAPIParser::Schemas 4 | class Response < Base 5 | include OpenAPIParser::MediaTypeSelectable 6 | 7 | openapi_attr_values :description 8 | 9 | # @!attribute [r] content 10 | # @return [Hash{String => MediaType}, nil] content_type to MediaType hash 11 | openapi_attr_hash_object :content, MediaType, reference: false 12 | 13 | # @!attribute [r] headers 14 | # @return [Hash{String => Header}, nil] header string to Header 15 | openapi_attr_hash_object :headers, Header, reference: true 16 | 17 | # @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] response_body 18 | # @param [OpenAPIParser::SchemaValidator::ResponseValidateOptions] response_validate_options 19 | def validate(response_body, response_validate_options) 20 | validate_header(response_body.headers) if response_validate_options.validate_header 21 | 22 | media_type = select_media_type(response_body.content_type) 23 | unless media_type 24 | if response_validate_options.strict && response_body_not_blank(response_body) 25 | raise ::OpenAPIParser::NotExistContentTypeDefinition, object_reference 26 | end 27 | 28 | return true 29 | end 30 | 31 | options = ::OpenAPIParser::SchemaValidator::Options.new(**response_validate_options.validator_options) 32 | media_type.validate_parameter(response_body.response_data, options) 33 | end 34 | 35 | # select media type by content_type (consider wild card definition) 36 | # @param [String] content_type 37 | # @return [OpenAPIParser::Schemas::MediaType, nil] 38 | def select_media_type(content_type) 39 | select_media_type_from_content(content_type, content) 40 | end 41 | 42 | private 43 | 44 | # @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] 45 | def response_body_not_blank(response_body) 46 | !(response_body.response_data.nil? || response_body.response_data.empty?) 47 | end 48 | 49 | # @param [Hash] response_headers 50 | def validate_header(response_headers) 51 | return unless headers 52 | 53 | headers.each do |name, schema| 54 | next unless response_headers.key?(name) 55 | 56 | value = response_headers[name] 57 | schema.validate(value) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/responses.rb: -------------------------------------------------------------------------------- 1 | # TODO: support extended property 2 | 3 | module OpenAPIParser::Schemas 4 | class Responses < Base 5 | # @!attribute [r] default 6 | # @return [Response, Reference, nil] default response object 7 | openapi_attr_object :default, Response, reference: true 8 | 9 | # @!attribute [r] response 10 | # @return [Hash{String => Response, Reference}, nil] response object indexed by status code. see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#patterned-fields-1 11 | openapi_attr_hash_body_objects 'response', Response, reject_keys: [:default], reference: true, allow_data_type: false 12 | 13 | # validate params data by definition 14 | # find response object by status_code and content_type 15 | # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#patterned-fields-1 16 | # @param [OpenAPIParser::RequestOperation::ValidatableResponseBody] response_body 17 | # @param [OpenAPIParser::SchemaValidator::ResponseValidateOptions] response_validate_options 18 | def validate(response_body, response_validate_options) 19 | return nil unless response 20 | 21 | if (res = find_response_object(response_body.status_code)) 22 | 23 | return res.validate(response_body, response_validate_options) 24 | end 25 | 26 | raise ::OpenAPIParser::NotExistStatusCodeDefinition, object_reference if response_validate_options.strict 27 | 28 | nil 29 | end 30 | 31 | private 32 | 33 | # @param [Integer] status_code 34 | # @return [Response] 35 | def find_response_object(status_code) 36 | if (res = response[status_code.to_s]) 37 | return res 38 | end 39 | 40 | wild_card = status_code_to_wild_card(status_code) 41 | if (res = response[wild_card]) 42 | return res 43 | end 44 | 45 | default 46 | end 47 | 48 | # parse 400 -> 4xx 49 | # OpenAPI3 allow 1xx, 2xx, 3xx... only, don't allow 41x 50 | # @param [Integer] status_code 51 | def status_code_to_wild_card(status_code) 52 | top = status_code / 100 53 | "#{top}XX" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/openapi_parser/schemas/schema.rb: -------------------------------------------------------------------------------- 1 | # TODO: support 'not' because I need check reference... 2 | # TODO: support 'xml', 'externalDocs' 3 | # TODO: support extended property 4 | 5 | module OpenAPIParser::Schemas 6 | class Schema < Base 7 | # @!attribute [r] title 8 | # @return [String, nil] 9 | # @!attribute [r] pattern 10 | # @return [String, nil] regexp 11 | # @!attribute [r] pattern 12 | # @return [String, nil] regexp 13 | # @!attribute [r] description 14 | # @return [String, nil] 15 | # @!attribute [r] format 16 | # @return [String, nil] 17 | # @!attribute [r] type 18 | # @return [String, nil] multiple types doesn't supported in OpenAPI3 19 | 20 | # @!attribute [r] maximum 21 | # @return [Float, nil] 22 | # @!attribute [r] multipleOf 23 | # @return [Float, nil] 24 | 25 | # @!attribute [r] maxLength 26 | # @return [Integer, nil] 27 | # @!attribute [r] minLength 28 | # @return [Integer, nil] 29 | # @!attribute [r] maxItems 30 | # @return [Integer, nil] 31 | # @!attribute [r] minItems 32 | # @return [Integer, nil] 33 | # @!attribute [r] maxProperties 34 | # @return [Integer, nil] 35 | # @!attribute [r] minProperties 36 | # @return [Integer, nil] 37 | 38 | # @!attribute [r] exclusiveMaximum 39 | # @return [Boolean, nil] 40 | # @!attribute [r] exclusiveMinimum 41 | # @return [Boolean, nil] 42 | # @!attribute [r] uniqueItems 43 | # @return [Boolean, nil] 44 | # @!attribute [r] nullable 45 | # @return [Boolean, nil] 46 | # @!attribute [r] deprecated 47 | # @return [Boolean, nil] 48 | 49 | # @!attribute [r] required 50 | # @return [Array, nil] at least one item included 51 | 52 | # @!attribute [r] enum 53 | # @return [Array, nil] any type array 54 | 55 | # @!attribute [r] default 56 | # @return [Object, nil] 57 | 58 | # @!attribute [r] example 59 | # @return [Object, nil] 60 | 61 | openapi_attr_values :title, :multipleOf, 62 | :maximum, :exclusiveMaximum, :minimum, :exclusiveMinimum, 63 | :maxLength, :minLength, 64 | :pattern, 65 | :maxItems, :minItems, :uniqueItems, 66 | :maxProperties, :minProperties, 67 | :required, :enum, 68 | :description, 69 | :format, 70 | :default, 71 | :type, 72 | :nullable, 73 | :example, 74 | :deprecated 75 | 76 | # @!attribute [r] read_only 77 | # @return [Boolean, nil] 78 | openapi_attr_value :read_only, schema_key: :readOnly 79 | 80 | # @!attribute [r] write_only 81 | # @return [Boolean, nil] 82 | openapi_attr_value :write_only, schema_key: :writeOnly 83 | 84 | # @!attribute [r] all_of 85 | # @return [Array, nil] 86 | openapi_attr_list_object :all_of, Schema, reference: true, schema_key: :allOf 87 | 88 | # @!attribute [r] one_of 89 | # @return [Array, nil] 90 | openapi_attr_list_object :one_of, Schema, reference: true, schema_key: :oneOf 91 | 92 | # @!attribute [r] any_of 93 | # @return [Array, nil] 94 | openapi_attr_list_object :any_of, Schema, reference: true, schema_key: :anyOf 95 | 96 | # @!attribute [r] items 97 | # @return [Schema, nil] 98 | openapi_attr_object :items, Schema, reference: true 99 | 100 | # @!attribute [r] properties 101 | # @return [Hash{String => Schema}, nil] 102 | openapi_attr_hash_object :properties, Schema, reference: true 103 | 104 | # @!attribute [r] discriminator 105 | # @return [Discriminator, nil] 106 | openapi_attr_object :discriminator, Discriminator 107 | 108 | # @!attribute [r] additional_properties 109 | # @return [Boolean, Schema, Reference, nil] 110 | openapi_attr_object :additional_properties, Schema, reference: true, allow_data_type: true, schema_key: :additionalProperties 111 | # additional_properties have default value 112 | # we should add default value feature in openapi_attr_object method, but we need temporary fix so override attr_reader 113 | remove_method :additional_properties 114 | def additional_properties 115 | @additional_properties.nil? ? true : @additional_properties 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/openapi_parser/version.rb: -------------------------------------------------------------------------------- 1 | module OpenAPIParser 2 | VERSION = '2.2.6'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /openapi_parser.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'openapi_parser/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'openapi_parser' 7 | spec.version = OpenAPIParser::VERSION 8 | spec.authors = ['ota42y'] 9 | spec.email = ['ota42y@gmail.com'] 10 | 11 | spec.summary = 'OpenAPI3 parser' 12 | spec.description = 'parser for OpenAPI 3.0 or later' 13 | spec.homepage = 'https://github.com/ota42y/openapi_parser' 14 | spec.license = 'MIT' 15 | spec.required_ruby_version = ">= 2.7.0" 16 | 17 | # Specify which files should be added to the gem when it is released. 18 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 19 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 20 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | end 22 | spec.bindir = 'exe' 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ['lib'] 25 | 26 | spec.add_development_dependency 'bundler', '>= 1.16' 27 | spec.add_development_dependency 'fincop' 28 | 29 | if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("3.2.0") 30 | spec.add_development_dependency 'pry', '~> 0.12.0' 31 | spec.add_development_dependency 'pry-byebug' 32 | end 33 | spec.add_development_dependency 'rake', '>= 12.3.3' 34 | spec.add_development_dependency 'rspec', '~> 3.0' 35 | spec.add_development_dependency 'rspec-parameterized' 36 | spec.add_development_dependency 'simplecov' 37 | spec.add_development_dependency "steep" 38 | # for steep + ruby-head 39 | spec.add_development_dependency 'base64', '~> 0.3.0' 40 | end 41 | -------------------------------------------------------------------------------- /sig/openapi_parser.rbs: -------------------------------------------------------------------------------- 1 | module OpenAPIParser 2 | def self.parse: (Hash[bot, bot] schema, ?Hash[bot, bot] config) -> OpenAPIParser::Schemas::OpenAPI 3 | def self.parse_with_filepath: (Hash[bot, bot] schema, String filepath, ?Hash[bot, bot] config) -> OpenAPIParser::Schemas::OpenAPI 4 | def self.load: (String filepath, ?Hash[bot, bot] config) -> OpenAPIParser::Schemas::OpenAPI 5 | def self.load_uri: (OpenAPIParser::readable_uri uri, config: untyped, schema_registry: Hash[bot, bot]) -> OpenAPIParser::Schemas::OpenAPI 6 | def self.file_uri: (String filepath) -> URI::Generic 7 | def self.parse_file: (String? content, String ext) -> Hash[bot, bot] 8 | def self.parse_yaml: (String? content) -> Hash[bot, bot] 9 | def self.parse_json: (String? content) -> Hash[bot, bot] 10 | def self.load_hash: (Hash[bot, bot] hash, config: untyped, uri: OpenAPIParser::readable_uri?, schema_registry: Hash[bot, bot]) -> OpenAPIParser::Schemas::OpenAPI 11 | end 12 | 13 | module OpenAPIParser 14 | module Schemas 15 | class OpenAPI 16 | def initialize: (Hash[bot, bot] hash, untyped config, uri: OpenAPIParser::readable_uri?, schema_registry: Hash[bot, bot]) -> OpenAPIParser::Schemas::OpenAPI 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /sig/openapi_parser/config.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module OpenAPIParser 3 | class Config 4 | @config: untyped 5 | @request_validator_options: OpenAPIParser::SchemaValidator::Options 6 | @response_validate_options: OpenAPIParser::SchemaValidator::ResponseValidateOptions 7 | alias request_body_options request_validator_options 8 | alias path_params_options request_validator_options 9 | 10 | def initialize: (untyped config) -> untyped 11 | def allow_empty_date_and_datetime: -> bool 12 | def datetime_coerce_class: -> (singleton(Object) | nil) 13 | def coerce_value: -> bool 14 | def expand_reference: -> bool 15 | def strict_response_validation: -> bool 16 | def strict_reference_validation: -> bool 17 | def validate_header: -> bool 18 | def request_validator_options: -> OpenAPIParser::SchemaValidator::Options 19 | def response_validate_options: -> OpenAPIParser::SchemaValidator::ResponseValidateOptions 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /sig/openapi_parser/errors.rbs: -------------------------------------------------------------------------------- 1 | module OpenAPIParser 2 | class OpenAPIError < StandardError 3 | def initialize: (untyped reference) -> untyped 4 | end 5 | 6 | class ValidateError < OpenAPIError 7 | def initialize: (untyped data, (String | nil) type, untyped reference) -> untyped 8 | def message: -> String 9 | 10 | def self.build_error_result: (Object value, OpenAPIParser::Schemas::Schema schema) -> [nil, OpenAPIParser::ValidateError] 11 | end 12 | 13 | class NotExistDiscriminatorMappedSchema < OpenAPIError 14 | def initialize: (untyped mapped_schema_reference, untyped reference) -> untyped 15 | def message: -> String 16 | end 17 | 18 | class NotExistDiscriminatorPropertyName < OpenAPIError 19 | def initialize: (untyped mapped_schema_reference, untyped value, untyped reference) -> untyped 20 | def message: -> String 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /sig/openapi_parser/reference_expander.rbs: -------------------------------------------------------------------------------- 1 | class OpenAPIParser::ReferenceExpander 2 | def self.expand: (OpenAPIParser::Schemas::OpenAPI openapi, untyped validate_references) -> nil 3 | end -------------------------------------------------------------------------------- /sig/openapi_parser/schema_validator.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module OpenAPIParser 3 | class SchemaValidator 4 | include Validatable 5 | @value: Hash[bot, bot] 6 | @schema: OpenAPIParser::Schemas::Schema 7 | @coerce_value: bool | nil 8 | @datetime_coerce_class: singleton(Object) | nil 9 | @string_validator: OpenAPIParser::SchemaValidator::StringValidator | nil 10 | @integer_validator: OpenAPIParser::SchemaValidator::IntegerValidator | nil 11 | @float_validator: OpenAPIParser::SchemaValidator::FloatValidator | nil 12 | @boolean_validator: OpenAPIParser::SchemaValidator::BooleanValidator | nil 13 | @object_validator: OpenAPIParser::SchemaValidator::ObjectValidator | nil 14 | @array_validator: OpenAPIParser::SchemaValidator::ArrayValidator | nil 15 | @any_of_validator: OpenAPIParser::SchemaValidator::AnyOfValidator | nil 16 | @all_of_validator: OpenAPIParser::SchemaValidator::AllOfValidator | nil 17 | @one_of_validator: OpenAPIParser::SchemaValidator::OneOfValidator | nil 18 | @nil_validator: OpenAPIParser::SchemaValidator::NilValidator | nil 19 | @unspecified_type_validator: OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator | nil 20 | 21 | def self.validate: (Hash[bot, bot] value, OpenAPIParser::Schemas::Schema schema, OpenAPIParser::SchemaValidator::Options options) -> Object 22 | def initialize: (Hash[bot, bot] value, OpenAPIParser::Schemas::Schema schema, OpenAPIParser::SchemaValidator::Options options) -> untyped 23 | def validate_data: -> Object 24 | def validate_schema: (Object value, OpenAPIParser::Schemas::Schema schema, **bot) -> [Object, OpenAPIParser::validate_error] 25 | def validate_integer: (Object value, OpenAPIParser::Schemas::Schema schema) -> [Object, OpenAPIParser::validate_error] 26 | 27 | private 28 | def validator: (Object value, OpenAPIParser::Schemas::Schema schema) -> [OpenAPIParser::SchemaValidator::Base, OpenAPIParser::validate_error] 29 | def string_validator: -> OpenAPIParser::SchemaValidator::StringValidator 30 | def integer_validator: -> OpenAPIParser::SchemaValidator::IntegerValidator 31 | def float_validator: -> OpenAPIParser::SchemaValidator::FloatValidator 32 | def boolean_validator: -> OpenAPIParser::SchemaValidator::BooleanValidator 33 | def object_validator: -> OpenAPIParser::SchemaValidator::ObjectValidator 34 | def array_validator: -> OpenAPIParser::SchemaValidator::ArrayValidator 35 | def any_of_validator: -> OpenAPIParser::SchemaValidator::AnyOfValidator 36 | def all_of_validator: -> OpenAPIParser::SchemaValidator::AllOfValidator 37 | def one_of_validator: -> OpenAPIParser::SchemaValidator::OneOfValidator 38 | def nil_validator: -> OpenAPIParser::SchemaValidator::NilValidator 39 | def unspecified_type_validator: -> OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator 40 | 41 | module Validatable 42 | def validate_schema: (Object value, OpenAPIParser::Schemas::Schema schema, **untyped) -> [Object, OpenAPIParser::validate_error] 43 | def validate_integer: (Object _value, OpenAPIParser::Schemas::Schema _schema) -> [Object, OpenAPIParser::validate_error] 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /sig/openapi_parser/schema_validators/base.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module OpenAPIParser 3 | class SchemaValidator 4 | class Base 5 | @coerce_value: bool | nil 6 | 7 | attr_reader validatable: OpenAPIParser::SchemaValidator::Validatable 8 | 9 | def initialize: (OpenAPIParser::SchemaValidator::Validatable validatable, (bool | nil) coerce_value) -> untyped 10 | def coerce_and_validate: (Object _value, OpenAPIParser::Schemas::Schema _schema, **untyped) -> [untyped, (ValidateError | NotExistDiscriminatorMappedSchema | nil)] 11 | def validate_discriminator_schema: ( 12 | OpenAPIParser::Schemas::Discriminator discriminator, 13 | Hash[String, bot] value, 14 | ?parent_discriminator_schemas: Array[OpenAPIParser::Schemas::Schema] 15 | ) -> [Object | nil, OpenAPIParser::OpenAPIError] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /sig/openapi_parser/schema_validators/options.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module OpenAPIParser 3 | class SchemaValidator 4 | class Options 5 | attr_reader allow_empty_date_and_datetime: bool | nil 6 | attr_reader coerce_value: bool | nil 7 | attr_reader datetime_coerce_class: singleton(Object) | nil 8 | attr_reader validate_header: bool 9 | def initialize: (?allow_empty_date_and_datetime: bool | nil, ?coerce_value: bool | nil, ?datetime_coerce_class: singleton(Object) | nil, ?validate_header: bool) -> untyped 10 | end 11 | 12 | class ResponseValidateOptions 13 | attr_reader strict: bool 14 | attr_reader validate_header: bool 15 | def initialize: (?strict: bool, ?validate_header: bool) -> untyped 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /sig/openapi_parser/schemas/base.rbs: -------------------------------------------------------------------------------- 1 | # Classes 2 | module OpenAPIParser 3 | module Schemas 4 | class Base 5 | include OpenAPIParser::Expandable 6 | include OpenAPIParser::Findable 7 | 8 | attr_reader parent: OpenAPIParser::Schemas::Base | nil 9 | attr_reader raw_schema: Hash[String, bot] 10 | attr_reader object_reference: String 11 | attr_reader root: OpenAPIParser::Schemas::OpenAPI 12 | def initialize: (String object_reference, OpenAPIParser::Schemas::Base | nil parent, OpenAPIParser::Schemas::OpenAPI root, Hash[String, bot] raw_schema) -> nil 13 | def after_init: -> nil 14 | def inspect: -> String 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /sig/openapi_parser/version.rbs: -------------------------------------------------------------------------------- 1 | module OpenAPIParser 2 | VERSION: String 3 | end 4 | -------------------------------------------------------------------------------- /sig/types.rbs: -------------------------------------------------------------------------------- 1 | module OpenURI 2 | module OpenRead 3 | def open: () -> (IO | nil) 4 | def path: () -> String 5 | def scheme: () -> String 6 | end 7 | end 8 | 9 | 10 | module Psych 11 | def self.safe_load: (untyped content, untyped parmitted_classes) -> Hash[bot, bot] 12 | end 13 | 14 | -------------------------------------------------------------------------------- /sig/wip_types.rbs: -------------------------------------------------------------------------------- 1 | module OpenAPIParser 2 | type readable_uri = URI::Generic | OpenURI::OpenRead 3 | type validate_error = nil 4 | end 5 | 6 | module OpenAPIParser 7 | module Schemas 8 | class Schema 9 | end 10 | end 11 | end 12 | 13 | class OpenAPIParser::SchemaValidator::Base 14 | end 15 | 16 | class OpenAPIParser::SchemaValidator::StringValidator 17 | end 18 | 19 | class OpenAPIParser::SchemaValidator::IntegerValidator 20 | end 21 | 22 | class OpenAPIParser::SchemaValidator::FloatValidator 23 | end 24 | 25 | class OpenAPIParser::SchemaValidator::BooleanValidator 26 | end 27 | 28 | class OpenAPIParser::SchemaValidator::ObjectValidator 29 | end 30 | 31 | class OpenAPIParser::SchemaValidator::ArrayValidator 32 | end 33 | 34 | class OpenAPIParser::SchemaValidator::AnyOfValidator 35 | end 36 | 37 | class OpenAPIParser::SchemaValidator::AllOfValidator 38 | end 39 | 40 | class OpenAPIParser::SchemaValidator::OneOfValidator 41 | end 42 | 43 | class OpenAPIParser::SchemaValidator::NilValidator 44 | end 45 | 46 | class OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator 47 | end 48 | 49 | class OpenAPIParser::Schemas::OpenAPI < OpenAPIParser::Schemas::Base 50 | attr_reader paths: untyped 51 | end 52 | 53 | module OpenAPIParser::Expandable 54 | def expand_reference: (OpenAPIParser::Schemas::OpenAPI root) -> nil 55 | end 56 | 57 | module OpenAPIParser::Findable 58 | def find_object: (String reference) -> ::OpenAPIParser::Schemas::Schema 59 | end 60 | 61 | class OpenAPIParser::Schemas::Discriminator < OpenAPIParser::Schemas::Base 62 | attr_reader property_name: (String | nil) 63 | attr_reader mapping: Hash[String, String] 64 | end 65 | -------------------------------------------------------------------------------- /spec/data/cyclic-remote-ref1.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /cyclic_reference: 7 | post: 8 | requestBody: 9 | required: true 10 | content: 11 | application/json: 12 | schema: 13 | $ref: cyclic-remote-ref2.yaml#/components/schemas/outer 14 | responses: 15 | '200': 16 | description: correct 17 | content: 18 | application/json: 19 | schema: 20 | type: object 21 | components: 22 | schemas: 23 | inner: 24 | type: integer 25 | -------------------------------------------------------------------------------- /spec/data/cyclic-remote-ref2.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | components: 6 | schemas: 7 | outer: 8 | type: object 9 | properties: 10 | content: 11 | $ref: cyclic-remote-ref1.yaml#/components/schemas/inner 12 | required: 13 | - content 14 | -------------------------------------------------------------------------------- /spec/data/path-item-ref-relative.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /ref-sample/relative: 7 | post: 8 | description: override here 9 | requestBody: 10 | content: 11 | application/json: 12 | schema: 13 | type: object 14 | properties: 15 | test: 16 | type: string 17 | required: 18 | - test 19 | responses: 20 | "204": 21 | description: empty 22 | -------------------------------------------------------------------------------- /spec/data/path-item-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /sample/{sample_id}: 7 | parameters: 8 | - name: sample_id 9 | in: path 10 | required: true 11 | schema: 12 | type: string 13 | post: 14 | description: override here 15 | requestBody: 16 | content: 17 | application/json: 18 | schema: 19 | type: object 20 | properties: 21 | test: 22 | type: string 23 | required: 24 | - test 25 | responses: 26 | '204': 27 | description: empty 28 | /ref-sample: 29 | $ref: '#/paths/~1sample~1%7Bsample_id%7D' 30 | /ref-sample/relative: 31 | $ref: 'path-item-ref-relative.yaml#/paths/~1ref-sample~1relative' 32 | -------------------------------------------------------------------------------- /spec/data/petstore-with-discriminator.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: apiteam@swagger.io 10 | url: http://swagger.io 11 | license: 12 | name: Apache 2.0 13 | url: https://www.apache.org/licenses/LICENSE-2.0.html 14 | servers: 15 | - url: http://petstore.swagger.io/api 16 | paths: 17 | /save_the_pets: 18 | post: 19 | description: Creates a new pet in the store. Duplicates are allowed 20 | operationId: addPet 21 | requestBody: 22 | description: Pet to add to the store 23 | required: true 24 | content: 25 | application/json: 26 | schema: 27 | $ref: '#/components/schemas/PetBaskets' 28 | responses: 29 | '200': 30 | description: pet response 31 | content: 32 | application/json: 33 | schema: 34 | type: object 35 | /save_the_pets_without_mapping: 36 | post: 37 | description: Creates a new pet in the store. Duplicates are allowed 38 | operationId: addPet 39 | requestBody: 40 | description: Pet to add to the store 41 | required: true 42 | content: 43 | application/json: 44 | schema: 45 | $ref: '#/components/schemas/PetBasketsWithoutMapping' 46 | responses: 47 | '200': 48 | description: pet response 49 | content: 50 | application/json: 51 | schema: 52 | type: object 53 | components: 54 | schemas: 55 | PetBaskets: 56 | type: object 57 | properties: 58 | baskets: 59 | type: array 60 | items: 61 | anyOf: 62 | - "$ref": "#/components/schemas/SquirrelBasket" 63 | - "$ref": "#/components/schemas/CatBasket" 64 | - "$ref": "#/components/schemas/TurtleBasket" 65 | - "$ref": "#/components/schemas/Dragon" 66 | - "$ref": "#/components/schemas/Hydra" 67 | discriminator: 68 | propertyName: name 69 | mapping: 70 | cats: "#/components/schemas/CatBasket" 71 | squirrels: "#/components/schemas/SquirrelBasket" 72 | turtles: "#/components/schemas/TurtleBasket" 73 | dragon: "#/components/schemas/Dragon" 74 | hydra: "#/components/schemas/Hydra" 75 | PetBasketsWithoutMapping: 76 | type: object 77 | properties: 78 | baskets: 79 | type: array 80 | items: 81 | anyOf: 82 | - "$ref": "#/components/schemas/SquirrelBasket" 83 | - "$ref": "#/components/schemas/CatBasket" 84 | discriminator: 85 | propertyName: name 86 | SquirrelBasket: 87 | type: object 88 | required: 89 | - name 90 | properties: 91 | name: 92 | type: string 93 | content: 94 | type: array 95 | items: 96 | "$ref": "#/components/schemas/Squirrel" 97 | additionalProperties: false 98 | Squirrel: 99 | type: object 100 | required: 101 | - name 102 | - nut_stock 103 | properties: 104 | name: 105 | type: string 106 | born_at: 107 | format: date-time 108 | nullable: true 109 | type: string 110 | description: 111 | nullable: true 112 | type: string 113 | nut_stock: 114 | nullable: true 115 | type: integer 116 | additionalProperties: true 117 | CatBasket: 118 | type: object 119 | required: 120 | - name 121 | properties: 122 | name: 123 | type: string 124 | content: 125 | type: array 126 | items: 127 | "$ref": "#/components/schemas/Cat" 128 | additionalProperties: false 129 | Cat: 130 | type: object 131 | required: 132 | - name 133 | - milk_stock 134 | properties: 135 | name: 136 | type: string 137 | born_at: 138 | format: date-time 139 | nullable: true 140 | type: string 141 | description: 142 | nullable: true 143 | type: string 144 | milk_stock: 145 | nullable: true 146 | type: integer 147 | additionalProperties: false 148 | TurtleBasket: 149 | type: object 150 | required: 151 | - name 152 | properties: 153 | name: 154 | type: string 155 | content: 156 | type: array 157 | items: 158 | "$ref": "#/components/schemas/NinjaTurtle" 159 | additionalProperties: false 160 | NinjaTurtle: 161 | type: object 162 | required: 163 | - name 164 | properties: 165 | name: 166 | type: string 167 | born_at: 168 | format: date-time 169 | nullable: true 170 | type: string 171 | description: 172 | nullable: true 173 | type: string 174 | required_combat_style: 175 | allOf: 176 | - anyOf: 177 | - "$ref": "#/components/schemas/DonatelloStyle" 178 | - "$ref": "#/components/schemas/MichelangeloStyle" 179 | - nullable: false 180 | type: object 181 | optional_combat_style: 182 | anyOf: 183 | - "$ref": "#/components/schemas/DonatelloStyle" 184 | - "$ref": "#/components/schemas/MichelangeloStyle" 185 | DonatelloStyle: 186 | type: object 187 | nullable: true 188 | properties: 189 | bo_color: 190 | type: string 191 | shuriken_count: 192 | type: integer 193 | additionalProperties: false 194 | MichelangeloStyle: 195 | type: object 196 | nullable: true 197 | properties: 198 | nunchaku_color: 199 | type: string 200 | grappling_hook_length: 201 | type: number 202 | format: double 203 | additionalProperties: false 204 | Dragon: 205 | allOf: 206 | - $ref: '#/components/schemas/DragonBody' 207 | - type: object 208 | required: 209 | - fire_range 210 | properties: 211 | fire_range: 212 | type: integer 213 | format: int64 214 | additionalProperties: false 215 | Hydra: 216 | allOf: 217 | - $ref: '#/components/schemas/DragonBody' 218 | - type: object 219 | required: 220 | - head_count 221 | properties: 222 | head_count: 223 | type: integer 224 | format: int64 225 | additionalProperties: 226 | type: string 227 | DragonBody: 228 | type: object 229 | required: 230 | - name 231 | properties: 232 | name: 233 | type: string 234 | mass: 235 | type: integer 236 | additionalProperties: false 237 | -------------------------------------------------------------------------------- /spec/data/petstore-with-mapped-polymorphism.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | description: This is a sample server Petstore server. 4 | version: 1.0.5 5 | title: Swagger Petstore 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | email: apiteam@swagger.io 9 | license: 10 | name: Apache 2.0 11 | url: http://www.apache.org/licenses/LICENSE-2.0.html 12 | tags: 13 | - name: pet 14 | description: Everything about your Pets 15 | externalDocs: 16 | description: Find out more 17 | url: http://swagger.io 18 | - name: store 19 | description: Access to Petstore orders 20 | - name: user 21 | description: Operations about user 22 | externalDocs: 23 | description: Find out more about our store 24 | url: http://swagger.io 25 | paths: 26 | "/pet": 27 | post: 28 | tags: 29 | - pet 30 | summary: Add a new pet to the store 31 | description: '' 32 | operationId: addPet 33 | requestBody: 34 | description: Pet object that needs to be added to the store 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | $ref: "#/components/schemas/Pet" 40 | responses: 41 | '405': 42 | description: Invalid input 43 | put: 44 | tags: 45 | - pet 46 | summary: Update an existing pet 47 | description: '' 48 | operationId: updatePet 49 | requestBody: 50 | description: Pet object that needs to be added to the store 51 | required: true 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Pet" 56 | responses: 57 | '400': 58 | description: Invalid ID supplied 59 | '404': 60 | description: Pet not found 61 | '405': 62 | description: Validation exception 63 | components: 64 | schemas: 65 | Pet: 66 | type: object 67 | discriminator: 68 | propertyName: petType 69 | mapping: 70 | tinyLion: '#/components/schemas/Cat' 71 | docileWolf: '#/components/schemas/Dog' 72 | properties: 73 | name: 74 | type: string 75 | petType: 76 | type: string 77 | required: 78 | - name 79 | - petType 80 | Cat: ## "Cat" will be used as the discriminator value 81 | description: A representation of a cat 82 | allOf: 83 | - $ref: '#/components/schemas/Pet' 84 | - type: object 85 | properties: 86 | huntingSkill: 87 | type: string 88 | description: The measured skill for hunting 89 | enum: 90 | - clueless 91 | - lazy 92 | - adventurous 93 | - aggressive 94 | required: 95 | - huntingSkill 96 | Dog: ## "Dog" will be used as the discriminator value 97 | description: A representation of a dog 98 | allOf: 99 | - $ref: '#/components/schemas/Pet' 100 | - type: object 101 | properties: 102 | packSize: 103 | type: integer 104 | format: int32 105 | description: the size of the pack the dog is from 106 | default: 0 107 | minimum: 0 108 | required: 109 | - packSize 110 | -------------------------------------------------------------------------------- /spec/data/petstore-with-polymorphism.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | description: This is a sample server Petstore server. 4 | version: 1.0.5 5 | title: Swagger Petstore 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | email: apiteam@swagger.io 9 | license: 10 | name: Apache 2.0 11 | url: http://www.apache.org/licenses/LICENSE-2.0.html 12 | tags: 13 | - name: pet 14 | description: Everything about your Pets 15 | externalDocs: 16 | description: Find out more 17 | url: http://swagger.io 18 | - name: store 19 | description: Access to Petstore orders 20 | - name: user 21 | description: Operations about user 22 | externalDocs: 23 | description: Find out more about our store 24 | url: http://swagger.io 25 | paths: 26 | "/pet": 27 | post: 28 | tags: 29 | - pet 30 | summary: Add a new pet to the store 31 | description: '' 32 | operationId: addPet 33 | requestBody: 34 | description: Pet object that needs to be added to the store 35 | required: true 36 | content: 37 | application/json: 38 | schema: 39 | $ref: "#/components/schemas/Pet" 40 | responses: 41 | '405': 42 | description: Invalid input 43 | put: 44 | tags: 45 | - pet 46 | summary: Update an existing pet 47 | description: '' 48 | operationId: updatePet 49 | requestBody: 50 | description: Pet object that needs to be added to the store 51 | required: true 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "#/components/schemas/Pet" 56 | responses: 57 | '400': 58 | description: Invalid ID supplied 59 | '404': 60 | description: Pet not found 61 | '405': 62 | description: Validation exception 63 | components: 64 | schemas: 65 | Pet: 66 | type: object 67 | discriminator: 68 | propertyName: petType 69 | properties: 70 | name: 71 | type: string 72 | petType: 73 | type: string 74 | required: 75 | - name 76 | - petType 77 | Cat: ## "Cat" will be used as the discriminator value 78 | description: A representation of a cat 79 | allOf: 80 | - $ref: '#/components/schemas/Pet' 81 | - type: object 82 | properties: 83 | huntingSkill: 84 | type: string 85 | description: The measured skill for hunting 86 | enum: 87 | - clueless 88 | - lazy 89 | - adventurous 90 | - aggressive 91 | required: 92 | - huntingSkill 93 | Dog: ## "Dog" will be used as the discriminator value 94 | description: A representation of a dog 95 | allOf: 96 | - $ref: '#/components/schemas/Pet' 97 | - type: object 98 | properties: 99 | packSize: 100 | type: integer 101 | format: int32 102 | description: the size of the pack the dog is from 103 | default: 0 104 | minimum: 0 105 | required: 106 | - packSize 107 | -------------------------------------------------------------------------------- /spec/data/petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "MIT" 8 | } 9 | }, 10 | "host": "petstore.swagger.io", 11 | "basePath": "/v1", 12 | "schemes": [ 13 | "http" 14 | ], 15 | "consumes": [ 16 | "application/json" 17 | ], 18 | "produces": [ 19 | "application/json" 20 | ], 21 | "paths": { 22 | "/pets": { 23 | "get": { 24 | "summary": "List all pets", 25 | "operationId": "listPets", 26 | "tags": [ 27 | "pets" 28 | ], 29 | "parameters": [ 30 | { 31 | "name": "limit", 32 | "in": "query", 33 | "description": "How many items to return at one time (max 100)", 34 | "required": false, 35 | "type": "integer", 36 | "format": "int32" 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "An paged array of pets", 42 | "headers": { 43 | "x-next": { 44 | "type": "string", 45 | "description": "A link to the next page of responses" 46 | } 47 | }, 48 | "schema": { 49 | "$ref": "#/definitions/Pets" 50 | } 51 | }, 52 | "default": { 53 | "description": "unexpected error", 54 | "schema": { 55 | "$ref": "#/definitions/Error" 56 | } 57 | } 58 | } 59 | }, 60 | "post": { 61 | "summary": "Create a pet", 62 | "operationId": "createPets", 63 | "tags": [ 64 | "pets" 65 | ], 66 | "responses": { 67 | "201": { 68 | "description": "Null response" 69 | }, 70 | "default": { 71 | "description": "unexpected error", 72 | "schema": { 73 | "$ref": "#/definitions/Error" 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | "/pets/{petId}": { 80 | "get": { 81 | "summary": "Info for a specific pet", 82 | "operationId": "showPetById", 83 | "tags": [ 84 | "pets" 85 | ], 86 | "parameters": [ 87 | { 88 | "name": "petId", 89 | "in": "path", 90 | "required": true, 91 | "description": "The id of the pet to retrieve", 92 | "type": "string" 93 | } 94 | ], 95 | "responses": { 96 | "200": { 97 | "description": "Expected response to a valid request", 98 | "schema": { 99 | "$ref": "#/definitions/Pets" 100 | } 101 | }, 102 | "default": { 103 | "description": "unexpected error", 104 | "schema": { 105 | "$ref": "#/definitions/Error" 106 | } 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "definitions": { 113 | "Pet": { 114 | "required": [ 115 | "id", 116 | "name" 117 | ], 118 | "properties": { 119 | "id": { 120 | "type": "integer", 121 | "format": "int64" 122 | }, 123 | "name": { 124 | "type": "string" 125 | }, 126 | "tag": { 127 | "type": "string" 128 | } 129 | } 130 | }, 131 | "Pets": { 132 | "type": "array", 133 | "items": { 134 | "$ref": "#/definitions/Pet" 135 | } 136 | }, 137 | "Error": { 138 | "required": [ 139 | "code", 140 | "message" 141 | ], 142 | "properties": { 143 | "code": { 144 | "type": "integer", 145 | "format": "int32" 146 | }, 147 | "message": { 148 | "type": "string" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /spec/data/petstore.json.unsupported_extension: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "MIT" 8 | } 9 | }, 10 | "host": "petstore.swagger.io", 11 | "basePath": "/v1", 12 | "schemes": [ 13 | "http" 14 | ], 15 | "consumes": [ 16 | "application/json" 17 | ], 18 | "produces": [ 19 | "application/json" 20 | ], 21 | "paths": { 22 | "/pets": { 23 | "get": { 24 | "summary": "List all pets", 25 | "operationId": "listPets", 26 | "tags": [ 27 | "pets" 28 | ], 29 | "parameters": [ 30 | { 31 | "name": "limit", 32 | "in": "query", 33 | "description": "How many items to return at one time (max 100)", 34 | "required": false, 35 | "type": "integer", 36 | "format": "int32" 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "An paged array of pets", 42 | "headers": { 43 | "x-next": { 44 | "type": "string", 45 | "description": "A link to the next page of responses" 46 | } 47 | }, 48 | "schema": { 49 | "$ref": "#/definitions/Pets" 50 | } 51 | }, 52 | "default": { 53 | "description": "unexpected error", 54 | "schema": { 55 | "$ref": "#/definitions/Error" 56 | } 57 | } 58 | } 59 | }, 60 | "post": { 61 | "summary": "Create a pet", 62 | "operationId": "createPets", 63 | "tags": [ 64 | "pets" 65 | ], 66 | "responses": { 67 | "201": { 68 | "description": "Null response" 69 | }, 70 | "default": { 71 | "description": "unexpected error", 72 | "schema": { 73 | "$ref": "#/definitions/Error" 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | "/pets/{petId}": { 80 | "get": { 81 | "summary": "Info for a specific pet", 82 | "operationId": "showPetById", 83 | "tags": [ 84 | "pets" 85 | ], 86 | "parameters": [ 87 | { 88 | "name": "petId", 89 | "in": "path", 90 | "required": true, 91 | "description": "The id of the pet to retrieve", 92 | "type": "string" 93 | } 94 | ], 95 | "responses": { 96 | "200": { 97 | "description": "Expected response to a valid request", 98 | "schema": { 99 | "$ref": "#/definitions/Pets" 100 | } 101 | }, 102 | "default": { 103 | "description": "unexpected error", 104 | "schema": { 105 | "$ref": "#/definitions/Error" 106 | } 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "definitions": { 113 | "Pet": { 114 | "required": [ 115 | "id", 116 | "name" 117 | ], 118 | "properties": { 119 | "id": { 120 | "type": "integer", 121 | "format": "int64" 122 | }, 123 | "name": { 124 | "type": "string" 125 | }, 126 | "tag": { 127 | "type": "string" 128 | } 129 | } 130 | }, 131 | "Pets": { 132 | "type": "array", 133 | "items": { 134 | "$ref": "#/definitions/Pet" 135 | } 136 | }, 137 | "Error": { 138 | "required": [ 139 | "code", 140 | "message" 141 | ], 142 | "properties": { 143 | "code": { 144 | "type": "integer", 145 | "format": "int32" 146 | }, 147 | "message": { 148 | "type": "string" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /spec/data/reference-broken.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /ref-sample/broken_reference: 7 | post: 8 | description: Broken Reference in YAML 9 | requestBody: 10 | $ref: '#/components/requestBodies/foobar' 11 | responses: 12 | "204": 13 | description: empty 14 | requestBodies: 15 | hoge: 16 | type: object 17 | description: This object is defined, but `foobar` is not 18 | -------------------------------------------------------------------------------- /spec/data/reference_in_responses.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /info: 7 | get: 8 | responses: 9 | 200: 10 | $ref: '#/components/responses/Response' 11 | 12 | components: 13 | responses: 14 | Response: 15 | description: reference response 16 | content: 17 | application/json: 18 | schema: 19 | type: object 20 | -------------------------------------------------------------------------------- /spec/data/remote-file-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /local_file_reference: 7 | post: 8 | requestBody: 9 | required: true 10 | content: 11 | application/json: 12 | schema: 13 | $ref: normal.yml#/components/schemas/all_of_base 14 | responses: 15 | '200': 16 | description: correct 17 | content: 18 | application/json: 19 | schema: 20 | type: object 21 | /reference_to_path_item_with_path_param: 22 | $ref: 'path-item-ref.yaml#/paths/~1sample~1%7Bsample_id%7D' 23 | -------------------------------------------------------------------------------- /spec/data/remote-http-ref.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /http_reference: 7 | post: 8 | requestBody: 9 | required: true 10 | content: 11 | application/json: 12 | schema: 13 | $ref: https://raw.githubusercontent.com/ota42y/openapi_parser/091b20838cf0bfbc7bed2c9968b10bb0b9e8a789/spec/data/normal.yml#/components/schemas/all_of_base 14 | responses: 15 | '200': 16 | description: correct 17 | content: 18 | application/json: 19 | schema: 20 | type: object 21 | -------------------------------------------------------------------------------- /spec/data/validate_test.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: OpenAPI3 Test 5 | paths: 6 | /validate_test: 7 | post: 8 | description: override here 9 | requestBody: 10 | content: 11 | 'application/json': 12 | schema: 13 | type: object 14 | properties: 15 | test: 16 | type: string 17 | responses: 18 | '204': 19 | description: empty 20 | -------------------------------------------------------------------------------- /spec/openapi_parser/concerns/expandable_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Expandable do 4 | let(:root) { OpenAPIParser.parse(normal_schema, {}) } 5 | 6 | describe 'expand_reference' do 7 | subject { root } 8 | 9 | it do 10 | subject 11 | expect(subject.find_object('#/paths/~1reference/get/parameters/0').class).to eq OpenAPIParser::Schemas::Parameter 12 | expect(subject.find_object('#/paths/~1reference/get/responses/default').class).to eq OpenAPIParser::Schemas::Response 13 | expect(subject.find_object('#/paths/~1reference/post/responses/default').class).to eq OpenAPIParser::Schemas::Response 14 | path = '#/paths/~1string_params_coercer/post/requestBody/content/application~1json/schema/properties/nested_array' 15 | expect(subject.find_object(path).class).to eq OpenAPIParser::Schemas::Schema 16 | end 17 | 18 | context 'undefined spec references' do 19 | let(:invalid_reference) { '#/paths/~1ref-sample~1broken_reference/get/requestBody' } 20 | let(:not_configured) { {} } 21 | let(:raise_on_invalid_reference) { { strict_reference_validation: true } } 22 | let(:misconfiguration) { 23 | { 24 | expand_reference: false, 25 | strict_reference_validation: true 26 | } 27 | } 28 | 29 | it 'raises when configured to do so' do 30 | raise_message = "'#/components/requestBodies/foobar' was referenced but could not be found" 31 | expect { OpenAPIParser.parse(broken_reference_schema, raise_on_invalid_reference) }.to( 32 | raise_error(OpenAPIParser::MissingReferenceError) { |error| expect(error.message).to eq(raise_message) } 33 | ) 34 | end 35 | 36 | it 'does not raise when not configured, returns nil reference' do 37 | subject = OpenAPIParser.parse(broken_reference_schema, not_configured) 38 | expect(subject.find_object(invalid_reference)).to be_nil 39 | end 40 | 41 | it 'does not raise when configured, but expand_reference is false' do 42 | subject = OpenAPIParser.parse(broken_reference_schema, misconfiguration) 43 | expect(subject.find_object(invalid_reference)).to be_nil 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/openapi_parser/concerns/findable_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Findable do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'openapi object' do 7 | subject { root } 8 | 9 | it do 10 | expect(subject.find_object('#/paths/~1pets/get').object_reference).to eq root.paths.path['/pets'].get.object_reference 11 | expect(subject.find_object('#/components/requestBodies/test_body').object_reference).to eq root.components.request_bodies['test_body'].object_reference 12 | 13 | expect(subject.instance_variable_get(:@find_object_cache).size).to eq 2 14 | end 15 | end 16 | 17 | describe 'schema object' do 18 | subject { root.paths } 19 | 20 | it do 21 | expect(subject.find_object('#/paths/~1pets/get').object_reference).to eq root.paths.path['/pets'].get.object_reference 22 | end 23 | end 24 | 25 | describe 'remote reference' do 26 | describe 'local file reference' do 27 | let(:root) { OpenAPIParser.load('spec/data/remote-file-ref.yaml') } 28 | let(:request_operation) { root.request_operation(:post, '/local_file_reference') } 29 | 30 | it 'validates request body using schema defined in another openapi yaml file' do 31 | request_operation.validate_request_body('application/json', { 'name' => 'name' }) 32 | end 33 | end 34 | 35 | describe 'http reference' do 36 | let(:root) { OpenAPIParser.load('spec/data/remote-http-ref.yaml') } 37 | let(:request_operation) { root.request_operation(:post, '/http_reference') } 38 | 39 | it 'validates request body using schema defined in openapi yaml file accessed via http' do 40 | request_operation.validate_request_body('application/json', { 'name' => 'name' }) 41 | end 42 | end 43 | 44 | describe 'cyclic remote reference' do 45 | let(:root) { OpenAPIParser.load('spec/data/cyclic-remote-ref1.yaml') } 46 | let(:request_operation) { root.request_operation(:post, '/cyclic_reference') } 47 | 48 | it 'validates request body using schema defined in another openapi yaml file' do 49 | request_operation.validate_request_body('application/json', { 'content' => 1 }) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/openapi_parser/concerns/media_type_selectable_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::MediaTypeSelectable do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe '#select_media_type(content_type, content)' do 7 | subject { media_type_selectable.select_media_type(content_type) } 8 | 9 | context '*/* exist' do 10 | let(:paths) { root.paths } 11 | let(:path_item) { paths.path['/pets'] } 12 | let(:operation) { path_item.get } 13 | let(:responses) { operation.responses } 14 | let(:media_type_selectable) { responses.response['200'] } 15 | 16 | context 'application/json' do 17 | let(:content_type) { 'application/json' } 18 | it { expect(subject.object_reference.include?('application~1json')).to eq true } 19 | end 20 | 21 | context 'application/unknown' do 22 | let(:content_type) { 'application/unknown' } 23 | it { expect(subject.object_reference.include?('application~1unknown')).to eq true } 24 | end 25 | 26 | context 'application/abc' do 27 | let(:content_type) { 'application/abc' } 28 | it { expect(subject.object_reference.include?('application~1*')).to eq true } 29 | end 30 | 31 | context 'not mach' do 32 | let(:content_type) { 'xyz/abc' } 33 | it { expect(subject.object_reference.include?('*~1*')).to eq true } 34 | end 35 | end 36 | 37 | context '*/* not exist' do 38 | let(:paths) { root.paths } 39 | let(:path_item) { paths.path['/pets'] } 40 | let(:operation) { path_item.post } 41 | let(:request_body) { operation.request_body } 42 | 43 | let(:media_type_selectable) { request_body } 44 | 45 | let(:content_type) { 'application/unknown' } 46 | 47 | it 'return nil' do 48 | expect(subject).to eq nil 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/openapi_parser/concerns/schema_loader/base_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::SchemaLoader::Base do 4 | describe '#load_data' do 5 | subject { OpenAPIParser::SchemaLoader::Base.new(nil, {}).load_data(nil, nil) } 6 | 7 | it { expect { subject }.to raise_error(StandardError).with_message('need implement') } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/openapi_parser/parameter_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::ParameterValidator do 4 | let(:root) { OpenAPIParser.parse(normal_schema, config) } 5 | let(:config) { {} } 6 | 7 | describe 'validate' do 8 | subject { request_operation.validate_request_parameter(params, {}) } 9 | 10 | let(:content_type) { 'application/json' } 11 | let(:http_method) { :get } 12 | let(:request_path) { '/validate' } 13 | let(:request_operation) { root.request_operation(http_method, request_path) } 14 | let(:params) { {} } 15 | 16 | context 'correct' do 17 | context 'no optional' do 18 | let(:params) { { 'query_string' => 'query', 'query_integer_list' => [1, 2], 'queryString' => 'Query' } } 19 | it { expect(subject).to eq({ 'query_string' => 'query', 'query_integer_list' => [1, 2], 'queryString' => 'Query' }) } 20 | end 21 | 22 | context 'with optional' do 23 | let(:params) { { 'query_string' => 'query', 'query_integer_list' => [1, 2], 'queryString' => 'Query', 'optional_integer' => 1 } } 24 | it { expect(subject).to eq({ 'optional_integer' => 1, 'query_integer_list' => [1, 2], 'queryString' => 'Query', 'query_string' => 'query' }) } 25 | end 26 | end 27 | 28 | context 'invalid' do 29 | context 'not exist required' do 30 | context 'not exist data' do 31 | let(:params) { { 'query_integer_list' => [1, 2], 'queryString' => 'Query' } } 32 | 33 | it do 34 | expect { subject }.to raise_error do |e| 35 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 36 | expect(e.message).to end_with('missing required parameters: query_string') 37 | end 38 | end 39 | end 40 | 41 | context 'not exist array' do 42 | let(:params) { { 'query_string' => 'query', 'queryString' => 'Query' } } 43 | 44 | it do 45 | expect { subject }.to raise_error do |e| 46 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 47 | expect(e.message).to end_with('missing required parameters: query_integer_list') 48 | end 49 | end 50 | end 51 | end 52 | 53 | context 'non null check' do 54 | context 'optional' do 55 | let(:params) { { 'query_string' => 'query', 'query_integer_list' => [1, 2], 'queryString' => 'Query', 'optional_integer' => nil } } 56 | it { expect { subject }.to raise_error(OpenAPIParser::NotNullError) } 57 | end 58 | 59 | context 'optional' do 60 | let(:params) { { 'query_string' => 'query', 'query_integer_list' => nil, 'queryString' => 'Query' } } 61 | it { expect { subject }.to raise_error(OpenAPIParser::NotNullError) } 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/openapi_parser/path_item_finder_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::PathItemFinder do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'parse_path_parameters' do 7 | subject { OpenAPIParser::PathItemFinder.new(root.paths) } 8 | 9 | it 'matches a single parameter with no additional characters' do 10 | result = subject.parse_path_parameters('{id}', '123') 11 | expect(result).to eq({'id' => '123'}) 12 | end 13 | 14 | it 'matches a single parameter with extension' do 15 | result = subject.parse_path_parameters('{id}.json', '123.json') 16 | expect(result).to eq({'id' => '123'}) 17 | end 18 | 19 | it 'matches a single parameter with additional characters' do 20 | result = subject.parse_path_parameters('stuff_{id}_hoge', 'stuff_123_hoge') 21 | expect(result).to eq({'id' => '123'}) 22 | end 23 | 24 | it 'matches multiple parameters with additional characters' do 25 | result = subject.parse_path_parameters('{stuff_with_underscores-and-hyphens}_{id}_hoge', '4_123_hoge') 26 | expect(result).to eq({'stuff_with_underscores-and-hyphens' => '4', 'id' => '123'}) 27 | end 28 | 29 | # Open API spec does not specifically define what characters are acceptable as a parameter name, 30 | # so allow everything in between {}. 31 | it 'matches when parameter contains regex characters' do 32 | result = subject.parse_path_parameters('{id?}.json', '123.json') 33 | expect(result).to eq({'id?' => '123'}) 34 | 35 | result = subject.parse_path_parameters('{id.}.json', '123.json') 36 | expect(result).to eq({'id.' => '123'}) 37 | 38 | result = subject.parse_path_parameters('{{id}}.json', '{123}.json') 39 | expect(result).to eq({'id' => '123'}) 40 | end 41 | 42 | it 'fails to match' do 43 | result = subject.parse_path_parameters('stuff_{id}_', '123') 44 | expect(result).to be_nil 45 | 46 | result = subject.parse_path_parameters('{p1}-{p2}.json', 'foo.json') 47 | expect(result).to be_nil 48 | 49 | result = subject.parse_path_parameters('{p1}.json', 'foo.txt') 50 | expect(result).to be_nil 51 | 52 | result = subject.parse_path_parameters('{{id}}.json', '123.json') 53 | expect(result).to be_nil 54 | end 55 | 56 | it 'fails to match when a Regex escape character is used in the path' do 57 | result = subject.parse_path_parameters('{id}.json', '123-json') 58 | expect(result).to be_nil 59 | end 60 | 61 | it 'fails to match no input' do 62 | result = subject.parse_path_parameters('', '') 63 | expect(result).to be_nil 64 | end 65 | 66 | it 'matches when the last character of the variable is the same as the next character' do 67 | result = subject.parse_path_parameters('{p1}schedule', 'adminsschedule') 68 | expect(result).to eq({'p1' => 'admins'}) 69 | 70 | result = subject.parse_path_parameters('{p1}schedule', 'usersschedule') 71 | expect(result).to eq({'p1' => 'users'}) 72 | end 73 | end 74 | 75 | describe 'find' do 76 | subject { OpenAPIParser::PathItemFinder.new(root.paths) } 77 | 78 | it do 79 | expect(subject.class).to eq OpenAPIParser::PathItemFinder 80 | 81 | result = subject.operation_object(:get, '/pets') 82 | expect(result.class).to eq OpenAPIParser::PathItemFinder::Result 83 | expect(result.original_path).to eq('/pets') 84 | expect(result.operation_object.object_reference).to eq root.find_object('#/paths/~1pets/get').object_reference 85 | expect(result.path_params.empty?).to eq true 86 | 87 | result = subject.operation_object(:get, '/pets/1') 88 | expect(result.original_path).to eq('/pets/{id}') 89 | expect(result.operation_object.object_reference).to eq root.find_object('#/paths/~1pets~1{id}/get').object_reference 90 | expect(result.path_params['id']).to eq '1' 91 | 92 | result = subject.operation_object(:post, '/pets/lessie/adopt/123') 93 | expect(result.original_path).to eq('/pets/{nickname}/adopt/{param_2}') 94 | expect(result.operation_object.object_reference) 95 | .to eq root.find_object('#/paths/~1pets~1{nickname}~1adopt~1{param_2}/post').object_reference 96 | expect(result.path_params['nickname']).to eq 'lessie' 97 | expect(result.path_params['param_2']).to eq '123' 98 | end 99 | 100 | it 'matches path items that end in a file extension' do 101 | result = subject.operation_object(:get, '/animals/123/456.json') 102 | expect(result.original_path).to eq('/animals/{groupId}/{id}.json') 103 | expect(result.operation_object.object_reference).to eq root.find_object('#/paths/~1animals~1{groupId}~1{id}.json/get').object_reference 104 | expect(result.path_params['groupId']).to eq '123' 105 | expect(result.path_params['id']).to eq '456' 106 | end 107 | 108 | it 'ignores invalid HTTP methods' do 109 | expect(subject.operation_object(:exit, '/pets')).to eq(nil) 110 | expect(subject.operation_object(:blah, '/pets')).to eq(nil) 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/openapi_parser/path_item_ref_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | RSpec.describe 'path item $ref' do 4 | let(:root) { OpenAPIParser.parse_with_filepath( 5 | path_item_ref_schema, path_item_ref_schema_path, {}) 6 | } 7 | let(:request_operation) { root.request_operation(:post, '/ref-sample') } 8 | 9 | it 'understands path item $ref' do 10 | ret = request_operation.validate_request_body('application/json', { 'test' => 'test' }) 11 | expect(ret).to eq({ 'test' => "test" }) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/openapi_parser/schema_validator/all_of_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::RequestBody do 4 | let(:root) { OpenAPIParser.parse(petstore_with_discriminator_schema, {}) } 5 | 6 | describe 'allOf nested objects' do 7 | let(:content_type) { 'application/json' } 8 | let(:http_method) { :post } 9 | let(:request_path) { '/save_the_pets' } 10 | let(:request_operation) { root.request_operation(http_method, request_path) } 11 | let(:params) { {} } 12 | 13 | context "without additionalProperties defined" do 14 | it 'passes when sending all properties' do 15 | body = { 16 | "baskets" => [ 17 | { 18 | "name" => "dragon", 19 | "mass" => 10, 20 | "fire_range" => 20 21 | } 22 | ] 23 | } 24 | request_operation.validate_request_body(content_type, body) 25 | end 26 | 27 | it 'fails when sending unknown properties' do 28 | body = { 29 | "baskets" => [ 30 | { 31 | "name" => "dragon", 32 | "mass" => 10, 33 | "fire_range" => 20, 34 | "speed" => 20 35 | } 36 | ] 37 | } 38 | 39 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 40 | expect(e).to be_kind_of(OpenAPIParser::NotExistPropertyDefinition) 41 | expect(e.message).to end_with("does not define properties: speed") 42 | end 43 | end 44 | 45 | it 'fails when missing required property' do 46 | body = { 47 | "baskets" => [ 48 | { 49 | "name" => "dragon", 50 | "mass" => 10, 51 | } 52 | ] 53 | } 54 | 55 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 56 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 57 | expect(e.message).to end_with("missing required parameters: fire_range") 58 | end 59 | end 60 | end 61 | 62 | context "with additionalProperties defined" do 63 | it 'passes when sending all properties' do 64 | body = { 65 | "baskets" => [ 66 | { 67 | "name" => "hydra", 68 | "mass" => 10, 69 | "head_count" => 20 70 | } 71 | ] 72 | } 73 | request_operation.validate_request_body(content_type, body) 74 | end 75 | 76 | it 'succeeds when sending unknown properties of correct type based on additionalProperties' do 77 | body = { 78 | "baskets" => [ 79 | { 80 | "name" => "hydra", 81 | "mass" => 10, 82 | "head_count" => 20, 83 | "speed" => "20" 84 | } 85 | ] 86 | } 87 | 88 | request_operation.validate_request_body(content_type, body) 89 | end 90 | 91 | it 'fails when sending unknown properties of correct type based on additionalProperties' do 92 | body = { 93 | "baskets" => [ 94 | { 95 | "name" => "hydra", 96 | "mass" => 10, 97 | "head_count" => 20, 98 | "speed" => 20 99 | } 100 | ] 101 | } 102 | 103 | # TODO for now we don't validate on additionalProperites, but this should fail on speed have to be string 104 | request_operation.validate_request_body(content_type, body) 105 | end 106 | 107 | it 'fails when missing required property' do 108 | body = { 109 | "baskets" => [ 110 | { 111 | "name" => "hydra", 112 | "mass" => 10, 113 | } 114 | ] 115 | } 116 | 117 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 118 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 119 | expect(e.message).to end_with("missing required parameters: head_count") 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /spec/openapi_parser/schema_validator/array_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::SchemaValidator::ArrayValidator do 4 | let(:replace_schema) { {} } 5 | let(:root) { OpenAPIParser.parse(build_validate_test_schema(replace_schema), config) } 6 | let(:config) { {} } 7 | let(:target_schema) do 8 | root.paths.path['/validate_test'].operation(:post).request_body.content['application/json'].schema 9 | end 10 | let(:options) { ::OpenAPIParser::SchemaValidator::Options.new } 11 | 12 | describe 'validate array' do 13 | subject { OpenAPIParser::SchemaValidator.validate(params, target_schema, options) } 14 | 15 | let(:params) { {} } 16 | let(:replace_schema) do 17 | { 18 | ids: { 19 | type: 'array', 20 | items: { 'type': 'integer' }, 21 | maxItems: 2, 22 | minItems: 1, 23 | uniqueItems: true 24 | }, 25 | } 26 | end 27 | 28 | context 'correct' do 29 | let(:params) { { 'ids' => [1] } } 30 | it { expect(subject).to eq({ 'ids' => [1] }) } 31 | end 32 | 33 | context 'invalid' do 34 | context 'max items breached' do 35 | let(:invalid_array) { [1,2,3,4] } 36 | let(:params) { { 'ids' => invalid_array } } 37 | 38 | it do 39 | expect { subject }.to raise_error do |e| 40 | expect(e).to be_kind_of(OpenAPIParser::MoreThanMaxItems) 41 | expect(e.message).to end_with("#{invalid_array} contains more than max items") 42 | end 43 | end 44 | end 45 | 46 | context 'min items breached' do 47 | let(:invalid_array) { [] } 48 | let(:params) { { 'ids' => invalid_array } } 49 | 50 | it do 51 | expect { subject }.to raise_error do |e| 52 | expect(e).to be_kind_of(OpenAPIParser::LessThanMinItems) 53 | expect(e.message).to end_with("#{invalid_array} contains fewer than min items") 54 | end 55 | end 56 | end 57 | 58 | context 'unique items breached' do 59 | let(:invalid_array) { [1, 1] } 60 | let(:params) { { 'ids' => invalid_array } } 61 | 62 | it do 63 | expect { subject }.to raise_error do |e| 64 | expect(e).to be_kind_of(OpenAPIParser::NotUniqueItems) 65 | expect(e.message).to end_with("#{invalid_array} contains duplicate items") 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/openapi_parser/schema_validator/base_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::SchemaValidator::Base do 4 | describe '#coerce_and_validate(_value, _schema)' do 5 | subject { OpenAPIParser::SchemaValidator::Base.new(nil, nil).coerce_and_validate(nil, nil) } 6 | 7 | it { expect { subject }.to raise_error(StandardError).with_message('need implement') } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/openapi_parser/schema_validator/integer_validator_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::SchemaValidator::IntegerValidator do 4 | let(:replace_schema) { {} } 5 | let(:root) { OpenAPIParser.parse(build_validate_test_schema(replace_schema), config) } 6 | let(:config) { {} } 7 | let(:target_schema) do 8 | root.paths.path['/validate_test'].operation(:post).request_body.content['application/json'].schema 9 | end 10 | let(:options) { ::OpenAPIParser::SchemaValidator::Options.new } 11 | 12 | describe 'validate integer maximum value' do 13 | subject { OpenAPIParser::SchemaValidator.validate(params, target_schema, options) } 14 | 15 | let(:params) { {} } 16 | let(:replace_schema) do 17 | { 18 | my_integer: { 19 | type: 'integer', 20 | maximum: 10, 21 | }, 22 | } 23 | end 24 | 25 | context 'correct' do 26 | let(:params) { { 'my_integer' => 10 } } 27 | it { expect(subject).to eq({ 'my_integer' => 10 }) } 28 | end 29 | 30 | context 'invalid' do 31 | context 'more than maximum' do 32 | let(:more_than_maximum) { 11 } 33 | let(:params) { { 'my_integer' => more_than_maximum } } 34 | 35 | it do 36 | expect { subject }.to raise_error do |e| 37 | expect(e).to be_kind_of(OpenAPIParser::MoreThanMaximum) 38 | expect(e.message).to end_with("#{more_than_maximum} is more than maximum value") 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | describe 'validate integer minimum value' do 46 | subject { OpenAPIParser::SchemaValidator.validate(params, target_schema, options) } 47 | 48 | let(:params) { {} } 49 | let(:replace_schema) do 50 | { 51 | my_integer: { 52 | type: 'integer', 53 | minimum: 10, 54 | }, 55 | } 56 | end 57 | 58 | context 'correct' do 59 | let(:params) { { 'my_integer' => 10 } } 60 | it { expect(subject).to eq({ 'my_integer' => 10 }) } 61 | end 62 | 63 | context 'invalid' do 64 | context 'less than minimum' do 65 | let(:less_than_minimum) { 9 } 66 | let(:params) { { 'my_integer' => less_than_minimum } } 67 | 68 | it do 69 | expect { subject }.to raise_error do |e| 70 | expect(e).to be_kind_of(OpenAPIParser::LessThanMinimum) 71 | expect(e.message).to end_with("#{less_than_minimum} is less than minimum value") 72 | end 73 | end 74 | end 75 | end 76 | end 77 | 78 | describe 'validate integer exclusiveMaximum value' do 79 | subject { OpenAPIParser::SchemaValidator.validate(params, target_schema, options) } 80 | 81 | let(:params) { {} } 82 | let(:replace_schema) do 83 | { 84 | my_integer: { 85 | type: 'integer', 86 | maximum: 10, 87 | exclusiveMaximum: true, 88 | }, 89 | } 90 | end 91 | 92 | context 'correct' do 93 | let(:params) { { 'my_integer' => 9 } } 94 | it { expect(subject).to eq({ 'my_integer' => 9 }) } 95 | end 96 | 97 | context 'invalid' do 98 | context 'more than or equal to exclusive maximum' do 99 | let(:more_than_equal_exclusive_maximum) { 10 } 100 | let(:params) { { 'my_integer' => more_than_equal_exclusive_maximum } } 101 | 102 | it do 103 | expect { subject }.to raise_error do |e| 104 | expect(e).to be_kind_of(OpenAPIParser::MoreThanExclusiveMaximum) 105 | expect(e.message).to end_with("#{more_than_equal_exclusive_maximum} cannot be more than or equal to exclusive maximum value") 106 | end 107 | end 108 | end 109 | end 110 | end 111 | 112 | describe 'validate integer exclusiveMinimum value' do 113 | subject { OpenAPIParser::SchemaValidator.validate(params, target_schema, options) } 114 | 115 | let(:params) { {} } 116 | let(:replace_schema) do 117 | { 118 | my_integer: { 119 | type: 'integer', 120 | minimum: 10, 121 | exclusiveMinimum: true, 122 | }, 123 | } 124 | end 125 | 126 | context 'correct' do 127 | let(:params) { { 'my_integer' => 11 } } 128 | it { expect(subject).to eq({ 'my_integer' => 11 }) } 129 | end 130 | 131 | context 'invalid' do 132 | context 'less than or equal to exclusive minimum' do 133 | let(:less_than_equal_exclusive_minimum) { 10 } 134 | let(:params) { { 'my_integer' => less_than_equal_exclusive_minimum } } 135 | 136 | it do 137 | expect { subject }.to raise_error do |e| 138 | expect(e).to be_kind_of(OpenAPIParser::LessThanExclusiveMinimum) 139 | expect(e.message).to end_with("#{less_than_equal_exclusive_minimum} cannot be less than or equal to exclusive minimum value") 140 | end 141 | end 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/base_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Base do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'inspect' do 7 | subject { root.inspect } 8 | 9 | it { expect(subject).to eq '#' } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/components_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Components do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'correct init' do 7 | subject { root.find_object('#/components') } 8 | 9 | it do 10 | expect(subject.root.object_id).to be root.object_id 11 | expect(subject.class).to eq OpenAPIParser::Schemas::Components 12 | expect(subject.object_reference).to eq '#/components' 13 | 14 | expect(subject.parameters['test'].class).to eq OpenAPIParser::Schemas::Parameter 15 | expect(subject.parameters['test_ref'].class).to eq OpenAPIParser::Schemas::Parameter 16 | 17 | expect(subject.schemas['Pet'].class).to eq OpenAPIParser::Schemas::Schema 18 | 19 | expect(subject.responses['normal'].class).to eq OpenAPIParser::Schemas::Response 20 | 21 | expect(subject.request_bodies['test_body'].class).to eq OpenAPIParser::Schemas::RequestBody 22 | expect(subject.request_bodies['test_body'].object_reference).to eq '#/components/requestBodies/test_body' 23 | 24 | expect(subject.headers['X-Rate-Limit-Limit'].class).to eq OpenAPIParser::Schemas::Header 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/discriminator_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::RequestBody do 4 | let(:root) { OpenAPIParser.parse(petstore_with_discriminator_schema, {}) } 5 | 6 | describe 'discriminator' do 7 | let(:content_type) { 'application/json' } 8 | let(:http_method) { :post } 9 | let(:request_path) { '/save_the_pets' } 10 | let(:request_operation) { root.request_operation(http_method, request_path) } 11 | let(:params) { {} } 12 | 13 | it 'picks correct object based on mapping and succeeds' do 14 | body = { 15 | "baskets" => [ 16 | { 17 | "name" => "cats", 18 | "content" => [ 19 | { 20 | "name" => "Mr. Cat", 21 | "born_at" => "2019-05-16T11:37:02.160Z", 22 | "description" => "Cat gentleman", 23 | "milk_stock" => 10 24 | } 25 | ] 26 | }, 27 | ] 28 | } 29 | 30 | request_operation.validate_request_body(content_type, body) 31 | end 32 | 33 | it 'picks correct object based on implicit mapping and succeeds' do 34 | body = { 35 | "baskets" => [ 36 | { 37 | "name" => "CatBasket", 38 | "content" => [ 39 | { 40 | "name" => "Mr. Cat", 41 | "born_at" => "2019-05-16T11:37:02.160Z", 42 | "description" => "Cat gentleman", 43 | "milk_stock" => 10 44 | } 45 | ] 46 | }, 47 | ] 48 | } 49 | 50 | request_operation.validate_request_body(content_type, body) 51 | end 52 | 53 | it 'picks correct object based on mapping and fails' do 54 | body = { 55 | "baskets" => [ 56 | { 57 | "name" => "cats", 58 | "content" => [ 59 | { 60 | "name" => "Mr. Cat", 61 | "born_at" => "2019-05-16T11:37:02.160Z", 62 | "description" => "Cat gentleman", 63 | "nut_stock" => 10 # passing squirrel attribute here, but discriminator still picks cats and fails 64 | } 65 | ] 66 | }, 67 | ] 68 | } 69 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 70 | expect(e).to be_kind_of(OpenAPIParser::NotExistPropertyDefinition) 71 | expect(e.message).to end_with("does not define properties: nut_stock") 72 | end 73 | end 74 | 75 | it 'picks correct object based on implicit mapping and fails' do 76 | body = { 77 | "baskets" => [ 78 | { 79 | "name" => "CatBasket", 80 | "content" => [ 81 | { 82 | "name" => "Mr. Cat", 83 | "born_at" => "2019-05-16T11:37:02.160Z", 84 | "description" => "Cat gentleman", 85 | "nut_stock" => 10 # passing squirrel attribute here, but discriminator still picks cats and fails 86 | } 87 | ] 88 | }, 89 | ] 90 | } 91 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 92 | expect(e).to be_kind_of(OpenAPIParser::NotExistPropertyDefinition) 93 | expect(e.message).to end_with("does not define properties: nut_stock") 94 | end 95 | end 96 | 97 | it "throws error when discriminator mapping is not found" do 98 | body = { 99 | "baskets" => [ 100 | { 101 | "name" => "dogs", 102 | "content" => [ 103 | { 104 | "name" => "Mr. Dog", 105 | "born_at" => "2019-05-16T11:37:02.160Z", 106 | "description" => "Dog bruiser", 107 | "nut_stock" => 10 108 | } 109 | ] 110 | }, 111 | ] 112 | } 113 | 114 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 115 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorMappedSchema)).to eq true 116 | expect(e.message).to match("^discriminator mapped schema #/components/schemas/dogs does not exist.*?$") 117 | end 118 | end 119 | 120 | it "throws error if discriminator propertyName is not present on object" do 121 | body = { 122 | "baskets" => [ 123 | { 124 | "content" => [ 125 | { 126 | "name" => "Mr. Dog", 127 | "born_at" => "2019-05-16T11:37:02.160Z", 128 | "description" => "Dog bruiser", 129 | "milk_stock" => 10 130 | } 131 | ] 132 | }, 133 | ] 134 | } 135 | 136 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 137 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorPropertyName)).to eq true 138 | expect(e.message).to match("^discriminator propertyName name does not exist in value.*?$") 139 | end 140 | end 141 | end 142 | 143 | describe 'discriminator without mapping' do 144 | let(:content_type) { 'application/json' } 145 | let(:http_method) { :post } 146 | let(:request_path) { '/save_the_pets_without_mapping' } 147 | let(:request_operation) { root.request_operation(http_method, request_path) } 148 | 149 | it 'picks correct object based on implicit mapping and succeeds' do 150 | body = { 151 | "baskets" => [ 152 | { 153 | "name" => "CatBasket", 154 | "content" => [ 155 | { 156 | "name" => "Mr. Cat", 157 | "born_at" => "2019-05-16T11:37:02.160Z", 158 | "description" => "Cat gentleman", 159 | "milk_stock" => 10 160 | } 161 | ] 162 | }, 163 | ] 164 | } 165 | 166 | request_operation.validate_request_body(content_type, body) 167 | end 168 | 169 | it 'picks correct object based on implicit mapping and fails' do 170 | body = { 171 | "baskets" => [ 172 | { 173 | "name" => "CatBasket", 174 | "content" => [ 175 | { 176 | "name" => "Mr. Cat", 177 | "born_at" => "2019-05-16T11:37:02.160Z", 178 | "description" => "Cat gentleman", 179 | "nut_stock" => 10 # passing squirrel attribute here, but discriminator still picks cats and fails 180 | } 181 | ] 182 | }, 183 | ] 184 | } 185 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 186 | expect(e).to be_kind_of(OpenAPIParser::NotExistPropertyDefinition) 187 | expect(e.message).to end_with("does not define properties: nut_stock") 188 | end 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/header_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Header do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'correct init' do 7 | let(:paths) { root.paths } 8 | let(:path_item) { paths.path['/pets'] } 9 | let(:operation) { path_item.get } 10 | let(:response) { operation.responses.response['200'] } 11 | 12 | it do 13 | h = response.headers['x-next'] 14 | 15 | expect(h).not_to be nil 16 | expect(h.object_reference).to eq '#/paths/~1pets/get/responses/200/headers/x-next' 17 | expect(h.root.object_id).to be root.object_id 18 | expect(h.schema.class).to be OpenAPIParser::Schemas::Schema 19 | end 20 | 21 | it 'headers reference' do 22 | h = response.headers['x-limit'] 23 | 24 | expect(h).not_to be nil 25 | expect(h.class).to be OpenAPIParser::Schemas::Header 26 | expect(h.object_reference).to eq '#/components/headers/X-Rate-Limit-Limit' 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/media_type_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::MediaType do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, { expand_reference: false }) } 5 | 6 | describe 'correct init' do 7 | subject { request_body.content['application/json'] } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | let(:operation) { path_item.post } 12 | let(:request_body) { operation.request_body } 13 | 14 | it do 15 | expect(subject.class).to eq OpenAPIParser::Schemas::MediaType 16 | expect(subject.object_reference).to eq '#/paths/~1pets/post/requestBody/content/application~1json' 17 | expect(subject.root.object_id).to be root.object_id 18 | 19 | expect(subject.schema.class).to eq OpenAPIParser::Schemas::Reference 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/open_api_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::OpenAPI do 4 | subject { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'init' do 7 | it 'correct init' do 8 | expect(subject).not_to be nil 9 | expect(subject.root.object_id).to eq subject.object_id 10 | end 11 | end 12 | 13 | describe '#openapi' do 14 | it { expect(subject.openapi).to eq '3.0.0' } 15 | end 16 | 17 | describe '#paths' do 18 | it { expect(subject.paths).not_to eq nil } 19 | end 20 | 21 | describe '#components' do 22 | it { expect(subject.components).not_to eq nil } 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/operation_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Operation do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, { expand_reference: false }) } 5 | 6 | describe '#init' do 7 | subject { path_item.get } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | 12 | it do 13 | expect(subject).not_to be nil 14 | expect(subject.object_reference).to eq '#/paths/~1pets/get' 15 | expect(subject.root.object_id).to be root.object_id 16 | end 17 | end 18 | 19 | describe 'attributes' do 20 | let(:paths) { root.paths } 21 | let(:path_item) { paths.path['/pets'] } 22 | let(:operation) { path_item.get } 23 | 24 | it do 25 | expect(operation.description.start_with?('Returns all pets')).to eq true 26 | expect(operation.tags).to eq ['tag_1', 'tag_2'] 27 | expect(operation.summary).to eq 'sum' 28 | expect(operation.operation_id).to eq 'findPets' 29 | expect(operation.deprecated).to eq true 30 | expect(operation.responses.class).to eq OpenAPIParser::Schemas::Responses 31 | expect(operation._openapi_all_child_objects.size).to eq 5 # parameters * 5 + responses 32 | end 33 | end 34 | 35 | describe 'parameters' do 36 | let(:paths) { root.paths } 37 | let(:path_item) { paths.path['/pets'] } 38 | let(:operation) { path_item.get } 39 | 40 | it do 41 | expect(operation.parameters.size).to eq 4 42 | expect(operation.parameters[0].kind_of?(OpenAPIParser::Schemas::Parameter)).to eq true 43 | expect(operation.parameters[2].kind_of?(OpenAPIParser::Schemas::Reference)).to eq true 44 | end 45 | end 46 | 47 | describe 'findable' do 48 | let(:paths) { root.paths } 49 | let(:path_item) { paths.path['/pets'] } 50 | let(:operation) { path_item.get } 51 | 52 | it do 53 | expect(operation.find_object('#/paths/~1pets/get').object_reference).to eq operation.object_reference 54 | expect(operation.find_object('#/paths/~1pets/get/1')).to eq nil 55 | expect(operation.find_object('#/paths/~1pets/get/responses').object_reference).to eq operation.responses.object_reference 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/parameter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Parameter do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'correct init' do 7 | subject { operation.parameters.first } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | let(:operation) { path_item.get } 12 | 13 | it do 14 | expect(subject).not_to be nil 15 | expect(subject.object_reference).to eq '#/paths/~1pets/get/parameters/0' 16 | expect(subject.root.object_id).to be root.object_id 17 | end 18 | end 19 | 20 | describe 'attributes' do 21 | subject { operation.parameters.first } 22 | 23 | let(:paths) { root.paths } 24 | let(:path_item) { paths.path['/pets'] } 25 | let(:operation) { path_item.get } 26 | 27 | it do 28 | results = { 29 | name: 'tags', 30 | in: 'query', 31 | description: 'tags to filter by', 32 | required: false, 33 | style: 'form', 34 | } 35 | 36 | results.each { |k, v| expect(subject.send(k)).to eq v } 37 | 38 | expect(subject.allow_empty_value).to eq true 39 | end 40 | end 41 | 42 | describe 'header support' do 43 | subject { path_item.parameters.last } 44 | 45 | let(:paths) { root.paths } 46 | let(:path_item) { paths.path['/animals/{id}'] } 47 | 48 | it do 49 | results = { 50 | name: 'token', 51 | in: 'header', 52 | description: 'token to be passed as a header', 53 | required: true, 54 | style: 'simple', 55 | } 56 | 57 | results.each { |k, v| expect(subject.send(k)).to eq v } 58 | 59 | expect(subject.schema.type).to eq 'integer' 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/path_item_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::PathItem do 4 | let(:root) { OpenAPIParser.parse(normal_schema, {}) } 5 | 6 | describe '#init' do 7 | subject { path_item } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/characters'] } 11 | 12 | it do 13 | expect(subject).not_to be nil 14 | expect(subject.object_reference).to eq '#/paths/~1characters' 15 | expect(subject.summary).to eq 'summary_text' 16 | expect(subject.description).to eq 'desc' 17 | expect(subject.root.object_id).to be root.object_id 18 | end 19 | end 20 | 21 | describe '#parameters' do 22 | subject { path_item } 23 | 24 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 25 | let(:paths) { root.paths } 26 | let(:path_item) { paths.path['/animals/{id}'] } 27 | 28 | it do 29 | expect(subject.parameters.size).to eq 2 30 | expect(subject.parameters.first.name).to eq 'id' 31 | end 32 | end 33 | 34 | describe '#get' do 35 | subject { path_item.get } 36 | 37 | let(:paths) { root.paths } 38 | let(:path_item) { paths.path['/characters'] } 39 | 40 | it do 41 | expect(subject).not_to eq nil 42 | expect(subject.kind_of?(OpenAPIParser::Schemas::Operation)).to eq true 43 | expect(subject.object_reference).to eq '#/paths/~1characters/get' 44 | end 45 | end 46 | 47 | describe '#post' do 48 | subject { path_item.post } 49 | 50 | let(:paths) { root.paths } 51 | let(:path_item) { paths.path['/characters'] } 52 | 53 | it do 54 | expect(subject).not_to eq nil 55 | expect(subject.kind_of?(OpenAPIParser::Schemas::Operation)).to eq true 56 | end 57 | end 58 | 59 | describe '#head' do 60 | subject { path_item.head } 61 | 62 | let(:paths) { root.paths } 63 | let(:path_item) { paths.path['/characters'] } 64 | 65 | it do 66 | expect(subject).to eq nil # head is null 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/paths_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Paths do 4 | let(:root) { OpenAPIParser.parse(normal_schema, {}) } 5 | 6 | describe 'correct init' do 7 | subject { root.paths } 8 | 9 | it do 10 | expect(subject).not_to be nil 11 | expect(subject.object_reference).to eq '#/paths' 12 | expect(subject.root.object_id).to be root.object_id 13 | end 14 | end 15 | 16 | describe 'invalid schema' do 17 | subject { root.paths } 18 | 19 | let(:invalid_schema) do 20 | data = normal_schema 21 | data.delete 'paths' 22 | data 23 | end 24 | let(:root) { OpenAPIParser.parse(invalid_schema, {}) } 25 | 26 | it { expect(subject).to eq nil } 27 | end 28 | 29 | describe '#path' do 30 | subject { paths.path[path_name] } 31 | 32 | let(:paths) { root.paths } 33 | let(:path_name) { '/characters' } 34 | 35 | it do 36 | expect(subject).not_to eq nil 37 | expect(subject.kind_of?(OpenAPIParser::Schemas::PathItem)).to eq true 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/polymorphism_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::RequestBody do 4 | let(:content_type) { 'application/json' } 5 | let(:http_method) { :post } 6 | let(:request_path) { '/pet' } 7 | let(:request_operation) { root.request_operation(http_method, request_path) } 8 | let(:params) { {} } 9 | 10 | context 'whith discriminator with mapping' do 11 | let(:root) { OpenAPIParser.parse(petstore_with_mapped_polymorphism_schema, {}) } 12 | 13 | context 'with valid body' do 14 | let(:body) do 15 | { 16 | "petType" => "Cat", 17 | "name" => "Mr. Cat", 18 | "huntingSkill" => "lazy" 19 | } 20 | end 21 | 22 | it 'picks correct object based on mapping and succeeds' do 23 | request_operation.validate_request_body(content_type, body) 24 | end 25 | end 26 | 27 | context 'with body missing required value' do 28 | let(:body) do 29 | { 30 | "petType" => "tinyLion", 31 | "name" => "Mr. Cat" 32 | } 33 | 34 | end 35 | 36 | it 'picks correct object based on mapping and fails' do 37 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 38 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 39 | expect(e.message).to end_with("missing required parameters: huntingSkill") 40 | end 41 | end 42 | end 43 | 44 | context 'with body containing unresolvable discriminator mapping' do 45 | let(:body) do 46 | { 47 | "petType" => "coolCow", 48 | "name" => "Ms. Cow" 49 | } 50 | end 51 | 52 | it "throws error" do 53 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 54 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorMappedSchema)).to eq true 55 | expect(e.message).to match("^discriminator mapped schema #/components/schemas/coolCow does not exist.*?$") 56 | end 57 | end 58 | end 59 | 60 | context 'with body missing discriminator propertyName' do 61 | let(:body) do 62 | { 63 | "name" => "Mr. Cat", 64 | "huntingSkill" => "lazy" 65 | } 66 | end 67 | 68 | it "throws error if discriminator propertyName is not present on object" do 69 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 70 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorPropertyName)).to eq true 71 | expect(e.message).to match("^discriminator propertyName petType does not exist in value.*?$") 72 | end 73 | end 74 | end 75 | end 76 | 77 | describe 'discriminator without mapping' do 78 | let(:root) { OpenAPIParser.parse(petstore_with_polymorphism_schema, {}) } 79 | 80 | context 'with valid body' do 81 | let(:body) do 82 | { 83 | "petType" => "Cat", 84 | "name" => "Mr. Cat", 85 | "huntingSkill" => "lazy" 86 | } 87 | end 88 | 89 | it 'picks correct object based on mapping and succeeds' do 90 | request_operation.validate_request_body(content_type, body) 91 | end 92 | end 93 | 94 | context 'with body missing required value' do 95 | let(:body) do 96 | { 97 | "petType" => "Cat", 98 | "name" => "Mr. Cat" 99 | } 100 | 101 | end 102 | 103 | it 'picks correct object based on mapping and fails' do 104 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 105 | expect(e).to be_kind_of(OpenAPIParser::NotExistRequiredKey) 106 | expect(e.message).to end_with("missing required parameters: huntingSkill") 107 | end 108 | end 109 | end 110 | 111 | context 'with body containing unresolvable discriminator mapping' do 112 | let(:body) do 113 | { 114 | "petType" => "Cow", 115 | "name" => "Ms. Cow" 116 | } 117 | end 118 | 119 | it "throws error" do 120 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 121 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorMappedSchema)).to eq true 122 | expect(e.message).to match("^discriminator mapped schema #/components/schemas/Cow does not exist.*?$") 123 | end 124 | end 125 | end 126 | 127 | context 'with body missing discriminator propertyName' do 128 | let(:body) do 129 | { 130 | "name" => "Mr. Cat", 131 | "huntingSkill" => "lazy" 132 | } 133 | end 134 | 135 | it "throws error if discriminator propertyName is not present on object" do 136 | expect { request_operation.validate_request_body(content_type, body) }.to raise_error do |e| 137 | expect(e.kind_of?(OpenAPIParser::NotExistDiscriminatorPropertyName)).to eq true 138 | expect(e.message).to match("^discriminator propertyName petType does not exist in value.*?$") 139 | end 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/reference_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Reference do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, { expand_reference: false }) } 5 | 6 | describe 'correct init' do 7 | subject { operation.parameters[2] } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | let(:operation) { path_item.get } 12 | 13 | it do 14 | expect(subject).not_to be nil 15 | expect(subject.class).to eq OpenAPIParser::Schemas::Reference 16 | expect(subject.object_reference).to eq '#/paths/~1pets/get/parameters/2' 17 | expect(subject.ref).to eq '#/components/parameters/test' 18 | expect(subject.root.object_id).to be root.object_id 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/request_body_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::RequestBody do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'correct init' do 7 | subject { operation.request_body } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | let(:operation) { path_item.post } 12 | 13 | it do 14 | expect(subject).not_to be nil 15 | expect(subject.object_reference).to eq '#/paths/~1pets/post/requestBody' 16 | expect(subject.description).to eq 'Pet to add to the store' 17 | expect(subject.required).to eq true 18 | expect(subject.content.class).to eq Hash 19 | expect(subject.root.object_id).to be root.object_id 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/response_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Response do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, {}) } 5 | 6 | describe 'correct init' do 7 | subject { responses.default } 8 | 9 | let(:paths) { root.paths } 10 | let(:path_item) { paths.path['/pets'] } 11 | let(:operation) { path_item.get } 12 | let(:responses) { operation.responses } 13 | 14 | it do 15 | expect(subject.class).to eq OpenAPIParser::Schemas::Response 16 | expect(subject.object_reference).to eq '#/paths/~1pets/get/responses/default' 17 | expect(subject.root.object_id).to be root.object_id 18 | 19 | expect(subject.description).to eq 'unexpected error' 20 | expect(subject.content.class).to eq Hash 21 | 22 | expect(subject.content['application/json'].class).to eq OpenAPIParser::Schemas::MediaType 23 | expect(subject.content['application/json'].object_reference).to eq '#/paths/~1pets/get/responses/default/content/application~1json' 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/openapi_parser/schemas/schema_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | RSpec.describe OpenAPIParser::Schemas::Schema do 4 | let(:root) { OpenAPIParser.parse(petstore_schema, { expand_reference: false }) } 5 | 6 | describe 'correct init' do 7 | let(:paths) { root.paths } 8 | let(:path_item) { paths.path['/pets'] } 9 | let(:operation) { path_item.get } 10 | 11 | it do 12 | parameters = operation.parameters 13 | s = parameters.first.schema 14 | # @type OpenAPIParser::Schemas::Schema s 15 | 16 | expect(s).not_to be nil 17 | expect(s.kind_of?(OpenAPIParser::Schemas::Schema)).to eq true 18 | expect(s.root.object_id).to be root.object_id 19 | expect(s.object_reference).to eq '#/paths/~1pets/get/parameters/0/schema' 20 | expect(s.type).to eq 'array' 21 | expect(s.items.class).to eq OpenAPIParser::Schemas::Schema 22 | expect(s.additional_properties.class).to eq OpenAPIParser::Schemas::Schema 23 | 24 | s = parameters[1].schema 25 | expect(s.format).to eq 'int32' 26 | expect(s.default).to eq 1 27 | 28 | s = parameters[3].schema 29 | expect(s.all_of.size).to eq 2 30 | expect(s.all_of[0].class).to eq OpenAPIParser::Schemas::Reference 31 | expect(s.all_of[1].class).to eq OpenAPIParser::Schemas::Schema 32 | 33 | expect(s.properties.class).to eq Hash 34 | expect(s.properties['pop'].class).to eq OpenAPIParser::Schemas::Schema 35 | expect(s.properties['pop'].type).to eq 'string' 36 | expect(s.properties['pop'].read_only).to eq true 37 | expect(s.properties['pop'].example).to eq 'test' 38 | expect(s.properties['pop'].deprecated).to eq true 39 | expect(s.properties['int'].write_only).to eq true 40 | expect(s.additional_properties).to eq true 41 | expect(s.description).to eq 'desc' 42 | expect(s.nullable).to eq true 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/openapi_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative './spec_helper' 2 | require 'uri' 3 | require 'json' 4 | require 'yaml' 5 | require 'pathname' 6 | 7 | RSpec.describe OpenAPIParser do 8 | it 'has a version number' do 9 | expect(OpenAPIParser::VERSION).not_to be nil 10 | end 11 | 12 | describe 'parse' do 13 | it do 14 | parsed = OpenAPIParser.parse(petstore_schema) 15 | root = OpenAPIParser.parse(petstore_schema, {}) 16 | expect(parsed.openapi).to eq root.openapi 17 | end 18 | end 19 | 20 | describe 'parse_with_filepath' do 21 | it 'loads correct schema' do 22 | parsed = OpenAPIParser.parse_with_filepath(petstore_schema, petstore_schema_path, {}) 23 | root = OpenAPIParser.parse(petstore_schema, {}) 24 | expect(parsed.openapi).to eq root.openapi 25 | end 26 | 27 | it 'sets @uri' do 28 | parsed = OpenAPIParser.parse_with_filepath(petstore_schema, petstore_schema_path, {}) 29 | schema_path = (Pathname.getwd + petstore_schema_path).to_s 30 | expect(parsed.instance_variable_get(:@uri).scheme).to eq "file" 31 | expect(parsed.instance_variable_get(:@uri).path).to eq schema_path 32 | end 33 | 34 | it 'does not set @uri when passed filepath is nil' do 35 | parsed = OpenAPIParser.parse_with_filepath(petstore_schema, nil, {}) 36 | expect(parsed.instance_variable_get(:@uri)).to be nil 37 | end 38 | end 39 | 40 | describe 'load' do 41 | let(:loaded) { OpenAPIParser.load(petstore_schema_path, {}) } 42 | 43 | it 'loads correct schema' do 44 | root = OpenAPIParser.parse(petstore_schema, {}) 45 | expect(loaded.openapi).to eq root.openapi 46 | end 47 | 48 | it 'sets @uri' do 49 | schema_path = (Pathname.getwd + petstore_schema_path).to_s 50 | expect(loaded.instance_variable_get(:@uri).scheme).to eq "file" 51 | expect(loaded.instance_variable_get(:@uri).path).to eq schema_path 52 | end 53 | end 54 | 55 | describe 'load_uri' do 56 | context 'with yaml extension' do 57 | let(:schema_registry) { {} } 58 | let(:uri) { URI.join("file:///", (Pathname.getwd + petstore_schema_path).to_s) } 59 | let!(:loaded) { OpenAPIParser.load_uri(uri, config: OpenAPIParser::Config.new({}), schema_registry: schema_registry) } 60 | 61 | it 'loads correct schema' do 62 | root = OpenAPIParser.parse(petstore_schema, {}) 63 | expect(loaded.openapi).to eq root.openapi 64 | end 65 | 66 | it 'sets @uri' do 67 | expect(loaded.instance_variable_get(:@uri)).to eq uri 68 | end 69 | 70 | it 'registers schema in schema_registry' do 71 | expect(schema_registry).to eq({ uri => loaded }) 72 | end 73 | end 74 | 75 | context 'with json extension' do 76 | let(:schema_registry) { {} } 77 | let(:uri) { URI.join("file:///", (Pathname.getwd + json_petstore_schema_path).to_s) } 78 | let!(:loaded) { OpenAPIParser.load_uri(uri, config: OpenAPIParser::Config.new({}), schema_registry: schema_registry) } 79 | 80 | it 'loads correct schema' do 81 | root = OpenAPIParser.parse(JSON.parse(IO.read(json_petstore_schema_path)), {}) 82 | expect(loaded.openapi).to eq root.openapi 83 | end 84 | 85 | it 'sets @uri' do 86 | expect(loaded.instance_variable_get(:@uri)).to eq uri 87 | end 88 | 89 | it 'registers schema in schema_registry' do 90 | expect(schema_registry).to eq({ uri => loaded }) 91 | end 92 | end 93 | 94 | context 'with yaml content and unsupported extension' do 95 | let(:schema_registry) { {} } 96 | let(:uri) { URI.join("file:///", (Pathname.getwd + yaml_with_unsupported_extension_petstore_schema_path).to_s) } 97 | let!(:loaded) { OpenAPIParser.load_uri(uri, config: OpenAPIParser::Config.new({}), schema_registry: schema_registry) } 98 | 99 | it 'loads correct schema' do 100 | root = OpenAPIParser.parse(YAML.load_file(yaml_with_unsupported_extension_petstore_schema_path), {}) 101 | expect(loaded.openapi).to eq root.openapi 102 | end 103 | 104 | it 'sets @uri' do 105 | expect(loaded.instance_variable_get(:@uri)).to eq uri 106 | end 107 | 108 | it 'registers schema in schema_registry' do 109 | expect(schema_registry).to eq({ uri => loaded }) 110 | end 111 | end 112 | 113 | context 'with json content and unsupported extension' do 114 | let(:schema_registry) { {} } 115 | let(:uri) { URI.join("file:///", (Pathname.getwd + json_with_unsupported_extension_petstore_schema_path).to_s) } 116 | let!(:loaded) { OpenAPIParser.load_uri(uri, config: OpenAPIParser::Config.new({}), schema_registry: schema_registry) } 117 | 118 | it 'loads correct schema' do 119 | root = OpenAPIParser.parse(JSON.parse(IO.read(json_with_unsupported_extension_petstore_schema_path)), {}) 120 | expect(loaded.openapi).to eq root.openapi 121 | end 122 | 123 | it 'sets @uri' do 124 | expect(loaded.instance_variable_get(:@uri)).to eq uri 125 | end 126 | 127 | it 'registers schema in schema_registry' do 128 | expect(schema_registry).to eq({ uri => loaded }) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("3.2.0") 3 | require 'pry' 4 | end 5 | require 'rspec-parameterized' 6 | require 'simplecov' 7 | require 'psych' 8 | 9 | SimpleCov.start do 10 | add_filter 'spec' 11 | end 12 | 13 | SimpleCov.minimum_coverage 99 14 | 15 | require 'openapi_parser' 16 | 17 | RSpec.configure do |config| 18 | # Enable flags like --only-failures and --next-failure 19 | config.example_status_persistence_file_path = '.rspec_status' 20 | 21 | # Disable RSpec exposing methods globally on `Module` and `main` 22 | config.disable_monkey_patching! 23 | 24 | config.expect_with :rspec do |c| 25 | c.syntax = :expect 26 | end 27 | end 28 | 29 | def load_yaml_file(path) 30 | Psych.safe_load(open(path).read, permitted_classes: [Date, Time]) 31 | end 32 | 33 | def normal_schema 34 | load_yaml_file('./spec/data/normal.yml') 35 | end 36 | 37 | def broken_reference_schema 38 | load_yaml_file('./spec/data/reference-broken.yaml') 39 | end 40 | 41 | def petstore_schema 42 | load_yaml_file(petstore_schema_path) 43 | end 44 | 45 | def petstore_schema_path 46 | './spec/data/petstore-expanded.yaml' 47 | end 48 | 49 | def petstore_with_discriminator_schema 50 | load_yaml_file('./spec/data/petstore-with-discriminator.yaml') 51 | end 52 | 53 | def petstore_with_mapped_polymorphism_schema 54 | load_yaml_file('./spec/data/petstore-with-mapped-polymorphism.yaml') 55 | end 56 | 57 | def petstore_with_polymorphism_schema 58 | load_yaml_file('./spec/data/petstore-with-polymorphism.yaml') 59 | end 60 | 61 | def json_petstore_schema_path 62 | './spec/data/petstore.json' 63 | end 64 | 65 | def json_with_unsupported_extension_petstore_schema_path 66 | './spec/data/petstore.json.unsupported_extension' 67 | end 68 | 69 | def yaml_with_unsupported_extension_petstore_schema_path 70 | './spec/data/petstore.yaml.unsupported_extension' 71 | end 72 | 73 | def path_item_ref_schema_path 74 | './spec/data/path-item-ref.yaml' 75 | end 76 | 77 | def path_item_ref_schema 78 | load_yaml_file(path_item_ref_schema_path) 79 | end 80 | 81 | def build_validate_test_schema(new_properties) 82 | b = load_yaml_file('./spec/data/validate_test.yaml') 83 | obj = b['paths']['/validate_test']['post']['requestBody']['content']['application/json']['schema']['properties'] 84 | obj.merge!(change_string_key(new_properties)) 85 | b 86 | end 87 | 88 | def change_string_key(hash) 89 | new_data = hash.map do |k, v| 90 | if v.kind_of?(Hash) 91 | [k.to_s, change_string_key(v)] 92 | elsif v.kind_of?(Array) 93 | [k.to_s, v.map { |child| change_string_key(child) }] 94 | else 95 | [k.to_s, v] 96 | end 97 | end 98 | 99 | new_data.to_h 100 | end 101 | --------------------------------------------------------------------------------