├── .coveralls.yml ├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── danger.yml │ ├── edge.yml │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .simplecov ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dangerfile ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── RELEASING.md ├── Rakefile ├── SECURITY.md ├── UPGRADING.md ├── benchmark ├── compile_many_routes.rb ├── large_model.rb ├── nested_params.rb ├── remounting.rb ├── resource │ └── vrp_example.json └── simple.rb ├── docker-compose.yml ├── docker ├── Dockerfile └── entrypoint.sh ├── gemfiles ├── dry_validation.gemfile ├── grape_entity.gemfile ├── hashie.gemfile ├── multi_json.gemfile ├── multi_xml.gemfile ├── rack_2_0.gemfile ├── rack_3_0.gemfile ├── rack_3_1.gemfile ├── rack_edge.gemfile ├── rails_6_1.gemfile ├── rails_7_0.gemfile ├── rails_7_1.gemfile ├── rails_7_2.gemfile ├── rails_8_0.gemfile └── rails_edge.gemfile ├── grape.gemspec ├── grape.png ├── lib ├── grape.rb └── grape │ ├── api.rb │ ├── api │ ├── helpers.rb │ └── instance.rb │ ├── content_types.rb │ ├── cookies.rb │ ├── dry_types.rb │ ├── dsl │ ├── api.rb │ ├── callbacks.rb │ ├── configuration.rb │ ├── desc.rb │ ├── headers.rb │ ├── helpers.rb │ ├── inside_route.rb │ ├── logger.rb │ ├── middleware.rb │ ├── parameters.rb │ ├── request_response.rb │ ├── routing.rb │ ├── settings.rb │ └── validations.rb │ ├── endpoint.rb │ ├── env.rb │ ├── error_formatter.rb │ ├── error_formatter │ ├── base.rb │ ├── json.rb │ ├── serializable_hash.rb │ ├── txt.rb │ └── xml.rb │ ├── exceptions │ ├── base.rb │ ├── conflicting_types.rb │ ├── empty_message_body.rb │ ├── incompatible_option_values.rb │ ├── invalid_accept_header.rb │ ├── invalid_formatter.rb │ ├── invalid_message_body.rb │ ├── invalid_parameters.rb │ ├── invalid_response.rb │ ├── invalid_version_header.rb │ ├── invalid_versioner_option.rb │ ├── invalid_with_option_for_represent.rb │ ├── method_not_allowed.rb │ ├── missing_group_type.rb │ ├── missing_mime_type.rb │ ├── missing_option.rb │ ├── missing_vendor_option.rb │ ├── too_deep_parameters.rb │ ├── too_many_multipart_files.rb │ ├── unknown_auth_strategy.rb │ ├── unknown_options.rb │ ├── unknown_parameter.rb │ ├── unknown_params_builder.rb │ ├── unknown_validator.rb │ ├── unsupported_group_type.rb │ ├── validation.rb │ ├── validation_array_errors.rb │ └── validation_errors.rb │ ├── extensions │ ├── active_support │ │ └── hash_with_indifferent_access.rb │ ├── hash.rb │ └── hashie │ │ └── mash.rb │ ├── formatter.rb │ ├── formatter │ ├── base.rb │ ├── json.rb │ ├── serializable_hash.rb │ ├── txt.rb │ └── xml.rb │ ├── json.rb │ ├── locale │ └── en.yml │ ├── middleware │ ├── auth │ │ ├── base.rb │ │ ├── dsl.rb │ │ ├── strategies.rb │ │ └── strategy_info.rb │ ├── base.rb │ ├── error.rb │ ├── filter.rb │ ├── formatter.rb │ ├── globals.rb │ ├── stack.rb │ ├── versioner.rb │ └── versioner │ │ ├── accept_version_header.rb │ │ ├── base.rb │ │ ├── header.rb │ │ ├── param.rb │ │ └── path.rb │ ├── namespace.rb │ ├── params_builder.rb │ ├── params_builder │ ├── base.rb │ ├── hash.rb │ ├── hash_with_indifferent_access.rb │ └── hashie_mash.rb │ ├── parser.rb │ ├── parser │ ├── base.rb │ ├── json.rb │ └── xml.rb │ ├── path.rb │ ├── presenters │ └── presenter.rb │ ├── railtie.rb │ ├── request.rb │ ├── router.rb │ ├── router │ ├── base_route.rb │ ├── greedy_route.rb │ ├── pattern.rb │ └── route.rb │ ├── serve_stream │ ├── file_body.rb │ ├── sendfile_response.rb │ └── stream_response.rb │ ├── types │ └── invalid_value.rb │ ├── util │ ├── base_inheritable.rb │ ├── cache.rb │ ├── endpoint_configuration.rb │ ├── header.rb │ ├── inheritable_setting.rb │ ├── inheritable_values.rb │ ├── lazy │ │ ├── block.rb │ │ ├── value.rb │ │ ├── value_array.rb │ │ ├── value_enumerable.rb │ │ └── value_hash.rb │ ├── media_type.rb │ ├── registry.rb │ ├── reverse_stackable_values.rb │ ├── stackable_values.rb │ └── strict_hash_configuration.rb │ ├── validations.rb │ ├── validations │ ├── attributes_doc.rb │ ├── attributes_iterator.rb │ ├── contract_scope.rb │ ├── multiple_attributes_iterator.rb │ ├── params_scope.rb │ ├── single_attribute_iterator.rb │ ├── types.rb │ ├── types │ │ ├── array_coercer.rb │ │ ├── custom_type_coercer.rb │ │ ├── custom_type_collection_coercer.rb │ │ ├── dry_type_coercer.rb │ │ ├── file.rb │ │ ├── invalid_value.rb │ │ ├── json.rb │ │ ├── multiple_type_coercer.rb │ │ ├── primitive_coercer.rb │ │ ├── set_coercer.rb │ │ └── variant_collection_coercer.rb │ ├── validator_factory.rb │ └── validators │ │ ├── all_or_none_of_validator.rb │ │ ├── allow_blank_validator.rb │ │ ├── as_validator.rb │ │ ├── at_least_one_of_validator.rb │ │ ├── base.rb │ │ ├── coerce_validator.rb │ │ ├── contract_scope_validator.rb │ │ ├── default_validator.rb │ │ ├── exactly_one_of_validator.rb │ │ ├── except_values_validator.rb │ │ ├── length_validator.rb │ │ ├── multiple_params_base.rb │ │ ├── mutual_exclusion_validator.rb │ │ ├── presence_validator.rb │ │ ├── regexp_validator.rb │ │ ├── same_as_validator.rb │ │ └── values_validator.rb │ ├── version.rb │ └── xml.rb └── spec ├── config └── spec_test_prof.rb ├── grape ├── api │ ├── custom_validations_spec.rb │ ├── deeply_included_options_spec.rb │ ├── defines_boolean_in_params_spec.rb │ ├── documentation_spec.rb │ ├── inherited_helpers_spec.rb │ ├── instance_spec.rb │ ├── invalid_format_spec.rb │ ├── mount_and_helpers_order_spec.rb │ ├── mount_and_rescue_from_spec.rb │ ├── namespace_parameters_in_route_spec.rb │ ├── nested_helpers_spec.rb │ ├── optional_parameters_in_route_spec.rb │ ├── parameters_modification_spec.rb │ ├── patch_method_helpers_spec.rb │ ├── recognize_path_spec.rb │ ├── required_parameters_in_route_spec.rb │ ├── required_parameters_with_invalid_method_spec.rb │ ├── routes_with_requirements_spec.rb │ ├── shared_helpers_exactly_one_of_spec.rb │ └── shared_helpers_spec.rb ├── api_remount_spec.rb ├── api_spec.rb ├── content_types_spec.rb ├── dsl │ ├── callbacks_spec.rb │ ├── desc_spec.rb │ ├── headers_spec.rb │ ├── helpers_spec.rb │ ├── inside_route_spec.rb │ ├── logger_spec.rb │ ├── middleware_spec.rb │ ├── parameters_spec.rb │ ├── request_response_spec.rb │ ├── routing_spec.rb │ └── settings_spec.rb ├── endpoint │ └── declared_spec.rb ├── endpoint_spec.rb ├── exceptions │ ├── base_spec.rb │ ├── body_parse_errors_spec.rb │ ├── invalid_accept_header_spec.rb │ ├── invalid_formatter_spec.rb │ ├── invalid_response_spec.rb │ ├── invalid_versioner_option_spec.rb │ ├── missing_group_type_spec.rb │ ├── missing_mime_type_spec.rb │ ├── missing_option_spec.rb │ ├── unknown_options_spec.rb │ ├── unknown_validator_spec.rb │ ├── unsupported_group_type_spec.rb │ ├── validation_errors_spec.rb │ └── validation_spec.rb ├── extensions │ └── param_builders │ │ ├── hash_spec.rb │ │ └── hash_with_indifferent_access_spec.rb ├── integration │ ├── global_namespace_function_spec.rb │ ├── rack_sendfile_spec.rb │ └── rack_spec.rb ├── loading_spec.rb ├── middleware │ ├── auth │ │ ├── base_spec.rb │ │ ├── dsl_spec.rb │ │ └── strategies_spec.rb │ ├── base_spec.rb │ ├── error_spec.rb │ ├── exception_spec.rb │ ├── formatter_spec.rb │ ├── globals_spec.rb │ ├── stack_spec.rb │ ├── versioner │ │ ├── accept_version_header_spec.rb │ │ ├── header_spec.rb │ │ ├── param_spec.rb │ │ └── path_spec.rb │ └── versioner_spec.rb ├── named_api_spec.rb ├── params_builder │ ├── hash_spec.rb │ └── hash_with_indifferent_access_spec.rb ├── parser_spec.rb ├── path_spec.rb ├── presenters │ └── presenter_spec.rb ├── request_spec.rb ├── router │ └── greedy_route_spec.rb ├── router_spec.rb ├── util │ ├── inheritable_setting_spec.rb │ ├── inheritable_values_spec.rb │ ├── media_type_spec.rb │ ├── reverse_stackable_values_spec.rb │ ├── stackable_values_spec.rb │ └── strict_hash_configuration_spec.rb ├── validations │ ├── attributes_doc_spec.rb │ ├── multiple_attributes_iterator_spec.rb │ ├── params_scope_spec.rb │ ├── single_attribute_iterator_spec.rb │ ├── types │ │ ├── array_coercer_spec.rb │ │ ├── primitive_coercer_spec.rb │ │ └── set_coercer_spec.rb │ ├── types_spec.rb │ └── validators │ │ ├── all_or_none_validator_spec.rb │ │ ├── allow_blank_validator_spec.rb │ │ ├── at_least_one_of_validator_spec.rb │ │ ├── coerce_validator_spec.rb │ │ ├── contract_scope_validator_spec.rb │ │ ├── default_validator_spec.rb │ │ ├── exactly_one_of_validator_spec.rb │ │ ├── except_values_validator_spec.rb │ │ ├── length_validator_spec.rb │ │ ├── mutual_exclusion_validator_spec.rb │ │ ├── presence_validator_spec.rb │ │ ├── regexp_validator_spec.rb │ │ ├── same_as_validator_spec.rb │ │ ├── values_validator_spec.rb │ │ └── zh-CN.yml └── validations_spec.rb ├── integration ├── dry_validation │ └── dry_validation_spec.rb ├── grape_entity │ └── entity_spec.rb ├── hashie │ └── hashie_spec.rb ├── multi_json │ └── json_spec.rb ├── multi_xml │ └── xml_spec.rb ├── rack_3_0 │ ├── headers_spec.rb │ └── version_spec.rb └── rails │ ├── mounting_spec.rb │ └── railtie_spec.rb ├── shared ├── deprecated_class_examples.rb └── versioning_examples.rb ├── spec_helper.rb └── support ├── basic_auth_encode_helpers.rb ├── chunked_response.rb ├── content_type_helpers.rb ├── cookie_jar.rb ├── deprecated_warning_handlers.rb ├── deregister.rb ├── endpoint_faker.rb ├── file_streamer.rb ├── integer_helpers.rb └── versioned_helpers.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: github 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | .com.apple.timemachine.supported 4 | 5 | ## TEXTMATE 6 | *.tmproj 7 | tmtags 8 | 9 | ## EMACS 10 | *~ 11 | \#* 12 | .\#* 13 | 14 | ## REDCAR 15 | .redcar 16 | 17 | ## VIM 18 | *.swp 19 | *.swo 20 | 21 | ## RUBYMINE 22 | .idea 23 | 24 | ## PROJECT::GENERAL 25 | coverage 26 | doc 27 | pkg 28 | .rvmrc 29 | .ruby-version 30 | .ruby-gemset 31 | .rspec_status 32 | .bundle 33 | .byebug_history 34 | dist 35 | Gemfile.lock 36 | gemfiles/*.lock 37 | tmp 38 | .yardoc 39 | 40 | ## Rubinius 41 | .rbx 42 | 43 | ## Bundler binstubs 44 | bin 45 | 46 | ## ripper-tags and gem-ctags 47 | tags 48 | 49 | ## PROJECT::SPECIFIC 50 | .project -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: "rubygems/grape" 2 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: danger 3 | on: pull_request 4 | 5 | jobs: 6 | danger: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 100 12 | - name: Set up Ruby 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 3.4 16 | bundler-cache: true 17 | - name: Run Danger 18 | run: | 19 | # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok 20 | TOKEN=$(echo -n Z2hwX2lYb0dPNXNyejYzOFJyaTV3QUxUdkNiS1dtblFwZTFuRXpmMwo= | base64 --decode) 21 | DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose 22 | -------------------------------------------------------------------------------- /.github/workflows/edge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: edge 3 | on: workflow_dispatch 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', ruby-head, truffleruby-head, jruby-head] 10 | gemfile: [rails_edge, rack_edge] 11 | exclude: 12 | - ruby: '2.7' 13 | gemfile: rails_edge 14 | - ruby: '3.0' 15 | gemfile: rails_edge 16 | runs-on: ubuntu-latest 17 | continue-on-error: true 18 | env: 19 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | 29 | - name: Run tests 30 | run: "RUBYOPT='--enable=frozen-string-literal' bundle exec rspec" 31 | 32 | - name: Coveralls 33 | uses: coverallsapp/github-action@v2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} 37 | parallel: true 38 | 39 | finish: 40 | needs: test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Coveralls Finished 44 | uses: coverallsapp/github-action@v2 45 | with: 46 | github-token: ${{ secrets.github_token }} 47 | parallel-finished: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | .com.apple.timemachine.supported 4 | 5 | ## TEXTMATE 6 | *.tmproj 7 | tmtags 8 | 9 | ## EMACS 10 | *~ 11 | \#* 12 | .\#* 13 | 14 | ## REDCAR 15 | .redcar 16 | 17 | ## VIM 18 | *.swp 19 | *.swo 20 | 21 | ## RUBYMINE 22 | .idea 23 | 24 | ## PROJECT::GENERAL 25 | coverage 26 | doc 27 | pkg 28 | .rvmrc 29 | .ruby-version 30 | .ruby-gemset 31 | .rspec_status 32 | .bundle 33 | .byebug_history 34 | dist 35 | Gemfile.lock 36 | gemfiles/*.lock 37 | tmp 38 | .yardoc 39 | 40 | ## Rubinius 41 | .rbx 42 | 43 | ## Bundler binstubs 44 | bin 45 | 46 | ## ripper-tags and gem-ctags 47 | tags 48 | 49 | ## PROJECT::SPECIFIC 50 | .project 51 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format=documentation 4 | --order=rand 5 | --warnings 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | TargetRubyVersion: 2.7 4 | SuggestExtensions: false 5 | Exclude: 6 | - vendor/**/* 7 | - bin/**/* 8 | 9 | require: 10 | - rubocop-performance 11 | - rubocop-rspec 12 | 13 | inherit_from: .rubocop_todo.yml 14 | 15 | Layout/LineLength: 16 | Max: 215 17 | 18 | Lint/EmptyBlock: 19 | Exclude: 20 | - spec/**/*_spec.rb 21 | 22 | Style/Documentation: 23 | Enabled: false 24 | 25 | Style/MultilineIfModifier: 26 | Enabled: false 27 | 28 | Style/RaiseArgs: 29 | Enabled: false 30 | 31 | Style/RedundantArrayConstructor: 32 | Enabled: false # doesn't work well with params definition 33 | 34 | Metrics/AbcSize: 35 | Max: 45 36 | 37 | Metrics/BlockLength: 38 | Max: 30 39 | Exclude: 40 | - spec/**/*_spec.rb 41 | 42 | Metrics/ClassLength: 43 | Max: 305 44 | 45 | Metrics/CyclomaticComplexity: 46 | Max: 15 47 | 48 | Metrics/ParameterLists: 49 | MaxOptionalParameters: 4 50 | 51 | Metrics/MethodLength: 52 | Max: 32 53 | 54 | Metrics/ModuleLength: 55 | Max: 220 56 | 57 | Metrics/PerceivedComplexity: 58 | Max: 15 59 | 60 | RSpec/ExampleLength: 61 | Max: 60 62 | 63 | RSpec/NestedGroups: 64 | Max: 6 65 | 66 | RSpec/SpecFilePathFormat: 67 | Enabled: false 68 | 69 | RSpec/SpecFilePathSuffix: 70 | Enabled: true 71 | 72 | RSpec/MultipleExpectations: 73 | Enabled: false 74 | 75 | RSpec/NamedSubject: 76 | Enabled: false 77 | 78 | RSpec/MultipleMemoizedHelpers: 79 | Max: 11 80 | 81 | RSpec/ContextWording: 82 | Enabled: false 83 | 84 | RSpec/MessageSpies: 85 | EnforcedStyle: receive 86 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['GITHUB_USER'] # only when running CI 4 | require 'simplecov-lcov' 5 | SimpleCov::Formatter::LcovFormatter.config do |c| 6 | c.report_with_single_file = true 7 | c.single_report_path = 'coverage/lcov.info' 8 | end 9 | 10 | SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter 11 | end 12 | 13 | SimpleCov.start do 14 | add_filter '/spec/' 15 | end 16 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --asset grape.png 2 | --markup markdown 3 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | danger.import_dangerfile(gem: 'ruby-grape-danger') 4 | toc.check! 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # when changing this file, run appraisal install ; rubocop -a gemfiles/*.gemfile 4 | 5 | source('https://rubygems.org') 6 | 7 | gemspec 8 | 9 | group :development, :test do 10 | gem 'builder', require: false 11 | gem 'bundler' 12 | gem 'rake' 13 | gem 'rubocop', '1.71.2', require: false 14 | gem 'rubocop-performance', '1.23.1', require: false 15 | gem 'rubocop-rspec', '3.4.0', require: false 16 | end 17 | 18 | group :development do 19 | gem 'benchmark-ips' 20 | gem 'benchmark-memory' 21 | gem 'guard' 22 | gem 'guard-rspec' 23 | gem 'guard-rubocop' 24 | end 25 | 26 | group :test do 27 | gem 'rack-contrib', require: false 28 | gem 'rack-test', '~> 2.1' 29 | gem 'rspec', '~> 3.13' 30 | gem 'ruby-grape-danger', '~> 0.2', require: false 31 | gem 'simplecov', '~> 0.21', require: false 32 | gem 'simplecov-lcov', '~> 0.8', require: false 33 | gem 'test-prof', require: false 34 | end 35 | 36 | platforms :jruby do 37 | gem 'racc' 38 | end 39 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard :rspec, all_on_start: true, cmd: 'bundle exec rspec' do 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 6 | watch('spec/spec_helper.rb') { 'spec' } 7 | end 8 | 9 | guard :rubocop do 10 | watch(/.+\.rb$/) 11 | watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require('rubygems') 4 | require('bundler') 5 | Bundler.setup(:default, :test, :development) 6 | 7 | Bundler::GemHelper.install_tasks 8 | 9 | require('rspec/core/rake_task') 10 | RSpec::Core::RakeTask.new(:spec) do |spec| 11 | spec.pattern = 'spec/**/*_spec.rb' 12 | spec.exclude_pattern = 'spec/integration/**/*_spec.rb' 13 | end 14 | 15 | RSpec::Core::RakeTask.new(:rcov) do |spec| 16 | spec.pattern = 'spec/**/*_spec.rb' 17 | spec.rcov = true 18 | end 19 | 20 | task(:spec) 21 | 22 | require('rainbow/ext/string') unless String.respond_to?(:color) 23 | 24 | require('rubocop/rake_task') 25 | RuboCop::RakeTask.new 26 | 27 | task(default: %i[rubocop spec]) 28 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Version 1.2.0 or newer is currently supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Tidelift acts as the security contact for this open-source project. To make a report, please email the security team at security@tidelift.com. See [tidelift.com/security](https://tidelift.com/security) for details and more options. 10 | 11 | -------------------------------------------------------------------------------- /benchmark/compile_many_routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require 'grape' 5 | require 'benchmark/ips' 6 | 7 | class API < Grape::API 8 | prefix :api 9 | version 'v1', using: :path 10 | 11 | 2000.times do |index| 12 | get "/test#{index}/" do 13 | 'hello' 14 | end 15 | end 16 | end 17 | 18 | Benchmark.ips do |ips| 19 | ips.report('Compiling 2000 routes') do 20 | API.compile! 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /benchmark/nested_params.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require 'grape' 5 | require 'benchmark/ips' 6 | 7 | class API < Grape::API 8 | prefix :api 9 | version 'v1', using: :path 10 | 11 | params do 12 | requires :address, type: Hash do 13 | requires :street, type: String 14 | requires :postal_code, type: Integer 15 | optional :city, type: String 16 | end 17 | end 18 | post '/' do 19 | 'hello' 20 | end 21 | end 22 | 23 | options = { 24 | method: Rack::POST, 25 | params: { 26 | address: { 27 | street: 'Alexis Pl.', 28 | postal_code: '90210', 29 | city: 'Beverly Hills' 30 | } 31 | } 32 | } 33 | 34 | env = Rack::MockRequest.env_for('/api/v1', options) 35 | 36 | 10.times do |i| 37 | env["HTTP_HEADER#{i}"] = '123' 38 | end 39 | 40 | Benchmark.ips do |ips| 41 | ips.report('POST with nested params') do 42 | API.call env 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /benchmark/remounting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require 'grape' 5 | require 'benchmark/memory' 6 | 7 | class VotingApi < Grape::API 8 | logger Logger.new($stdout) 9 | 10 | helpers do 11 | def logger 12 | VotingApi.logger 13 | end 14 | end 15 | 16 | namespace 'votes' do 17 | get do 18 | logger 19 | end 20 | end 21 | end 22 | 23 | class PostApi < Grape::API 24 | mount VotingApi 25 | end 26 | 27 | class CommentAPI < Grape::API 28 | mount VotingApi 29 | end 30 | 31 | env = Rack::MockRequest.env_for('/votes', method: Rack::GET) 32 | 33 | Benchmark.memory do |api| 34 | calls = 1000 35 | 36 | api.report('using Array') do 37 | VotingApi.instance_variable_set(:@setup, []) 38 | calls.times { PostApi.call(env) } 39 | puts " setup size: #{VotingApi.instance_variable_get(:@setup).size}" 40 | end 41 | 42 | api.report('using Set') do 43 | VotingApi.instance_variable_set(:@setup, Set.new) 44 | calls.times { PostApi.call(env) } 45 | puts " setup size: #{VotingApi.instance_variable_get(:@setup).size}" 46 | end 47 | 48 | api.compare! 49 | end 50 | -------------------------------------------------------------------------------- /benchmark/simple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | require 'grape' 5 | require 'benchmark/ips' 6 | 7 | class API < Grape::API 8 | prefix :api 9 | version 'v1', using: :path 10 | get '/' do 11 | 'hello' 12 | end 13 | end 14 | 15 | options = { 16 | method: Rack::GET 17 | } 18 | 19 | env = Rack::MockRequest.env_for('/api/v1', options) 20 | 21 | 10.times do |i| 22 | env["HTTP_HEADER#{i}"] = '123' 23 | end 24 | 25 | Benchmark.ips do |ips| 26 | ips.report('simple') do 27 | API.call env 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | gems: 5 | 6 | services: 7 | grape: 8 | build: 9 | context: . 10 | dockerfile: docker/Dockerfile 11 | args: 12 | - RUBY_VERSION=${RUBY_VERSION:-3} 13 | stdin_open: true 14 | tty: true 15 | volumes: 16 | - .:/var/grape 17 | - gems:/usr/local/bundle 18 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION=3 2 | FROM ruby:${RUBY_VERSION}-alpine 3 | 4 | ENV BUNDLE_PATH /usr/local/bundle/gems 5 | ENV LIB_PATH /var/grape 6 | ENV RUBYOPT --enable-frozen-string-literal --yjit 7 | ENV LD_PRELOAD libjemalloc.so.2 8 | ENV MALLOC_CONF dirty_decay_ms:1000,narenas:2,background_thread:true 9 | 10 | RUN apk add --update --no-cache make gcc git libc-dev yaml-dev gcompat jemalloc && \ 11 | gem update --system && gem install bundler 12 | 13 | WORKDIR $LIB_PATH 14 | 15 | COPY /docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh 16 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 17 | 18 | ENTRYPOINT ["docker-entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Useful information 6 | echo -e "$(ruby --version)\nrubygems $(gem --version)\n$(bundle version)" 7 | if [ -z "${GEMFILE}" ] 8 | then 9 | echo "Running default Gemfile" 10 | else 11 | export BUNDLE_GEMFILE="./gemfiles/${GEMFILE}.gemfile" 12 | echo "Running gemfile: ${GEMFILE}" 13 | fi 14 | 15 | # Keep gems in the latest possible state 16 | (bundle check || bundle install) && bundle update && exec bundle exec ${@} 17 | -------------------------------------------------------------------------------- /gemfiles/dry_validation.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'dry-validation' 6 | -------------------------------------------------------------------------------- /gemfiles/grape_entity.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'grape-entity' 6 | -------------------------------------------------------------------------------- /gemfiles/hashie.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'hashie' 6 | -------------------------------------------------------------------------------- /gemfiles/multi_json.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem 'multi_json' 4 | 5 | eval_gemfile '../Gemfile' 6 | -------------------------------------------------------------------------------- /gemfiles/multi_xml.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem 'multi_xml' 4 | 5 | eval_gemfile '../Gemfile' 6 | -------------------------------------------------------------------------------- /gemfiles/rack_2_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rack', '~> 2.0' 6 | -------------------------------------------------------------------------------- /gemfiles/rack_3_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rack', '~> 3.0.0' 6 | -------------------------------------------------------------------------------- /gemfiles/rack_3_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rack', '~> 3.1' 6 | -------------------------------------------------------------------------------- /gemfiles/rack_edge.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rack', github: 'rack/rack' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_6_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'mutex_m' 6 | gem 'rails', '~> 6.1' 7 | gem 'tzinfo-data', require: false 8 | -------------------------------------------------------------------------------- /gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'mutex_m' 6 | gem 'rails', '~> 7.0.0' 7 | gem 'tzinfo-data', require: false 8 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rails', '~> 7.1.0' 6 | gem 'tzinfo-data', require: false 7 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rails', '~> 7.2.0' 6 | gem 'tzinfo-data', require: false 7 | -------------------------------------------------------------------------------- /gemfiles/rails_8_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rails', '~> 8.0' 6 | gem 'tzinfo-data', require: false 7 | -------------------------------------------------------------------------------- /gemfiles/rails_edge.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile '../Gemfile' 4 | 5 | gem 'rails', github: 'rails/rails' 6 | gem 'tzinfo-data', require: false 7 | -------------------------------------------------------------------------------- /grape.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 4 | require 'grape/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'grape' 8 | s.version = Grape::VERSION 9 | s.platform = Gem::Platform::RUBY 10 | s.authors = ['Michael Bleigh'] 11 | s.email = ['michael@intridea.com'] 12 | s.homepage = 'https://github.com/ruby-grape/grape' 13 | s.summary = 'A simple Ruby framework for building REST-like APIs.' 14 | s.description = 'A Ruby framework for rapid API development with great conventions.' 15 | s.license = 'MIT' 16 | s.metadata = { 17 | 'bug_tracker_uri' => 'https://github.com/ruby-grape/grape/issues', 18 | 'changelog_uri' => "https://github.com/ruby-grape/grape/blob/v#{s.version}/CHANGELOG.md", 19 | 'documentation_uri' => "https://www.rubydoc.info/gems/grape/#{s.version}", 20 | 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}", 21 | 'rubygems_mfa_required' => 'true' 22 | } 23 | 24 | s.add_dependency 'activesupport', '>= 6.1' 25 | s.add_dependency 'dry-types', '>= 1.1' 26 | s.add_dependency 'mustermann-grape', '~> 1.1.0' 27 | s.add_dependency 'rack', '>= 2' 28 | s.add_dependency 'zeitwerk' 29 | 30 | s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] 31 | s.require_paths = ['lib'] 32 | s.required_ruby_version = '>= 2.7.0' 33 | end 34 | -------------------------------------------------------------------------------- /grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby-grape/grape/14d9f9d86f7dc047912ca21adcbe05bb5605d4de/grape.png -------------------------------------------------------------------------------- /lib/grape/api/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | class API 5 | module Helpers 6 | include Grape::DSL::Helpers::BaseHelper 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/grape/content_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ContentTypes 5 | module_function 6 | 7 | # Content types are listed in order of preference. 8 | DEFAULTS = { 9 | xml: 'application/xml', 10 | serializable_hash: 'application/json', 11 | json: 'application/json', 12 | binary: 'application/octet-stream', 13 | txt: 'text/plain' 14 | }.freeze 15 | 16 | MIME_TYPES = Grape::ContentTypes::DEFAULTS.except(:serializable_hash).invert.freeze 17 | 18 | def content_types_for(from_settings) 19 | from_settings.presence || DEFAULTS 20 | end 21 | 22 | def mime_types_for(from_settings) 23 | return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS 24 | 25 | from_settings.each_with_object({}) do |(k, v), types_without_params| 26 | # remove optional parameter 27 | types_without_params[v.split(';', 2).first] = k 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/grape/cookies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | class Cookies 5 | extend Forwardable 6 | 7 | DELETED_COOKIES_ATTRS = { 8 | max_age: '0', 9 | value: '', 10 | expires: Time.at(0) 11 | }.freeze 12 | 13 | def_delegators :cookies, :[], :each 14 | 15 | def initialize(rack_cookies) 16 | @cookies = rack_cookies 17 | @send_cookies = nil 18 | end 19 | 20 | def response_cookies 21 | return unless @send_cookies 22 | 23 | send_cookies.each do |name| 24 | yield name, cookies[name] 25 | end 26 | end 27 | 28 | def []=(name, value) 29 | cookies[name] = value 30 | send_cookies << name 31 | end 32 | 33 | # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 34 | def delete(name, **opts) 35 | self.[]=(name, opts.merge(DELETED_COOKIES_ATTRS)) 36 | end 37 | 38 | private 39 | 40 | def cookies 41 | return @cookies unless @cookies.is_a?(Proc) 42 | 43 | @cookies = @cookies.call.with_indifferent_access 44 | end 45 | 46 | def send_cookies 47 | @send_cookies ||= Set.new 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/grape/dry_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DryTypes 5 | # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is 6 | # a container in this case. Check documentation for more information 7 | # https://dry-rb.org/gems/dry-types/1.2/getting-started/ 8 | include Dry.Types() 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/grape/dsl/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module API 6 | extend ActiveSupport::Concern 7 | 8 | include Grape::DSL::Validations 9 | include Grape::DSL::Callbacks 10 | include Grape::DSL::Configuration 11 | include Grape::DSL::Helpers 12 | include Grape::DSL::Middleware 13 | include Grape::DSL::RequestResponse 14 | include Grape::DSL::Routing 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape/dsl/callbacks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | # Blocks can be executed before or after every API call, using `before`, `after`, 6 | # `before_validation` and `after_validation`. 7 | # 8 | # Before and after callbacks execute in the following order: 9 | # 10 | # 1. `before` 11 | # 2. `before_validation` 12 | # 3. _validations_ 13 | # 4. `after_validation` 14 | # 5. _the API call_ 15 | # 6. `after` 16 | # 17 | # Steps 4, 5 and 6 only happen if validation succeeds. 18 | module Callbacks 19 | extend ActiveSupport::Concern 20 | 21 | include Grape::DSL::Configuration 22 | 23 | module ClassMethods 24 | # Execute the given block before validation, coercion, or any endpoint 25 | # code is executed. 26 | def before(&block) 27 | namespace_stackable(:befores, block) 28 | end 29 | 30 | # Execute the given block after `before`, but prior to validation or 31 | # coercion. 32 | def before_validation(&block) 33 | namespace_stackable(:before_validations, block) 34 | end 35 | 36 | # Execute the given block after validations and coercions, but before 37 | # any endpoint code. 38 | def after_validation(&block) 39 | namespace_stackable(:after_validations, block) 40 | end 41 | 42 | # Execute the given block after the endpoint code has run. 43 | def after(&block) 44 | namespace_stackable(:afters, block) 45 | end 46 | 47 | # Allows you to specify a something that will always be executed after a call 48 | # API call. Unlike the `after` block, this code will run even on 49 | # unsuccesful requests. 50 | # @example 51 | # class ExampleAPI < Grape::API 52 | # before do 53 | # ApiLogger.start 54 | # end 55 | # finally do 56 | # ApiLogger.close 57 | # end 58 | # end 59 | # 60 | # This will make sure that the ApiLogger is opened and closed around every 61 | # request 62 | # @param ensured_block [Proc] The block to be executed after every api_call 63 | def finally(&block) 64 | namespace_stackable(:finallies, block) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/grape/dsl/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module Configuration 6 | extend ActiveSupport::Concern 7 | 8 | module ClassMethods 9 | include Grape::DSL::Settings 10 | include Grape::DSL::Logger 11 | include Grape::DSL::Desc 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/dsl/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module Headers 6 | # This method has four responsibilities: 7 | # 1. Set a specifc header value by key 8 | # 2. Retrieve a specifc header value by key 9 | # 3. Retrieve all headers that have been set 10 | # 4. Delete a specifc header key-value pair 11 | def header(key = nil, val = nil) 12 | if key 13 | val ? header[key] = val : header.delete(key) 14 | else 15 | @header ||= Grape::Util::Header.new 16 | end 17 | end 18 | alias headers header 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/dsl/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module Logger 6 | include Grape::DSL::Settings 7 | 8 | attr_writer :logger 9 | 10 | # Set or retrive the configured logger. If none was configured, this 11 | # method will create a new one, logging to stdout. 12 | # @param logger [Object] the new logger to use 13 | def logger(logger = nil) 14 | if logger 15 | global_setting(:logger, logger) 16 | else 17 | global_setting(:logger) || global_setting(:logger, ::Logger.new($stdout)) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/grape/dsl/middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module Middleware 6 | extend ActiveSupport::Concern 7 | 8 | include Grape::DSL::Configuration 9 | 10 | module ClassMethods 11 | # Apply a custom middleware to the API. Applies 12 | # to the current namespace and any children, but 13 | # not parents. 14 | # 15 | # @param middleware_class [Class] The class of the middleware you'd like 16 | # to inject. 17 | def use(middleware_class, *args, &block) 18 | arr = [:use, middleware_class, *args] 19 | arr << block if block 20 | 21 | namespace_stackable(:middleware, arr) 22 | end 23 | 24 | def insert(*args, &block) 25 | arr = [:insert, *args] 26 | arr << block if block 27 | 28 | namespace_stackable(:middleware, arr) 29 | end 30 | 31 | def insert_before(*args, &block) 32 | arr = [:insert_before, *args] 33 | arr << block if block 34 | 35 | namespace_stackable(:middleware, arr) 36 | end 37 | 38 | def insert_after(*args, &block) 39 | arr = [:insert_after, *args] 40 | arr << block if block 41 | 42 | namespace_stackable(:middleware, arr) 43 | end 44 | 45 | # Retrieve an array of the middleware classes 46 | # and arguments that are currently applied to the 47 | # application. 48 | def middleware 49 | namespace_stackable(:middleware) || [] 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/grape/dsl/validations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module DSL 5 | module Validations 6 | extend ActiveSupport::Concern 7 | 8 | include Grape::DSL::Configuration 9 | 10 | module ClassMethods 11 | # Clears all defined parameters and validations. The main purpose of it is to clean up 12 | # settings, so next endpoint won't interfere with previous one. 13 | # 14 | # params do 15 | # # params for the endpoint below this block 16 | # end 17 | # post '/current' do 18 | # # whatever 19 | # end 20 | # 21 | # # somewhere between them the reset_validations! method gets called 22 | # 23 | # params do 24 | # # params for the endpoint below this block 25 | # end 26 | # post '/next' do 27 | # # whatever 28 | # end 29 | def reset_validations! 30 | unset_namespace_stackable :declared_params 31 | unset_namespace_stackable :validations 32 | unset_namespace_stackable :params 33 | end 34 | 35 | # Opens a root-level ParamsScope, defining parameter coercions and 36 | # validations for the endpoint. 37 | # @yield instance context of the new scope 38 | def params(&block) 39 | Grape::Validations::ParamsScope.new(api: self, type: Hash, &block) 40 | end 41 | 42 | # Declare the contract to be used for the endpoint's parameters. 43 | # @param contract [Class | Dry::Schema::Processor] 44 | # The contract or schema to be used for validation. Optional. 45 | # @yield a block yielding a new instance of Dry::Schema::Params 46 | # subclass, allowing to define the schema inline. When the 47 | # +contract+ parameter is a schema, it will be used as a parent. Optional. 48 | def contract(contract = nil, &block) 49 | raise ArgumentError, 'Either contract or block must be provided' unless contract || block 50 | raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema) 51 | 52 | Grape::Validations::ContractScope.new(self, contract, &block) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/grape/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Env 5 | API_VERSION = 'api.version' 6 | API_ENDPOINT = 'api.endpoint' 7 | API_REQUEST_INPUT = 'api.request.input' 8 | API_REQUEST_BODY = 'api.request.body' 9 | API_TYPE = 'api.type' 10 | API_SUBTYPE = 'api.subtype' 11 | API_VENDOR = 'api.vendor' 12 | API_FORMAT = 'api.format' 13 | 14 | GRAPE_REQUEST = 'grape.request' 15 | GRAPE_REQUEST_HEADERS = 'grape.request.headers' 16 | GRAPE_REQUEST_PARAMS = 'grape.request.params' 17 | GRAPE_ROUTING_ARGS = 'grape.routing_args' 18 | GRAPE_ALLOWED_METHODS = 'grape.allowed_methods' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/grape/error_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ErrorFormatter 5 | extend Grape::Util::Registry 6 | 7 | module_function 8 | 9 | def formatter_for(format, error_formatters = nil, default_error_formatter = nil) 10 | return error_formatters[format] if error_formatters&.key?(format) 11 | 12 | registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/error_formatter/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ErrorFormatter 5 | class Json < Base 6 | class << self 7 | def format_structured_message(structured_message) 8 | ::Grape::Json.dump(structured_message) 9 | end 10 | 11 | private 12 | 13 | def wrap_message(message) 14 | return message if message.is_a?(Hash) 15 | return message.as_json if message.is_a?(Exceptions::ValidationErrors) 16 | 17 | { error: ensure_utf8(message) } 18 | end 19 | 20 | def ensure_utf8(message) 21 | return message unless message.respond_to? :encode 22 | 23 | message.encode('UTF-8', invalid: :replace, undef: :replace) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/grape/error_formatter/serializable_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ErrorFormatter 5 | class SerializableHash < Json; end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/grape/error_formatter/txt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ErrorFormatter 5 | class Txt < Base 6 | def self.format_structured_message(structured_message) 7 | message = structured_message[:message] || Grape::Json.dump(structured_message) 8 | Array.wrap(message).tap do |final_message| 9 | if structured_message.key?(:backtrace) 10 | final_message << 'backtrace:' 11 | final_message.concat(structured_message[:backtrace]) 12 | end 13 | if structured_message.key?(:original_exception) 14 | final_message << 'original exception:' 15 | final_message << structured_message[:original_exception] 16 | end 17 | end.join("\r\n ") 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/error_formatter/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ErrorFormatter 5 | class Xml < Base 6 | def self.format_structured_message(structured_message) 7 | structured_message.respond_to?(:to_xml) ? structured_message.to_xml(root: :error) : structured_message.to_s 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/conflicting_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class ConflictingTypes < Base 6 | def initialize 7 | super(message: compose_message(:conflicting_types), status: 400) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/empty_message_body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class EmptyMessageBody < Base 6 | def initialize(body_format) 7 | super(message: compose_message(:empty_message_body, body_format: body_format), status: 400) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/incompatible_option_values.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class IncompatibleOptionValues < Base 6 | def initialize(option1, value1, option2, value2) 7 | super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_accept_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidAcceptHeader < Base 6 | def initialize(message, headers) 7 | super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidFormatter < Base 6 | def initialize(klass, to_format) 7 | super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_message_body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidMessageBody < Base 6 | def initialize(body_format) 7 | super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidParameters < Base 6 | def initialize 7 | super(message: compose_message(:invalid_parameters), status: 400) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidResponse < Base 6 | def initialize 7 | super(message: compose_message(:invalid_response)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_version_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidVersionHeader < Base 6 | def initialize(message, headers) 7 | super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_versioner_option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidVersionerOption < Base 6 | def initialize(strategy) 7 | super(message: compose_message(:invalid_versioner_option, strategy: strategy)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/invalid_with_option_for_represent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class InvalidWithOptionForRepresent < Base 6 | def initialize 7 | super(message: compose_message(:invalid_with_option_for_represent)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/method_not_allowed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class MethodNotAllowed < Base 6 | def initialize(headers) 7 | super(message: '405 Not Allowed', status: 405, headers: headers) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/missing_group_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class MissingGroupType < Base 6 | def initialize 7 | super(message: compose_message(:missing_group_type)) 8 | end 9 | end 10 | end 11 | end 12 | 13 | Grape::Exceptions::MissingGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::MissingGroupTypeError', 'Grape::Exceptions::MissingGroupType', Grape.deprecator) 14 | -------------------------------------------------------------------------------- /lib/grape/exceptions/missing_mime_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class MissingMimeType < Base 6 | def initialize(new_format) 7 | super(message: compose_message(:missing_mime_type, new_format: new_format)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/missing_option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class MissingOption < Base 6 | def initialize(option) 7 | super(message: compose_message(:missing_option, option: option)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/missing_vendor_option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class MissingVendorOption < Base 6 | def initialize 7 | super(message: compose_message(:missing_vendor_option)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/too_deep_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class TooDeepParameters < Base 6 | def initialize(limit) 7 | super(message: compose_message(:too_deep_parameters, limit: limit), status: 400) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/too_many_multipart_files.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class TooManyMultipartFiles < Base 6 | def initialize(limit) 7 | super(message: compose_message(:too_many_multipart_files, limit: limit), status: 413) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unknown_auth_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnknownAuthStrategy < Base 6 | def initialize(strategy:) 7 | super(message: compose_message(:unknown_auth_strategy, strategy: strategy)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unknown_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnknownOptions < Base 6 | def initialize(options) 7 | super(message: compose_message(:unknown_options, options: options)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unknown_parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnknownParameter < Base 6 | def initialize(param) 7 | super(message: compose_message(:unknown_parameter, param: param)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unknown_params_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnknownParamsBuilder < Base 6 | def initialize(params_builder_type) 7 | super(message: compose_message(:unknown_params_builder, params_builder_type: params_builder_type)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unknown_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnknownValidator < Base 6 | def initialize(validator_type) 7 | super(message: compose_message(:unknown_validator, validator_type: validator_type)) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/exceptions/unsupported_group_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class UnsupportedGroupType < Base 6 | def initialize 7 | super(message: compose_message(:unsupported_group_type)) 8 | end 9 | end 10 | end 11 | end 12 | 13 | Grape::Exceptions::UnsupportedGroupTypeError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Grape::Exceptions::UnsupportedGroupTypeError', 'Grape::Exceptions::UnsupportedGroupType', Grape.deprecator) 14 | -------------------------------------------------------------------------------- /lib/grape/exceptions/validation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class Validation < Base 6 | attr_accessor :params, :message_key 7 | 8 | def initialize(params:, message: nil, status: nil, headers: nil) 9 | @params = params 10 | if message 11 | @message_key = message if message.is_a?(Symbol) 12 | message = translate_message(message) 13 | end 14 | 15 | super(status: status, message: message, headers: headers) 16 | end 17 | 18 | # Remove all the unnecessary stuff from Grape::Exceptions::Base like status 19 | # and headers when converting a validation error to json or string 20 | def as_json(*_args) 21 | to_s 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/grape/exceptions/validation_array_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class ValidationArrayErrors < Base 6 | attr_reader :errors 7 | 8 | def initialize(errors) 9 | super() 10 | @errors = errors 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grape/exceptions/validation_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Exceptions 5 | class ValidationErrors < Base 6 | ERRORS_FORMAT_KEY = 'grape.errors.format' 7 | DEFAULT_ERRORS_FORMAT = '%s %s' 8 | 9 | include Enumerable 10 | 11 | attr_reader :errors 12 | 13 | def initialize(errors: [], headers: {}) 14 | @errors = errors.group_by(&:params) 15 | super(message: full_messages.join(', '), status: 400, headers: headers) 16 | end 17 | 18 | def each 19 | errors.each_pair do |attribute, errors| 20 | errors.each do |error| 21 | yield attribute, error 22 | end 23 | end 24 | end 25 | 26 | def as_json(**_opts) 27 | errors.map do |k, v| 28 | { 29 | params: k, 30 | messages: v.map(&:to_s) 31 | } 32 | end 33 | end 34 | 35 | def to_json(*_opts) 36 | as_json.to_json 37 | end 38 | 39 | def full_messages 40 | messages = map do |attributes, error| 41 | I18n.t( 42 | ERRORS_FORMAT_KEY, 43 | default: DEFAULT_ERRORS_FORMAT, 44 | attributes: translate_attributes(attributes), 45 | message: error.message 46 | ) 47 | end 48 | messages.uniq! 49 | messages 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/grape/extensions/active_support/hash_with_indifferent_access.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Extensions 5 | module ActiveSupport 6 | module HashWithIndifferentAccess 7 | module ParamBuilder 8 | extend ::ActiveSupport::Concern 9 | 10 | included do 11 | Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' 12 | namespace_inheritable(:build_params_with, :hash_with_indifferent_access) 13 | end 14 | 15 | def build_params 16 | ::ActiveSupport::HashWithIndifferentAccess.new(rack_params).tap do |params| 17 | params.deep_merge!(grape_routing_args) if env.key?(Grape::Env::GRAPE_ROUTING_ARGS) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/grape/extensions/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Extensions 5 | module Hash 6 | module ParamBuilder 7 | extend ::ActiveSupport::Concern 8 | 9 | included do 10 | Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' 11 | namespace_inheritable(:build_params_with, :hash) 12 | end 13 | 14 | def build_params 15 | rack_params.deep_dup.tap do |params| 16 | params.deep_symbolize_keys! 17 | 18 | if env.key?(Grape::Env::GRAPE_ROUTING_ARGS) 19 | grape_routing_args.deep_symbolize_keys! 20 | params.deep_merge!(grape_routing_args) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/grape/extensions/hashie/mash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Extensions 5 | module Hashie 6 | module Mash 7 | module ParamBuilder 8 | extend ::ActiveSupport::Concern 9 | 10 | included do 11 | Grape.deprecator.warn 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' 12 | namespace_inheritable(:build_params_with, :hashie_mash) 13 | end 14 | 15 | def build_params 16 | ::Hashie::Mash.new(rack_params).tap do |params| 17 | params.deep_merge!(grape_routing_args) if env.key?(Grape::Env::GRAPE_ROUTING_ARGS) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/grape/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | extend Grape::Util::Registry 6 | 7 | module_function 8 | 9 | DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj } 10 | 11 | def formatter_for(api_format, formatters) 12 | return formatters[api_format] if formatters&.key?(api_format) 13 | 14 | registry[api_format] || DEFAULT_LAMBDA_FORMATTER 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape/formatter/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | class Base 6 | def self.call(_object, _env) 7 | raise NotImplementedError 8 | end 9 | 10 | def self.inherited(klass) 11 | super 12 | Formatter.register(klass) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/grape/formatter/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | class Json < Base 6 | def self.call(object, _env) 7 | return object.to_json if object.respond_to?(:to_json) 8 | 9 | ::Grape::Json.dump(object) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/grape/formatter/serializable_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | class SerializableHash < Base 6 | class << self 7 | def call(object, _env) 8 | return object if object.is_a?(String) 9 | return ::Grape::Json.dump(serialize(object)) if serializable?(object) 10 | return object.to_json if object.respond_to?(:to_json) 11 | 12 | ::Grape::Json.dump(object) 13 | end 14 | 15 | private 16 | 17 | def serializable?(object) 18 | object.respond_to?(:serializable_hash) || array_serializable?(object) || object.is_a?(Hash) 19 | end 20 | 21 | def serialize(object) 22 | if object.respond_to? :serializable_hash 23 | object.serializable_hash 24 | elsif array_serializable?(object) 25 | object.map(&:serializable_hash) 26 | elsif object.is_a?(Hash) 27 | object.transform_values { |v| serialize(v) } 28 | else 29 | object 30 | end 31 | end 32 | 33 | def array_serializable?(object) 34 | object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/grape/formatter/txt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | class Txt < Base 6 | def self.call(object, _env) 7 | object.respond_to?(:to_txt) ? object.to_txt : object.to_s 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/formatter/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Formatter 5 | class Xml < Base 6 | def self.call(object, _env) 7 | return object.to_xml if object.respond_to?(:to_xml) 8 | 9 | raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/grape/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | if defined?(::MultiJson) 5 | Json = ::MultiJson 6 | else 7 | Json = ::JSON 8 | Json::ParseError = Json::ParserError 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/grape/middleware/auth/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Auth 6 | class Base < Grape::Middleware::Base 7 | def initialize(app, **options) 8 | super 9 | @auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy| 10 | raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy 11 | end 12 | end 13 | 14 | def call!(env) 15 | @env = env 16 | @auth_strategy.create(app, options) do |*args| 17 | context.instance_exec(*args, &options[:proc]) 18 | end.call(env) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/grape/middleware/auth/dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Auth 6 | module DSL 7 | def auth(type = nil, options = {}, &block) 8 | if type 9 | namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block)) 10 | use Grape::Middleware::Auth::Base, namespace_inheritable(:auth) 11 | else 12 | namespace_inheritable(:auth) 13 | end 14 | end 15 | 16 | # Add HTTP Basic authorization to the API. 17 | # 18 | # @param [Hash] options A hash of options. 19 | # @option options [String] :realm "API Authorization" The HTTP Basic realm. 20 | def http_basic(options = {}, &block) 21 | options[:realm] ||= 'API Authorization' 22 | auth :http_basic, options, &block 23 | end 24 | 25 | def http_digest(options = {}, &block) 26 | options[:realm] ||= 'API Authorization' 27 | 28 | if options[:realm].respond_to?(:values_at) 29 | options[:realm][:opaque] ||= 'secret' 30 | else 31 | options[:opaque] ||= 'secret' 32 | end 33 | 34 | auth :http_digest, options, &block 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/grape/middleware/auth/strategies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Auth 6 | module Strategies 7 | module_function 8 | 9 | def add(label, strategy, option_fetcher = ->(_) { [] }) 10 | auth_strategies[label] = StrategyInfo.new(strategy, option_fetcher) 11 | end 12 | 13 | def auth_strategies 14 | @auth_strategies ||= { 15 | http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }) 16 | } 17 | end 18 | 19 | def [](label) 20 | auth_strategies[label] 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/grape/middleware/auth/strategy_info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Auth 6 | StrategyInfo = Struct.new(:auth_class, :settings_fetcher) do 7 | def create(app, options, &block) 8 | strategy_args = settings_fetcher.call(options) 9 | 10 | auth_class.new(app, *strategy_args, &block) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/middleware/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | # This is a simple middleware for adding before and after filters 6 | # to Grape APIs. It is used like so: 7 | # 8 | # use Grape::Middleware::Filter, before: -> { do_something }, after: -> { do_something } 9 | class Filter < Base 10 | def before 11 | app.instance_eval(&options[:before]) if options[:before] 12 | end 13 | 14 | def after 15 | app.instance_eval(&options[:after]) if options[:after] 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/grape/middleware/globals.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | class Globals < Base 6 | def before 7 | request = Grape::Request.new(@env, build_params_with: @options[:build_params_with]) 8 | @env[Grape::Env::GRAPE_REQUEST] = request 9 | @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers 10 | @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT] 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grape/middleware/versioner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Versioners set env['api.version'] when a version is defined on an API and 4 | # on the requests. The current methods for determining version are: 5 | # 6 | # :header - version from HTTP Accept header. 7 | # :accept_version_header - version from HTTP Accept-Version header 8 | # :path - version from uri. e.g. /v1/resource 9 | # :param - version from uri query string, e.g. /v1/resource?apiver=v1 10 | # See individual classes for details. 11 | module Grape 12 | module Middleware 13 | module Versioner 14 | extend Grape::Util::Registry 15 | 16 | module_function 17 | 18 | # @param strategy [Symbol] :path, :header, :accept_version_header or :param 19 | # @return a middleware class based on strategy 20 | def using(strategy) 21 | raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy) 22 | 23 | registry[strategy] 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/grape/middleware/versioner/accept_version_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Versioner 6 | # This middleware sets various version related rack environment variables 7 | # based on the HTTP Accept-Version header 8 | # 9 | # Example: For request header 10 | # Accept-Version: v1 11 | # 12 | # The following rack env variables are set: 13 | # 14 | # env['api.version'] => 'v1' 15 | # 16 | # If version does not match this route, then a 406 is raised with 17 | # X-Cascade header to alert Grape::Router to attempt the next matched 18 | # route. 19 | class AcceptVersionHeader < Base 20 | def before 21 | potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub) 22 | not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank? 23 | 24 | return if potential_version.blank? 25 | 26 | not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version) 27 | env[Grape::Env::API_VERSION] = potential_version 28 | end 29 | 30 | private 31 | 32 | def not_acceptable!(message) 33 | throw :error, status: 406, headers: error_headers, message: message 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/grape/middleware/versioner/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Versioner 6 | class Base < Grape::Middleware::Base 7 | DEFAULT_OPTIONS = { 8 | pattern: /.*/i.freeze, 9 | version_options: { 10 | strict: false, 11 | cascade: true, 12 | parameter: 'apiver' 13 | }.freeze 14 | }.freeze 15 | 16 | def self.inherited(klass) 17 | super 18 | Versioner.register(klass) 19 | end 20 | 21 | def versions 22 | options[:versions] 23 | end 24 | 25 | def prefix 26 | options[:prefix] 27 | end 28 | 29 | def mount_path 30 | options[:mount_path] 31 | end 32 | 33 | def pattern 34 | options[:pattern] 35 | end 36 | 37 | def version_options 38 | options[:version_options] 39 | end 40 | 41 | def strict? 42 | version_options[:strict] 43 | end 44 | 45 | # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking 46 | # of routes (see Grape::Router) for more information). To prevent 47 | # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`. 48 | def cascade? 49 | version_options[:cascade] 50 | end 51 | 52 | def parameter_key 53 | version_options[:parameter] 54 | end 55 | 56 | def vendor 57 | version_options[:vendor] 58 | end 59 | 60 | def error_headers 61 | cascade? ? { 'X-Cascade' => 'pass' } : {} 62 | end 63 | 64 | def potential_version_match?(potential_version) 65 | versions.blank? || versions.any? { |v| v.to_s == potential_version } 66 | end 67 | 68 | def version_not_found! 69 | throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' } 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/grape/middleware/versioner/param.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Versioner 6 | # This middleware sets various version related rack environment variables 7 | # based on the request parameters and removes that parameter from the 8 | # request parameters for subsequent middleware and API. 9 | # If the version substring does not match any potential initialized 10 | # versions, a 404 error is thrown. 11 | # If the version substring is not passed the version (highest mounted) 12 | # version will be used. 13 | # 14 | # Example: For a uri path 15 | # /resource?apiver=v1 16 | # 17 | # The following rack env variables are set and path is rewritten to 18 | # '/resource': 19 | # 20 | # env['api.version'] => 'v1' 21 | class Param < Base 22 | def before 23 | potential_version = query_params[parameter_key] 24 | return if potential_version.blank? 25 | 26 | version_not_found! unless potential_version_match?(potential_version) 27 | env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/grape/middleware/versioner/path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Middleware 5 | module Versioner 6 | # This middleware sets various version related rack environment variables 7 | # based on the uri path and removes the version substring from the uri 8 | # path. If the version substring does not match any potential initialized 9 | # versions, a 404 error is thrown. 10 | # 11 | # Example: For a uri path 12 | # /v1/resource 13 | # 14 | # The following rack env variables are set and path is rewritten to 15 | # '/resource': 16 | # 17 | # env['api.version'] => 'v1' 18 | # 19 | class Path < Base 20 | def before 21 | path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO]) 22 | return if path_info == '/' 23 | 24 | [mount_path, Grape::Router.normalize_path(prefix)].each do |path| 25 | path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path) 26 | end 27 | 28 | slash_position = path_info.index('/', 1) # omit the first one 29 | return unless slash_position 30 | 31 | potential_version = path_info[1..slash_position - 1] 32 | return unless potential_version.match?(pattern) 33 | 34 | version_not_found! unless potential_version_match?(potential_version) 35 | env[Grape::Env::API_VERSION] = potential_version 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/grape/namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | # A container for endpoints or other namespaces, which allows for both 5 | # logical grouping of endpoints as well as sharing common configuration. 6 | # May also be referred to as group, segment, or resource. 7 | class Namespace 8 | attr_reader :space, :options 9 | 10 | # @param space [String] the name of this namespace 11 | # @param options [Hash] options hash 12 | # @option options :requirements [Hash] param-regex pairs, all of which must 13 | # be met by a request's params for all endpoints in this namespace, or 14 | # validation will fail and return a 422. 15 | def initialize(space, options) 16 | @space = space.to_s 17 | @options = options 18 | end 19 | 20 | # Retrieves the requirements from the options hash, if given. 21 | # @return [Hash] 22 | def requirements 23 | options[:requirements] || {} 24 | end 25 | 26 | # (see ::joined_space_path) 27 | def self.joined_space(settings) 28 | settings&.map(&:space) 29 | end 30 | 31 | # Join the namespaces from a list of settings to create a path prefix. 32 | # @param settings [Array] list of Grape::Util::InheritableSettings. 33 | def self.joined_space_path(settings) 34 | JoinedSpaceCache[joined_space(settings)] 35 | end 36 | 37 | class JoinedSpaceCache < Grape::Util::Cache 38 | def initialize 39 | super 40 | @cache = Hash.new do |h, joined_space| 41 | h[joined_space] = Grape::Router.normalize_path(joined_space.join('/')) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/grape/params_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ParamsBuilder 5 | extend Grape::Util::Registry 6 | 7 | SHORT_NAME_LOOKUP = { 8 | 'Grape::Extensions::Hash::ParamBuilder' => :hash, 9 | 'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access, 10 | 'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash 11 | }.freeze 12 | 13 | module_function 14 | 15 | def params_builder_for(short_name) 16 | verified_short_name = verify_short_name!(short_name) 17 | 18 | raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name) 19 | 20 | registry[verified_short_name] 21 | end 22 | 23 | def verify_short_name!(short_name) 24 | return short_name if short_name.is_a?(Symbol) 25 | 26 | class_name = short_name.name 27 | SHORT_NAME_LOOKUP[class_name].tap do |real_short_name| 28 | Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead." 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/grape/params_builder/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ParamsBuilder 5 | class Base 6 | class << self 7 | def call(_params) 8 | raise NotImplementedError 9 | end 10 | 11 | def inherited(klass) 12 | super 13 | ParamsBuilder.register(klass) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/grape/params_builder/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ParamsBuilder 5 | class Hash < Base 6 | def self.call(params) 7 | params.deep_symbolize_keys 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/params_builder/hash_with_indifferent_access.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ParamsBuilder 5 | class HashWithIndifferentAccess < Base 6 | def self.call(params) 7 | params.with_indifferent_access 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/params_builder/hashie_mash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ParamsBuilder 5 | class HashieMash < Base 6 | def self.call(params) 7 | ::Hashie::Mash.new(params) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Parser 5 | extend Grape::Util::Registry 6 | 7 | module_function 8 | 9 | def parser_for(format, parsers = nil) 10 | return parsers[format] if parsers&.key?(format) 11 | 12 | registry[format] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/parser/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Parser 5 | class Base 6 | def self.call(_object, _env) 7 | raise NotImplementedError 8 | end 9 | 10 | def self.inherited(klass) 11 | super 12 | Parser.register(klass) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/grape/parser/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Parser 5 | class Json < Base 6 | def self.call(object, _env) 7 | ::Grape::Json.load(object) 8 | rescue ::Grape::Json::ParseError 9 | # handle JSON parsing errors via the rescue handlers or provide error message 10 | raise Grape::Exceptions::InvalidMessageBody.new('application/json') 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grape/parser/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Parser 5 | class Xml < Base 6 | def self.call(object, _env) 7 | ::Grape::Xml.parse(object) 8 | rescue ::Grape::Xml::ParseError 9 | # handle XML parsing errors via the rescue handlers or provide error message 10 | raise Grape::Exceptions::InvalidMessageBody.new('application/xml') 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grape/presenters/presenter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Presenters 5 | class Presenter 6 | def self.represent(object, **_options) 7 | object 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/grape/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | class Railtie < ::Rails::Railtie 5 | initializer 'grape.deprecator' do |app| 6 | app.deprecators[:grape] = Grape.deprecator 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/grape/router/base_route.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | class Router 5 | class BaseRoute 6 | delegate_missing_to :@options 7 | 8 | attr_reader :index, :pattern, :options 9 | 10 | def initialize(options) 11 | @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options) 12 | end 13 | 14 | alias attributes options 15 | 16 | def regexp_capture_index 17 | CaptureIndexCache[index] 18 | end 19 | 20 | def pattern_regexp 21 | pattern.to_regexp 22 | end 23 | 24 | def to_regexp(index) 25 | @index = index 26 | Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})") 27 | end 28 | 29 | class CaptureIndexCache < Grape::Util::Cache 30 | def initialize 31 | super 32 | @cache = Hash.new do |h, index| 33 | h[index] = "_#{index}" 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/grape/router/greedy_route.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Act like a Grape::Router::Route but for greedy_match 4 | # see @neutral_map 5 | 6 | module Grape 7 | class Router 8 | class GreedyRoute < BaseRoute 9 | def initialize(pattern, options) 10 | @pattern = pattern 11 | super(options) 12 | end 13 | 14 | # Grape::Router:Route defines params as a function 15 | def params(_input = nil) 16 | options[:params] || {} 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/grape/router/route.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | class Router 5 | class Route < BaseRoute 6 | extend Forwardable 7 | 8 | FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) } 9 | NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) } 10 | 11 | attr_reader :app, :request_method 12 | 13 | def_delegators :pattern, :path, :origin 14 | 15 | def initialize(method, origin, path, options) 16 | @request_method = upcase_method(method) 17 | @pattern = Grape::Router::Pattern.new(origin, path, options) 18 | @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD 19 | super(options) 20 | end 21 | 22 | def convert_to_head_request! 23 | @request_method = Rack::HEAD 24 | end 25 | 26 | def exec(env) 27 | @app.call(env) 28 | end 29 | 30 | def apply(app) 31 | @app = app 32 | self 33 | end 34 | 35 | def match?(input) 36 | return false if input.blank? 37 | 38 | @match_function.call(input, pattern) 39 | end 40 | 41 | def params(input = nil) 42 | return params_without_input if input.blank? 43 | 44 | parsed = pattern.params(input) 45 | return {} unless parsed 46 | 47 | parsed.compact.symbolize_keys 48 | end 49 | 50 | private 51 | 52 | def params_without_input 53 | @params_without_input ||= pattern.captures_default.merge(attributes.params) 54 | end 55 | 56 | def upcase_method(method) 57 | method_s = method.to_s 58 | Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/grape/serve_stream/file_body.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ServeStream 5 | CHUNK_SIZE = 16_384 6 | 7 | # Class helps send file through API 8 | class FileBody 9 | attr_reader :path 10 | 11 | # @param path [String] 12 | def initialize(path) 13 | @path = path 14 | end 15 | 16 | # Need for Rack::Sendfile middleware 17 | # 18 | # @return [String] 19 | def to_path 20 | path 21 | end 22 | 23 | def each 24 | File.open(path, 'rb') do |file| 25 | while (chunk = file.read(CHUNK_SIZE)) 26 | yield chunk 27 | end 28 | end 29 | end 30 | 31 | def ==(other) 32 | path == other.path 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/grape/serve_stream/sendfile_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ServeStream 5 | # Response should respond to to_path method 6 | # for using Rack::SendFile middleware 7 | class SendfileResponse < Rack::Response 8 | def respond_to?(method_name, include_all = false) 9 | if method_name == :to_path 10 | @body.respond_to?(:to_path, include_all) 11 | else 12 | super 13 | end 14 | end 15 | 16 | def to_path 17 | @body.to_path 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/serve_stream/stream_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module ServeStream 5 | # A simple class used to identify responses which represent streams (or files) and do not 6 | # need to be formatted or pre-read by Rack::Response 7 | class StreamResponse 8 | attr_reader :stream 9 | 10 | # @param stream [Object] 11 | def initialize(stream) 12 | @stream = stream 13 | end 14 | 15 | # Equality provided mostly for tests. 16 | # 17 | # @return [Boolean] 18 | def ==(other) 19 | stream == other.stream 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/grape/types/invalid_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # only exists to make it shorter for external use 4 | module Grape 5 | module Types 6 | InvalidValue = Class.new(Grape::Validations::Types::InvalidValue) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/grape/util/base_inheritable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | # Base for classes which need to operate with own values kept 6 | # in the hash and inherited values kept in a Hash-like object. 7 | class BaseInheritable 8 | attr_accessor :inherited_values, :new_values 9 | 10 | # @param inherited_values [Object] An object implementing an interface 11 | # of the Hash class. 12 | def initialize(inherited_values = nil) 13 | @inherited_values = inherited_values || {} 14 | @new_values = {} 15 | end 16 | 17 | def delete(key) 18 | new_values.delete key 19 | end 20 | 21 | def initialize_copy(other) 22 | super 23 | self.inherited_values = other.inherited_values 24 | self.new_values = other.new_values.dup 25 | end 26 | 27 | def keys 28 | if new_values.any? 29 | inherited_values.keys.tap do |combined| 30 | combined.concat(new_values.keys) 31 | combined.uniq! 32 | end 33 | else 34 | inherited_values.keys 35 | end 36 | end 37 | 38 | def key?(name) 39 | inherited_values.key?(name) || new_values.key?(name) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/grape/util/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class Cache 6 | include Singleton 7 | 8 | attr_reader :cache 9 | 10 | class << self 11 | extend Forwardable 12 | def_delegators :cache, :[] 13 | def_delegators :instance, :cache 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape/util/endpoint_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class EndpointConfiguration < Lazy::ValueHash 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/grape/util/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | if Gem::Version.new(Rack.release) >= Gem::Version.new('3') 6 | require 'rack/headers' 7 | Header = Rack::Headers 8 | else 9 | require 'rack/utils' 10 | Header = Rack::Utils::HeaderHash 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/grape/util/inheritable_values.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class InheritableValues < BaseInheritable 6 | def [](name) 7 | values[name] 8 | end 9 | 10 | def []=(name, value) 11 | new_values[name] = value 12 | end 13 | 14 | def merge(new_hash) 15 | values.merge!(new_hash) 16 | end 17 | 18 | def to_hash 19 | values 20 | end 21 | 22 | protected 23 | 24 | def values 25 | @inherited_values.merge(@new_values) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/grape/util/lazy/block.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Lazy 6 | class Block 7 | def initialize(&new_block) 8 | @block = new_block 9 | end 10 | 11 | def evaluate_from(configuration) 12 | @block.call(configuration) 13 | end 14 | 15 | def evaluate 16 | @block.call({}) 17 | end 18 | 19 | def lazy? 20 | true 21 | end 22 | 23 | def to_s 24 | evaluate.to_s 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/grape/util/lazy/value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Lazy 6 | class Value 7 | attr_reader :access_keys 8 | 9 | def initialize(value, access_keys = []) 10 | @value = value 11 | @access_keys = access_keys 12 | end 13 | 14 | def evaluate_from(configuration) 15 | matching_lazy_value = configuration.fetch(@access_keys) 16 | matching_lazy_value.evaluate 17 | end 18 | 19 | def evaluate 20 | @value 21 | end 22 | 23 | def lazy? 24 | true 25 | end 26 | 27 | def reached_by(parent_access_keys, access_key) 28 | @access_keys = parent_access_keys + [access_key] 29 | self 30 | end 31 | 32 | def to_s 33 | evaluate.to_s 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/grape/util/lazy/value_array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Lazy 6 | class ValueArray < ValueEnumerable 7 | def initialize(array) 8 | super 9 | @value_hash = [] 10 | array.each_with_index do |value, index| 11 | self[index] = value 12 | end 13 | end 14 | 15 | def evaluate 16 | @value_hash.map(&:evaluate) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/util/lazy/value_enumerable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Lazy 6 | class ValueEnumerable < Value 7 | def [](key) 8 | if @value_hash[key].nil? 9 | Value.new(nil).reached_by(access_keys, key) 10 | else 11 | @value_hash[key].reached_by(access_keys, key) 12 | end 13 | end 14 | 15 | def fetch(access_keys) 16 | fetched_keys = access_keys.dup 17 | value = self[fetched_keys.shift] 18 | fetched_keys.any? ? value.fetch(fetched_keys) : value 19 | end 20 | 21 | def []=(key, value) 22 | @value_hash[key] = case value 23 | when Hash 24 | ValueHash.new(value) 25 | when Array 26 | ValueArray.new(value) 27 | else 28 | Value.new(value) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/grape/util/lazy/value_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Lazy 6 | class ValueHash < ValueEnumerable 7 | def initialize(hash) 8 | super 9 | @value_hash = ActiveSupport::HashWithIndifferentAccess.new 10 | hash.each do |key, value| 11 | self[key] = value 12 | end 13 | end 14 | 15 | def evaluate 16 | @value_hash.transform_values(&:evaluate) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/util/media_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class MediaType 6 | attr_reader :type, :subtype, :vendor, :version, :format 7 | 8 | # based on the HTTP Accept header with the pattern: 9 | # application/vnd.:vendor-:version+:format 10 | VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?[a-z0-9.\-_!^]+?)(?:-(?[a-z0-9*.]+))?(?:\+(?[a-z0-9*\-.]+))?\z/.freeze 11 | 12 | def initialize(type:, subtype:) 13 | @type = type 14 | @subtype = subtype 15 | VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m| 16 | @vendor = m[:vendor] 17 | @version = m[:version] 18 | @format = m[:format] 19 | end 20 | end 21 | 22 | def ==(other) 23 | eql?(other) 24 | end 25 | 26 | def eql?(other) 27 | self.class == other.class && 28 | other.type == type && 29 | other.subtype == subtype && 30 | other.vendor == vendor && 31 | other.version == version && 32 | other.format == format 33 | end 34 | 35 | def hash 36 | [self.class, type, subtype, vendor, version, format].hash 37 | end 38 | 39 | class << self 40 | def best_quality(header, available_media_types) 41 | parse(best_quality_media_type(header, available_media_types)) 42 | end 43 | 44 | def parse(media_type) 45 | return if media_type.blank? 46 | 47 | type, subtype = media_type.split('/', 2) 48 | return if type.blank? || subtype.blank? 49 | 50 | new(type: type, subtype: subtype) 51 | end 52 | 53 | def match?(media_type) 54 | return false if media_type.blank? 55 | 56 | subtype = media_type.split('/', 2).last 57 | return false if subtype.blank? 58 | 59 | VENDOR_VERSION_HEADER_REGEX.match?(subtype) 60 | end 61 | 62 | def best_quality_media_type(header, available_media_types) 63 | header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header, available_media_types) 64 | end 65 | end 66 | 67 | private_class_method :best_quality_media_type 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/grape/util/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | module Registry 6 | def register(klass) 7 | short_name = build_short_name(klass) 8 | return if short_name.nil? 9 | 10 | warn "#{short_name} is already registered with class #{klass}" if registry.key?(short_name) 11 | registry[short_name] = klass 12 | end 13 | 14 | private 15 | 16 | def build_short_name(klass) 17 | return if klass.name.blank? 18 | 19 | klass.name.demodulize.underscore 20 | end 21 | 22 | def registry 23 | @registry ||= {}.with_indifferent_access 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/grape/util/reverse_stackable_values.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class ReverseStackableValues < StackableValues 6 | protected 7 | 8 | def concat_values(inherited_value, new_value) 9 | return inherited_value unless new_value 10 | 11 | new_value + inherited_value 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/util/stackable_values.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Util 5 | class StackableValues < BaseInheritable 6 | # Even if there is no value, an empty array will be returned. 7 | def [](name) 8 | inherited_value = inherited_values[name] 9 | new_value = new_values[name] 10 | 11 | return new_value || [] unless inherited_value 12 | 13 | concat_values(inherited_value, new_value) 14 | end 15 | 16 | def []=(name, value) 17 | new_values[name] ||= [] 18 | new_values[name].push value 19 | end 20 | 21 | def to_hash 22 | keys.each_with_object({}) do |key, result| 23 | result[key] = self[key] 24 | end 25 | end 26 | 27 | protected 28 | 29 | def concat_values(inherited_value, new_value) 30 | return inherited_value unless new_value 31 | 32 | inherited_value + new_value 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/grape/validations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | extend Grape::Util::Registry 6 | 7 | module_function 8 | 9 | def require_validator(short_name) 10 | raise Grape::Exceptions::UnknownValidator, short_name unless registry.key?(short_name) 11 | 12 | registry[short_name] 13 | end 14 | 15 | def build_short_name(klass) 16 | return if klass.name.blank? 17 | 18 | klass.name.demodulize.underscore.delete_suffix('_validator') 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/grape/validations/attributes_doc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an 6 | # internal API), the class only cleans up attributes to avoid junk in RAM. 7 | 8 | class AttributesDoc 9 | attr_accessor :type, :values 10 | 11 | # @param api [Grape::API::Instance] 12 | # @param scope [Validations::ParamsScope] 13 | def initialize(api, scope) 14 | @api = api 15 | @scope = scope 16 | @type = type 17 | end 18 | 19 | def extract_details(validations) 20 | details[:required] = validations.key?(:presence) 21 | 22 | desc = validations.delete(:desc) || validations.delete(:description) 23 | 24 | details[:desc] = desc if desc 25 | 26 | documentation = validations.delete(:documentation) 27 | 28 | details[:documentation] = documentation if documentation 29 | 30 | details[:default] = validations[:default] if validations.key?(:default) 31 | 32 | details[:min_length] = validations[:length][:min] if validations.key?(:length) && validations[:length].key?(:min) 33 | details[:max_length] = validations[:length][:max] if validations.key?(:length) && validations[:length].key?(:max) 34 | end 35 | 36 | def document(attrs) 37 | return if @api.namespace_inheritable(:do_not_document) 38 | 39 | details[:type] = type.to_s if type 40 | details[:values] = values if values 41 | 42 | documented_attrs = attrs.each_with_object({}) do |name, memo| 43 | memo[@scope.full_name(name)] = details 44 | end 45 | 46 | @api.namespace_stackable(:params, documented_attrs) 47 | end 48 | 49 | def required 50 | details[:required] 51 | end 52 | 53 | protected 54 | 55 | def details 56 | @details ||= {} 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/grape/validations/attributes_iterator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | class AttributesIterator 6 | include Enumerable 7 | 8 | attr_reader :scope 9 | 10 | def initialize(validator, scope, params) 11 | @scope = scope 12 | @attrs = validator.attrs 13 | @original_params = scope.params(params) 14 | @params = Array.wrap(@original_params) 15 | end 16 | 17 | def each(&block) 18 | do_each(@params, &block) # because we need recursion for nested arrays 19 | end 20 | 21 | private 22 | 23 | def do_each(params_to_process, parent_indicies = [], &block) 24 | @scope.reset_index # gets updated depending on the size of params_to_process 25 | params_to_process.each_with_index do |resource_params, index| 26 | # when we get arrays of arrays it means that target element located inside array 27 | # we need this because we want to know parent arrays indicies 28 | if resource_params.is_a?(Array) 29 | do_each(resource_params, [index] + parent_indicies, &block) 30 | next 31 | end 32 | 33 | if @scope.type == Array 34 | next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array 35 | 36 | # fill current and parent scopes with correct array indicies 37 | parent_scope = @scope.parent 38 | parent_indicies.each do |parent_index| 39 | parent_scope.index = parent_index 40 | parent_scope = parent_scope.parent 41 | end 42 | @scope.index = index 43 | end 44 | 45 | yield_attributes(resource_params, @attrs, &block) 46 | end 47 | end 48 | 49 | def yield_attributes(_resource_params, _attrs) 50 | raise NotImplementedError 51 | end 52 | 53 | # This is a special case so that we can ignore tree's where option 54 | # values are missing lower down. Unfortunately we can remove this 55 | # are the parameter parsing stage as they are required to ensure 56 | # the correct indexing is maintained 57 | def skip?(val) 58 | val == Grape::DSL::Parameters::EmptyOptionalValue 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/grape/validations/contract_scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | class ContractScope 6 | # Declare the contract to be used for the endpoint's parameters. 7 | # @param api [API] the API endpoint to modify. 8 | # @param contract the contract or schema to be used for validation. Optional. 9 | # @yield a block yielding a new schema class. Optional. 10 | def initialize(api, contract = nil, &block) 11 | # When block is passed, the first arg is either schema or nil. 12 | contract = Dry::Schema.Params(parent: contract, &block) if block 13 | 14 | if contract.respond_to?(:schema) 15 | # It's a Dry::Validation::Contract, then. 16 | contract = contract.new 17 | key_map = contract.schema.key_map 18 | else 19 | # Dry::Schema::Processor, hopefully. 20 | key_map = contract.key_map 21 | end 22 | 23 | api.namespace_stackable(:contract_key_map, key_map) 24 | 25 | validator_options = { 26 | validator_class: Grape::Validations.require_validator(:contract_scope), 27 | opts: { schema: contract, fail_fast: false } 28 | } 29 | 30 | api.namespace_stackable(:validations, validator_options) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/grape/validations/multiple_attributes_iterator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | class MultipleAttributesIterator < AttributesIterator 6 | private 7 | 8 | def yield_attributes(resource_params, _attrs) 9 | yield resource_params unless skip?(resource_params) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/grape/validations/single_attribute_iterator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | class SingleAttributeIterator < AttributesIterator 6 | private 7 | 8 | def yield_attributes(val, attrs) 9 | return if skip?(val) 10 | 11 | attrs.each do |attr_name| 12 | yield val, attr_name, empty?(val) 13 | end 14 | end 15 | 16 | # Primitives like Integers and Booleans don't respond to +empty?+. 17 | # It could be possible to use +blank?+ instead, but 18 | # 19 | # false.blank? 20 | # => true 21 | def empty?(val) 22 | val.respond_to?(:empty?) ? val.empty? : val.nil? 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/grape/validations/types/array_coercer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # Coerces elements in an array. It might be an array of strings or integers or 7 | # an array of arrays of integers. 8 | # 9 | # It could've been possible to use an +of+ 10 | # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/) 11 | # provided by dry-types. Unfortunately, it doesn't work for Grape because of 12 | # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer` 13 | # maintains Virtus behavior in coercing. 14 | class ArrayCoercer < DryTypeCoercer 15 | def initialize(type, strict = false) 16 | super 17 | 18 | @coercer = scope::Array 19 | @subtype = type.first 20 | end 21 | 22 | def call(_val) 23 | collection = super 24 | return collection if collection.is_a?(InvalidValue) 25 | 26 | coerce_elements collection 27 | end 28 | 29 | protected 30 | 31 | attr_reader :subtype 32 | 33 | def coerce_elements(collection) 34 | return if collection.nil? 35 | 36 | collection.each_with_index do |elem, index| 37 | return InvalidValue.new if reject?(elem) 38 | 39 | coerced_elem = elem_coercer.call(elem) 40 | 41 | return coerced_elem if coerced_elem.is_a?(InvalidValue) 42 | 43 | collection[index] = coerced_elem 44 | end 45 | 46 | collection 47 | end 48 | 49 | # This method maintains logic which was defined by Virtus for arrays. 50 | # Virtus doesn't allow nil in arrays. 51 | def reject?(val) 52 | val.nil? 53 | end 54 | 55 | def elem_coercer 56 | @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/grape/validations/types/custom_type_collection_coercer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # See {CustomTypeCoercer} for details on types 7 | # that will be supported by this by this coercer. 8 | # This coercer works in the same way as +CustomTypeCoercer+ 9 | # except that it expects to receive an array of strings to 10 | # coerce and will return an array (or optionally, a set) 11 | # of coerced values. 12 | # 13 | # +CustomTypeCoercer+ is already capable of providing type 14 | # checking for arrays where an independent coercion method 15 | # is supplied. As such, +CustomTypeCollectionCoercer+ does 16 | # not allow for such a method to be supplied independently 17 | # of the type. 18 | class CustomTypeCollectionCoercer < CustomTypeCoercer 19 | # A new coercer for collections of the given type. 20 | # 21 | # @param type [Class,#parse] 22 | # type to which items in the array should be coerced. 23 | # Must implement a +parse+ method which accepts a string, 24 | # and for the purposes of type-checking it may either be 25 | # a class, or it may implement a +coerced?+, +parsed?+ or 26 | # +call+ method (in that order of precedence) which 27 | # accepts a single argument and returns true if the given 28 | # array item has been coerced correctly. 29 | # @param set [Boolean] 30 | # when true, a +Set+ will be returned by {#call} instead 31 | # of an +Array+ and duplicate items will be discarded. 32 | def initialize(type, set = false) 33 | super(type) 34 | @set = set 35 | end 36 | 37 | # Coerces the given value. 38 | # 39 | # @param value [Array] an array of values to be coerced 40 | # @return [Array,Set] the coerced result. May be an +Array+ or a 41 | # +Set+ depending on the setting given to the constructor 42 | def call(value) 43 | coerced = value.map do |item| 44 | coerced_item = super(item) 45 | 46 | return coerced_item if coerced_item.is_a?(InvalidValue) 47 | 48 | coerced_item 49 | end 50 | 51 | @set ? Set.new(coerced) : coerced 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/grape/validations/types/dry_type_coercer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DryTypes 4 | # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is 5 | # a container in this case. Check documentation for more information 6 | # https://dry-rb.org/gems/dry-types/1.2/getting-started/ 7 | include Dry.Types() 8 | end 9 | 10 | module Grape 11 | module Validations 12 | module Types 13 | # A base class for classes which must identify a coercer to be used. 14 | # If the +strict+ argument is true, it won't coerce the given value 15 | # but check its type. More information there 16 | # https://dry-rb.org/gems/dry-types/1.2/built-in-types/ 17 | class DryTypeCoercer 18 | class << self 19 | # Returns a collection coercer which corresponds to a given type. 20 | # Example: 21 | # 22 | # collection_coercer_for(Array) 23 | # #=> Grape::Validations::Types::ArrayCoercer 24 | def collection_coercer_for(type) 25 | case type 26 | when Array 27 | ArrayCoercer 28 | when Set 29 | SetCoercer 30 | else 31 | raise ArgumentError, "Unknown type: #{type}" 32 | end 33 | end 34 | 35 | # Returns an instance of a coercer for a given type 36 | def coercer_instance_for(type, strict = false) 37 | klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type) 38 | klass.new(type, strict) 39 | end 40 | end 41 | 42 | def initialize(type, strict = false) 43 | @type = type 44 | @strict = strict 45 | @scope = strict ? DryTypes::Strict : DryTypes::Params 46 | end 47 | 48 | # Coerces the given value to a type which was specified during 49 | # initialization as a type argument. 50 | # 51 | # @param val [Object] 52 | def call(val) 53 | return if val.nil? 54 | 55 | @coercer[val] 56 | rescue Dry::Types::CoercionError => _e 57 | InvalidValue.new 58 | end 59 | 60 | protected 61 | 62 | attr_reader :scope, :type, :strict 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/grape/validations/types/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # Implementation for parameters that are multipart file objects. 7 | # Actual handling of these objects is provided by +Rack::Request+; 8 | # this class is here only to assert that rack's handling has succeeded. 9 | class File 10 | class << self 11 | def parse(input) 12 | return if input.nil? 13 | return InvalidValue.new unless parsed?(input) 14 | 15 | # Processing of multipart file objects 16 | # is already taken care of by Rack::Request. 17 | # Nothing to do here. 18 | input 19 | end 20 | 21 | def parsed?(value) 22 | # Rack::Request creates a Hash with filename, 23 | # content type and an IO object. Do a bit of basic 24 | # duck-typing. 25 | value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/grape/validations/types/invalid_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # Instances of this class may be used as tokens to denote that a parameter value could not be 7 | # coerced. The given message will be used as a validation error. 8 | class InvalidValue 9 | attr_reader :message 10 | 11 | def initialize(message = nil) 12 | @message = message 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape/validations/types/set_coercer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # Takes the given array and converts it to a set. Every element of the set 7 | # is also coerced. 8 | class SetCoercer < ArrayCoercer 9 | def initialize(type, strict = false) 10 | super 11 | 12 | @coercer = nil 13 | end 14 | 15 | def call(value) 16 | return InvalidValue.new unless value.is_a?(Array) 17 | 18 | coerce_elements(value) 19 | end 20 | 21 | protected 22 | 23 | def coerce_elements(collection) 24 | collection.each_with_object(Set.new) do |elem, memo| 25 | coerced_elem = elem_coercer.call(elem) 26 | 27 | return coerced_elem if coerced_elem.is_a?(InvalidValue) 28 | 29 | memo.add(coerced_elem) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/grape/validations/types/variant_collection_coercer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Types 6 | # This class wraps {MultipleTypeCoercer}, for use with collections 7 | # that allow members of more than one type. 8 | class VariantCollectionCoercer 9 | # Construct a new coercer that will attempt to coerce 10 | # a list of values such that all members are of one of 11 | # the given types. The container may also optionally be 12 | # coerced to a +Set+. An arbitrary coercion +method+ may 13 | # be supplied, which will be passed the entire collection 14 | # as a parameter and should return a new collection, or 15 | # may return the same one if no coercion was required. 16 | # 17 | # @param types [Array,Set] list of allowed types, 18 | # also specifying the container type 19 | # @param method [#call,#parse] method by which values should be coerced 20 | def initialize(types, method = nil) 21 | @types = types 22 | @method = method.respond_to?(:parse) ? method.method(:parse) : method 23 | 24 | # If we have a coercion method, pass it in here to save 25 | # building another one, even though we call it directly. 26 | @member_coercer = MultipleTypeCoercer.new types, method 27 | end 28 | 29 | # Coerce the given value. 30 | # 31 | # @param value [Array] collection of values to be coerced 32 | # @return [Array,Set,InvalidValue] 33 | # the coerced result, or an instance 34 | # of {InvalidValue} if the value could not be coerced. 35 | def call(value) 36 | return unless value.is_a? Array 37 | 38 | value = 39 | if @method 40 | @method.call(value) 41 | else 42 | value.map { |v| @member_coercer.call(v) } 43 | end 44 | return Set.new value if @types.is_a? Set 45 | 46 | value 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/grape/validations/validator_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | class ValidatorFactory 6 | def self.create_validator(options) 7 | options[:validator_class].new(options[:attributes], 8 | options[:options], 9 | options[:required], 10 | options[:params_scope], 11 | options[:opts]) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/all_or_none_of_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class AllOrNoneOfValidator < MultipleParamsBase 7 | def validate_params!(params) 8 | keys = keys_in_common(params) 9 | return if keys.empty? || keys.length == all_keys.length 10 | 11 | raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/allow_blank_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class AllowBlankValidator < Base 7 | def validate_param!(attr_name, params) 8 | return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) 9 | 10 | value = params[attr_name] 11 | value = value.scrub if value.respond_to?(:scrub) 12 | 13 | return if value == false || value.present? 14 | 15 | raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank)) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/as_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class AsValidator < Base 7 | # We use a validator for renaming parameters. This is just a marker for 8 | # the parameter scope to handle the renaming. No actual validation 9 | # happens here. 10 | def validate_param!(*); end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/at_least_one_of_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class AtLeastOneOfValidator < MultipleParamsBase 7 | def validate_params!(params) 8 | return unless keys_in_common(params).empty? 9 | 10 | raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/contract_scope_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class ContractScopeValidator < Base 7 | attr_reader :schema 8 | 9 | def initialize(_attrs, _options, _required, _scope, opts) 10 | super 11 | @schema = opts.fetch(:schema) 12 | end 13 | 14 | # Validates a given request. 15 | # @param request [Grape::Request] the request currently being handled 16 | # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed 17 | # @return [void] 18 | def validate(request) 19 | res = schema.call(request.params) 20 | 21 | if res.success? 22 | request.params.deep_merge!(res.to_h) 23 | return 24 | end 25 | 26 | raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) 27 | end 28 | 29 | private 30 | 31 | def build_errors_from_messages(messages) 32 | messages.map do |message| 33 | full_name = message.path.first.to_s 34 | full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 35 | Grape::Exceptions::Validation.new(params: [full_name], message: message.text) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/default_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class DefaultValidator < Base 7 | def initialize(attrs, options, required, scope, opts = {}) 8 | @default = options 9 | super 10 | end 11 | 12 | def validate_param!(attr_name, params) 13 | params[attr_name] = if @default.is_a? Proc 14 | if @default.parameters.empty? 15 | @default.call 16 | else 17 | @default.call(params) 18 | end 19 | elsif @default.frozen? || !@default.duplicable? 20 | @default 21 | else 22 | @default.dup 23 | end 24 | end 25 | 26 | def validate!(params) 27 | attrs = SingleAttributeIterator.new(self, @scope, params) 28 | attrs.each do |resource_params, attr_name| 29 | next unless @scope.meets_dependency?(resource_params, params) 30 | 31 | validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/exactly_one_of_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class ExactlyOneOfValidator < MultipleParamsBase 7 | def validate_params!(params) 8 | keys = keys_in_common(params) 9 | return if keys.length == 1 10 | raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.empty? 11 | 12 | raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/except_values_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class ExceptValuesValidator < Base 7 | def initialize(attrs, options, required, scope, opts) 8 | @except = options.is_a?(Hash) ? options[:value] : options 9 | super 10 | end 11 | 12 | def validate_param!(attr_name, params) 13 | return unless params.try(:key?, attr_name) 14 | 15 | excepts = @except.is_a?(Proc) ? @except.call : @except 16 | return if excepts.nil? 17 | 18 | param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) 19 | raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/length_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class LengthValidator < Base 7 | def initialize(attrs, options, required, scope, opts) 8 | @min = options[:min] 9 | @max = options[:max] 10 | @is = options[:is] 11 | 12 | super 13 | 14 | raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) 15 | raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) 16 | raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max 17 | 18 | return if @is.nil? 19 | raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive? 20 | raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil? 21 | end 22 | 23 | def validate_param!(attr_name, params) 24 | param = params[attr_name] 25 | 26 | return unless param.respond_to?(:length) 27 | 28 | return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is) 29 | 30 | raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) 31 | end 32 | 33 | def build_message 34 | if options_key?(:message) 35 | @option[:message] 36 | elsif @min && @max 37 | format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max 38 | elsif @min 39 | format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min 40 | elsif @max 41 | format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max 42 | else 43 | format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/multiple_params_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class MultipleParamsBase < Base 7 | def validate!(params) 8 | attributes = MultipleAttributesIterator.new(self, @scope, params) 9 | array_errors = [] 10 | 11 | attributes.each do |resource_params| 12 | validate_params!(resource_params) 13 | rescue Grape::Exceptions::Validation => e 14 | array_errors << e 15 | end 16 | 17 | raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? 18 | end 19 | 20 | private 21 | 22 | def keys_in_common(resource_params) 23 | return [] unless resource_params.is_a?(Hash) 24 | 25 | all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } 26 | end 27 | 28 | def all_keys 29 | attrs.map { |attr| @scope.full_name(attr) } 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/mutual_exclusion_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class MutualExclusionValidator < MultipleParamsBase 7 | def validate_params!(params) 8 | keys = keys_in_common(params) 9 | return if keys.length <= 1 10 | 11 | raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/presence_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class PresenceValidator < Base 7 | def validate_param!(attr_name, params) 8 | return if params.try(:key?, attr_name) 9 | 10 | raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/regexp_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class RegexpValidator < Base 7 | def validate_param!(attr_name, params) 8 | return unless params.try(:key?, attr_name) 9 | return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.scrub.match?((options_key?(:value) ? @option[:value] : @option)) } 10 | 11 | raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/same_as_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class SameAsValidator < Base 7 | def validate_param!(attr_name, params) 8 | confirmation = options_key?(:value) ? @option[:value] : @option 9 | return if params[attr_name] == params[confirmation] 10 | 11 | raise Grape::Exceptions::Validation.new( 12 | params: [@scope.full_name(attr_name)], 13 | message: build_message 14 | ) 15 | end 16 | 17 | private 18 | 19 | def build_message 20 | if options_key?(:message) 21 | @option[:message] 22 | else 23 | format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/grape/validations/validators/values_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | module Validations 5 | module Validators 6 | class ValuesValidator < Base 7 | def initialize(attrs, options, required, scope, opts) 8 | @values = options.is_a?(Hash) ? options[:value] : options 9 | super 10 | end 11 | 12 | def validate_param!(attr_name, params) 13 | return unless params.is_a?(Hash) 14 | 15 | val = params[attr_name] 16 | 17 | return if val.nil? && !required_for_root_scope? 18 | 19 | val = val.scrub if val.respond_to?(:scrub) 20 | 21 | # don't forget that +false.blank?+ is true 22 | return if val != false && val.blank? && @allow_blank 23 | 24 | return if check_values?(val, attr_name) 25 | 26 | raise Grape::Exceptions::Validation.new( 27 | params: [@scope.full_name(attr_name)], 28 | message: message(:values) 29 | ) 30 | end 31 | 32 | private 33 | 34 | def check_values?(val, attr_name) 35 | values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values 36 | return true if values.nil? 37 | 38 | param_array = val.nil? ? [nil] : Array.wrap(val) 39 | return param_array.all? { |param| values.include?(param) } unless values.is_a?(Proc) 40 | 41 | begin 42 | param_array.all? { |param| values.call(param) } 43 | rescue StandardError => e 44 | warn "Error '#{e}' raised while validating attribute '#{attr_name}'" 45 | false 46 | end 47 | end 48 | 49 | def required_for_root_scope? 50 | return false unless @required 51 | 52 | scope = @scope 53 | scope = scope.parent while scope.lateral? 54 | 55 | scope.root? 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/grape/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | # The current version of Grape. 5 | VERSION = '2.4.0' 6 | end 7 | -------------------------------------------------------------------------------- /lib/grape/xml.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Grape 4 | if defined?(::MultiXml) 5 | Xml = ::MultiXml 6 | else 7 | Xml = ::ActiveSupport::XmlMini 8 | Xml::ParseError = StandardError 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/config/spec_test_prof.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_prof/recipes/rspec/let_it_be' 4 | 5 | TestProf::BeforeAll.adapter = Class.new do 6 | def begin_transaction; end 7 | 8 | def rollback_transaction; end 9 | end.new 10 | -------------------------------------------------------------------------------- /spec/grape/api/deeply_included_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API do 4 | let(:app) do 5 | main_api = api 6 | Class.new(Grape::API) do 7 | mount main_api 8 | end 9 | end 10 | 11 | let(:api) do 12 | deeply_included_options = options 13 | Class.new(Grape::API) do 14 | include deeply_included_options 15 | 16 | resource :users do 17 | get do 18 | status 200 19 | end 20 | end 21 | end 22 | end 23 | 24 | let(:options) do 25 | deep_included_options_default = default 26 | Module.new do 27 | extend ActiveSupport::Concern 28 | include deep_included_options_default 29 | end 30 | end 31 | 32 | let(:default) do 33 | Module.new do 34 | extend ActiveSupport::Concern 35 | included do 36 | format :json 37 | end 38 | end 39 | end 40 | 41 | it 'works for unspecified format' do 42 | get '/users' 43 | expect(last_response.status).to be 200 44 | expect(last_response.content_type).to eql 'application/json' 45 | end 46 | 47 | it 'works for specified format' do 48 | get '/users.json' 49 | expect(last_response.status).to be 200 50 | expect(last_response.content_type).to eql 'application/json' 51 | end 52 | 53 | it "doesn't work for format different than specified" do 54 | get '/users.txt' 55 | expect(last_response.status).to be 404 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/grape/api/defines_boolean_in_params_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API::Instance do 4 | describe 'boolean constant' do 5 | let(:app) do 6 | Class.new(Grape::API) do 7 | params do 8 | requires :message, type: Grape::API::Boolean 9 | end 10 | post :echo do 11 | { class: params[:message].class.name, value: params[:message] } 12 | end 13 | end 14 | end 15 | 16 | let(:expected_body) do 17 | { class: 'TrueClass', value: true }.to_s 18 | end 19 | 20 | it 'sets Boolean as a type' do 21 | post '/echo?message=true' 22 | expect(last_response.status).to eq(201) 23 | expect(last_response.body).to eq expected_body 24 | end 25 | 26 | context 'Params endpoint type' do 27 | subject { app.new.router.map[Rack::POST].first.options[:params]['message'][:type] } 28 | 29 | it 'params type is a boolean' do 30 | expect(subject).to eq 'Grape::API::Boolean' 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/grape/api/documentation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API do 4 | subject { Class.new(described_class) } 5 | 6 | let(:app) { subject } 7 | 8 | context 'an endpoint with documentation' do 9 | it 'documents parameters' do 10 | subject.params do 11 | requires 'price', type: Float, desc: 'Sales price' 12 | end 13 | subject.get '/' 14 | 15 | expect(subject.routes.first.params['price']).to eq(required: true, 16 | type: 'Float', 17 | desc: 'Sales price') 18 | end 19 | 20 | it 'allows documentation with a hash' do 21 | documentation = { example: 'Joe' } 22 | 23 | subject.params do 24 | requires 'first_name', documentation: documentation 25 | end 26 | subject.get '/' 27 | 28 | expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation) 29 | end 30 | end 31 | 32 | context 'an endpoint without documentation' do 33 | before do 34 | subject.do_not_document! 35 | 36 | subject.params do 37 | requires :city, type: String, desc: 'Should be ignored' 38 | optional :postal_code, type: Integer 39 | end 40 | subject.post '/' do 41 | declared(params).to_json 42 | end 43 | end 44 | 45 | it 'does not document parameters for the endpoint' do 46 | expect(subject.routes.first.params).to eq({}) 47 | end 48 | 49 | it 'still declares params internally' do 50 | data = { city: 'Berlin', postal_code: 10_115 } 51 | 52 | post '/', data 53 | 54 | expect(last_response.body).to eq(data.to_json) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/grape/api/invalid_format_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace do 12 | format :json 13 | content_type :json, 'application/json' 14 | params do 15 | requires :id, desc: 'Identifier.' 16 | end 17 | get ':id' do 18 | { 19 | id: params[:id], 20 | format: params[:format] 21 | } 22 | end 23 | end 24 | end 25 | 26 | context 'get' do 27 | it 'no format' do 28 | get '/foo' 29 | expect(last_response.status).to eq 200 30 | expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: nil)) 31 | end 32 | 33 | it 'json format' do 34 | get '/foo.json' 35 | expect(last_response.status).to eq 200 36 | expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'json')) 37 | end 38 | 39 | it 'invalid format' do 40 | get '/foo.invalid' 41 | expect(last_response.status).to eq 200 42 | expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'invalid')) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/grape/api/namespace_parameters_in_route_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace :me do 12 | namespace :pending do 13 | get '/' do 14 | 'banana' 15 | end 16 | end 17 | put ':id' do 18 | params[:id] 19 | end 20 | end 21 | end 22 | 23 | context 'get' do 24 | it 'responds without ext' do 25 | get '/me/pending' 26 | expect(last_response.status).to eq 200 27 | expect(last_response.body).to eq 'banana' 28 | end 29 | end 30 | 31 | context 'put' do 32 | it 'responds' do 33 | put '/me/foo' 34 | expect(last_response.status).to eq 200 35 | expect(last_response.body).to eq 'foo' 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/grape/api/nested_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API::Helpers do 4 | let(:helper_methods) do 5 | Module.new do 6 | extend Grape::API::Helpers 7 | def current_user 8 | @current_user ||= params[:current_user] 9 | end 10 | end 11 | end 12 | let(:nested) do 13 | context = self 14 | 15 | Class.new(Grape::API) do 16 | resource :level1 do 17 | helpers context.helper_methods 18 | 19 | get do 20 | current_user 21 | end 22 | 23 | resource :level2 do 24 | get do 25 | current_user 26 | end 27 | end 28 | end 29 | end 30 | end 31 | let(:main) do 32 | context = self 33 | 34 | Class.new(Grape::API) do 35 | mount context.nested 36 | end 37 | end 38 | 39 | def app 40 | main 41 | end 42 | 43 | it 'can access helpers from a mounted resource' do 44 | get '/level1', current_user: 'hello' 45 | expect(last_response.body).to eq('hello') 46 | end 47 | 48 | it 'can access helpers from a mounted resource in a nested resource' do 49 | get '/level1/level2', current_user: 'world' 50 | expect(last_response.body).to eq('world') 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/grape/api/optional_parameters_in_route_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace :api do 12 | get ':id(/:ext)' do 13 | [params[:id], params[:ext]].compact.join('/') 14 | end 15 | 16 | put ':id' do 17 | params[:id] 18 | end 19 | end 20 | end 21 | 22 | context 'get' do 23 | it 'responds without ext' do 24 | get '/api/foo' 25 | expect(last_response.status).to eq 200 26 | expect(last_response.body).to eq 'foo' 27 | end 28 | 29 | it 'responds with ext' do 30 | get '/api/foo/bar' 31 | expect(last_response.status).to eq 200 32 | expect(last_response.body).to eq 'foo/bar' 33 | end 34 | end 35 | 36 | context 'put' do 37 | it 'responds' do 38 | put '/api/foo' 39 | expect(last_response.status).to eq 200 40 | expect(last_response.body).to eq 'foo' 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/grape/api/parameters_modification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace :test do 12 | params do 13 | optional :foo, default: +'-abcdef' 14 | end 15 | get do 16 | params[:foo].slice!(0) 17 | params[:foo] 18 | end 19 | end 20 | end 21 | 22 | context 'when route modifies param value' do 23 | it 'param default should not change' do 24 | get '/test' 25 | expect(last_response.status).to eq 200 26 | expect(last_response.body).to eq 'abcdef' 27 | 28 | get '/test' 29 | expect(last_response.status).to eq 200 30 | expect(last_response.body).to eq 'abcdef' 31 | 32 | get '/test?foo=-123456' 33 | expect(last_response.status).to eq 200 34 | expect(last_response.body).to eq '123456' 35 | 36 | get '/test' 37 | expect(last_response.status).to eq 200 38 | expect(last_response.body).to eq 'abcdef' 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/grape/api/patch_method_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API::Helpers do 4 | let(:patch_public) do 5 | Class.new(Grape::API) do 6 | format :json 7 | version 'public-v1', using: :header, vendor: 'grape' 8 | 9 | get do 10 | { ok: 'public' } 11 | end 12 | end 13 | end 14 | let(:auth_methods) do 15 | Module.new do 16 | def authenticate!; end 17 | end 18 | end 19 | let(:patch_private) do 20 | context = self 21 | 22 | Class.new(Grape::API) do 23 | format :json 24 | version 'private-v1', using: :header, vendor: 'grape' 25 | 26 | helpers context.auth_methods 27 | 28 | before do 29 | authenticate! 30 | end 31 | 32 | get do 33 | { ok: 'private' } 34 | end 35 | end 36 | end 37 | let(:main) do 38 | context = self 39 | 40 | Class.new(Grape::API) do 41 | mount context.patch_public 42 | mount context.patch_private 43 | end 44 | end 45 | 46 | def app 47 | main 48 | end 49 | 50 | context 'patch' do 51 | it 'public' do 52 | patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' 53 | expect(last_response.status).to eq 405 54 | end 55 | 56 | it 'private' do 57 | patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' 58 | expect(last_response.status).to eq 405 59 | end 60 | 61 | it 'default' do 62 | patch '/' 63 | expect(last_response.status).to eq 405 64 | end 65 | end 66 | 67 | context 'default' do 68 | it 'public' do 69 | get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' 70 | expect(last_response.status).to eq 200 71 | expect(last_response.body).to eq({ ok: 'public' }.to_json) 72 | end 73 | 74 | it 'private' do 75 | get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' 76 | expect(last_response.status).to eq 200 77 | expect(last_response.body).to eq({ ok: 'private' }.to_json) 78 | end 79 | 80 | it 'default' do 81 | get '/' 82 | expect(last_response.status).to eq 200 83 | expect(last_response.body).to eq({ ok: 'public' }.to_json) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/grape/api/required_parameters_in_route_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace :api do 12 | get ':id' do 13 | [params[:id], params[:ext]].compact.join('/') 14 | end 15 | 16 | put ':something_id' do 17 | params[:something_id] 18 | end 19 | end 20 | end 21 | 22 | context 'get' do 23 | it 'responds' do 24 | get '/api/foo' 25 | expect(last_response.status).to eq 200 26 | expect(last_response.body).to eq 'foo' 27 | end 28 | end 29 | 30 | context 'put' do 31 | it 'responds' do 32 | put '/api/foo' 33 | expect(last_response.status).to eq 200 34 | expect(last_response.body).to eq 'foo' 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/grape/api/required_parameters_with_invalid_method_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | before do 11 | subject.namespace do 12 | params do 13 | requires :id, desc: 'Identifier.' 14 | end 15 | get ':id' do 16 | end 17 | end 18 | end 19 | 20 | context 'post' do 21 | it '405' do 22 | post '/something' 23 | expect(last_response.status).to eq 405 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/grape/api/routes_with_requirements_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Endpoint do 4 | subject { Class.new(Grape::API) } 5 | 6 | def app 7 | subject 8 | end 9 | 10 | context 'get' do 11 | it 'routes to a namespace param with dots' do 12 | subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do 13 | get '/' do 14 | params[:ns_with_dots] 15 | end 16 | end 17 | 18 | get '/test.id.with.dots' 19 | expect(last_response.status).to eq 200 20 | expect(last_response.body).to eq 'test.id.with.dots' 21 | end 22 | 23 | it 'routes to a path with multiple params with dots' do 24 | subject.get ':id_with_dots/:another_id_with_dots', requirements: { id_with_dots: %r{[^/]+}, 25 | another_id_with_dots: %r{[^/]+} } do 26 | "#{params[:id_with_dots]}/#{params[:another_id_with_dots]}" 27 | end 28 | 29 | get '/test.id/test2.id' 30 | expect(last_response.status).to eq 200 31 | expect(last_response.body).to eq 'test.id/test2.id' 32 | end 33 | 34 | it 'routes to namespace and path params with dots, with overridden requirements' do 35 | subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do 36 | get ':another_id_with_dots', requirements: { ns_with_dots: %r{[^/]+}, 37 | another_id_with_dots: %r{[^/]+} } do 38 | "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" 39 | end 40 | end 41 | 42 | get '/test.id/test2.id' 43 | expect(last_response.status).to eq 200 44 | expect(last_response.body).to eq 'test.id/test2.id' 45 | end 46 | 47 | it 'routes to namespace and path params with dots, with merged requirements' do 48 | subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do 49 | get ':another_id_with_dots', requirements: { another_id_with_dots: %r{[^/]+} } do 50 | "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" 51 | end 52 | end 53 | 54 | get '/test.id/test2.id' 55 | expect(last_response.status).to eq 200 56 | expect(last_response.body).to eq 'test.id/test2.id' 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/grape/api/shared_helpers_exactly_one_of_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API::Helpers do 4 | let(:app) do 5 | Class.new(Grape::API) do 6 | helpers Module.new do 7 | extend Grape::API::Helpers 8 | 9 | params :drink do 10 | optional :beer 11 | optional :wine 12 | exactly_one_of :beer, :wine 13 | end 14 | end 15 | format :json 16 | 17 | params do 18 | requires :orderType, type: String, values: %w[food drink] 19 | given orderType: ->(val) { val == 'food' } do 20 | optional :pasta 21 | optional :pizza 22 | exactly_one_of :pasta, :pizza 23 | end 24 | given orderType: ->(val) { val == 'drink' } do 25 | use :drink 26 | end 27 | end 28 | get do 29 | declared(params, include_missing: true) 30 | end 31 | end 32 | end 33 | 34 | it 'defines parameters' do 35 | get '/', orderType: 'food', pizza: 'mista' 36 | expect(last_response.status).to eq 200 37 | expect(last_response.body).to eq({ orderType: 'food', 38 | pasta: nil, pizza: 'mista', 39 | beer: nil, wine: nil }.to_json) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/grape/api/shared_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API::Helpers do 4 | subject do 5 | shared_params = Module.new do 6 | extend Grape::API::Helpers 7 | 8 | params :pagination do 9 | optional :page, type: Integer 10 | optional :size, type: Integer 11 | end 12 | end 13 | 14 | Class.new(Grape::API) do 15 | helpers shared_params 16 | format :json 17 | 18 | params do 19 | use :pagination 20 | end 21 | get do 22 | declared(params, include_missing: true) 23 | end 24 | end 25 | end 26 | 27 | def app 28 | subject 29 | end 30 | 31 | it 'defines parameters' do 32 | get '/' 33 | expect(last_response.status).to eq 200 34 | expect(last_response.body).to eq({ page: nil, size: nil }.to_json) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/grape/content_types_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::ContentTypes do 4 | describe 'DEFAULTS' do 5 | subject { described_class::DEFAULTS } 6 | 7 | let(:expected_value) do 8 | { 9 | xml: 'application/xml', 10 | serializable_hash: 'application/json', 11 | json: 'application/json', 12 | binary: 'application/octet-stream', 13 | txt: 'text/plain' 14 | }.freeze 15 | end 16 | 17 | it { is_expected.to eq(expected_value) } 18 | end 19 | 20 | describe 'MIME_TYPES' do 21 | subject { described_class::MIME_TYPES } 22 | 23 | let(:expected_value) do 24 | { 25 | 'application/xml' => :xml, 26 | 'application/json' => :json, 27 | 'application/octet-stream' => :binary, 28 | 'text/plain' => :txt 29 | }.freeze 30 | end 31 | 32 | it { is_expected.to eq(expected_value) } 33 | end 34 | 35 | describe '.content_types_for' do 36 | subject { described_class.content_types_for(from_settings) } 37 | 38 | context 'when from_settings is present' do 39 | let(:from_settings) { { a: :b } } 40 | 41 | it { is_expected.to eq(from_settings) } 42 | end 43 | 44 | context 'when from_settings is not present' do 45 | let(:from_settings) { nil } 46 | 47 | it { is_expected.to be(described_class::DEFAULTS) } 48 | end 49 | end 50 | 51 | describe '.mime_types_for' do 52 | subject { described_class.mime_types_for(from_settings) } 53 | 54 | context 'when from_settings is equal to Grape::ContentTypes::DEFAULTS' do 55 | let(:from_settings) do 56 | { 57 | xml: 'application/xml', 58 | serializable_hash: 'application/json', 59 | json: 'application/json', 60 | binary: 'application/octet-stream', 61 | txt: 'text/plain' 62 | }.freeze 63 | end 64 | 65 | it { is_expected.to be(described_class::MIME_TYPES) } 66 | end 67 | 68 | context 'when from_settings is not equal to Grape::ContentTypes::DEFAULTS' do 69 | let(:from_settings) do 70 | { 71 | xml: 'application/xml;charset=utf-8' 72 | } 73 | end 74 | 75 | it { is_expected.to eq('application/xml' => :xml) } 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/grape/dsl/callbacks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::DSL::Callbacks do 4 | subject { dummy_class } 5 | 6 | let(:dummy_class) do 7 | Class.new do 8 | include Grape::DSL::Callbacks 9 | end 10 | end 11 | 12 | let(:proc) { -> {} } 13 | 14 | describe '.before' do 15 | it 'adds a block to "before"' do 16 | expect(subject).to receive(:namespace_stackable).with(:befores, proc) 17 | subject.before(&proc) 18 | end 19 | end 20 | 21 | describe '.before_validation' do 22 | it 'adds a block to "before_validation"' do 23 | expect(subject).to receive(:namespace_stackable).with(:before_validations, proc) 24 | subject.before_validation(&proc) 25 | end 26 | end 27 | 28 | describe '.after_validation' do 29 | it 'adds a block to "after_validation"' do 30 | expect(subject).to receive(:namespace_stackable).with(:after_validations, proc) 31 | subject.after_validation(&proc) 32 | end 33 | end 34 | 35 | describe '.after' do 36 | it 'adds a block to "after"' do 37 | expect(subject).to receive(:namespace_stackable).with(:afters, proc) 38 | subject.after(&proc) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/grape/dsl/headers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::DSL::Headers do 4 | subject { dummy_class.new } 5 | 6 | let(:dummy_class) do 7 | Class.new do 8 | include Grape::DSL::Headers 9 | end 10 | end 11 | 12 | let(:header_data) do 13 | { 'first key' => 'First Value', 14 | 'second key' => 'Second Value' } 15 | end 16 | 17 | context 'when headers are set' do 18 | describe '#header' do 19 | before do 20 | header_data.each { |k, v| subject.header(k, v) } 21 | end 22 | 23 | describe 'get' do 24 | it 'returns a specifc value' do 25 | expect(subject.header['first key']).to eq 'First Value' 26 | expect(subject.header['second key']).to eq 'Second Value' 27 | end 28 | 29 | it 'returns all set headers' do 30 | expect(subject.header).to eq header_data 31 | expect(subject.headers).to eq header_data 32 | end 33 | end 34 | 35 | describe 'set' do 36 | it 'returns value' do 37 | expect(subject.header('third key', 'Third Value')) 38 | expect(subject.header['third key']).to eq 'Third Value' 39 | end 40 | end 41 | 42 | describe 'delete' do 43 | it 'deletes a header key-value pair' do 44 | expect(subject.header('first key')).to eq header_data['first key'] 45 | expect(subject.header).not_to have_key('first key') 46 | end 47 | end 48 | end 49 | end 50 | 51 | context 'when no headers are set' do 52 | describe '#header' do 53 | it 'returns nil' do 54 | expect(subject.header['first key']).to be_nil 55 | expect(subject.header('first key')).to be_nil 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/grape/dsl/logger_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::DSL::Logger do 4 | subject { Class.new(dummy_logger) } 5 | 6 | let(:dummy_logger) do 7 | Class.new do 8 | extend Grape::DSL::Logger 9 | end 10 | end 11 | 12 | let(:logger) { instance_double(Logger) } 13 | 14 | describe '.logger' do 15 | it 'sets a logger' do 16 | subject.logger logger 17 | expect(subject.logger).to eq logger 18 | end 19 | 20 | it 'returns a logger' do 21 | expect(subject.logger(logger)).to eq logger 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/grape/dsl/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::DSL::Middleware do 4 | subject { dummy_class } 5 | 6 | let(:dummy_class) do 7 | Class.new do 8 | include Grape::DSL::Middleware 9 | end 10 | end 11 | 12 | let(:proc) { -> {} } 13 | let(:foo_middleware) { Class.new } 14 | let(:bar_middleware) { Class.new } 15 | 16 | describe '.use' do 17 | it 'adds a middleware with the right operation' do 18 | expect(subject).to receive(:namespace_stackable).with(:middleware, [:use, foo_middleware, :arg1, proc]) 19 | 20 | subject.use foo_middleware, :arg1, &proc 21 | end 22 | end 23 | 24 | describe '.insert' do 25 | it 'adds a middleware with the right operation' do 26 | expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert, 0, :arg1, proc]) 27 | 28 | subject.insert 0, :arg1, &proc 29 | end 30 | end 31 | 32 | describe '.insert_before' do 33 | it 'adds a middleware with the right operation' do 34 | expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_before, foo_middleware, :arg1, proc]) 35 | 36 | subject.insert_before foo_middleware, :arg1, &proc 37 | end 38 | end 39 | 40 | describe '.insert_after' do 41 | it 'adds a middleware with the right operation' do 42 | expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_after, foo_middleware, :arg1, proc]) 43 | 44 | subject.insert_after foo_middleware, :arg1, &proc 45 | end 46 | end 47 | 48 | describe '.middleware' do 49 | it 'returns the middleware stack' do 50 | subject.use foo_middleware, :arg1, &proc 51 | subject.insert_before bar_middleware, :arg1, :arg2 52 | 53 | expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]] 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/grape/exceptions/invalid_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::InvalidFormatter do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new(String, 'xml') 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include( 11 | 'cannot convert String to xml' 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/grape/exceptions/invalid_response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::InvalidResponse do 4 | describe '#message' do 5 | let(:error) { described_class.new } 6 | 7 | it 'contains the problem in the message' do 8 | expect(error.message).to include('Invalid response') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/grape/exceptions/invalid_versioner_option_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::InvalidVersionerOption do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new('headers') 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include( 11 | 'unknown :using for versioner: headers' 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/grape/exceptions/missing_group_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared/deprecated_class_examples' 4 | 5 | RSpec.describe Grape::Exceptions::MissingGroupType do 6 | describe '#message' do 7 | subject { described_class.new.message } 8 | 9 | it { is_expected.to include 'group type is required' } 10 | end 11 | 12 | describe 'Grape::Exceptions::MissingGroupTypeError' do 13 | let(:deprecated_class) { Grape::Exceptions::MissingGroupTypeError } 14 | 15 | it_behaves_like 'deprecated class' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/grape/exceptions/missing_mime_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::MissingMimeType do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new('new_json') 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include 'missing mime type for new_json' 11 | end 12 | 13 | it 'contains the resolution in the message' do 14 | expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' " 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/grape/exceptions/missing_option_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::MissingOption do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new(:path) 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include( 11 | 'you must specify :path options' 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/grape/exceptions/unknown_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::UnknownOptions do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new(%i[a b]) 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include( 11 | 'unknown options: ' 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/grape/exceptions/unknown_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::UnknownValidator do 4 | describe '#message' do 5 | let(:error) do 6 | described_class.new('gt_10') 7 | end 8 | 9 | it 'contains the problem in the message' do 10 | expect(error.message).to include( 11 | 'unknown validator: gt_10' 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/grape/exceptions/unsupported_group_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shared/deprecated_class_examples' 4 | 5 | RSpec.describe Grape::Exceptions::UnsupportedGroupType do 6 | subject { described_class.new } 7 | 8 | describe '#message' do 9 | subject { described_class.new.message } 10 | 11 | it { is_expected.to include 'group type must be Array, Hash, JSON or Array[JSON]' } 12 | end 13 | 14 | describe 'Grape::Exceptions::UnsupportedGroupTypeError' do 15 | let(:deprecated_class) { Grape::Exceptions::UnsupportedGroupTypeError } 16 | 17 | it_behaves_like 'deprecated class' 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/grape/exceptions/validation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Exceptions::Validation do 4 | it 'fails when params are missing' do 5 | expect { described_class.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) 6 | end 7 | 8 | context 'when message is a symbol' do 9 | it 'stores message_key' do 10 | expect(described_class.new(params: ['id'], message: :presence).message_key).to eq(:presence) 11 | end 12 | end 13 | 14 | context 'when message is a String' do 15 | it 'does not store the message_key' do 16 | expect(described_class.new(params: ['id'], message: 'presence').message_key).to be_nil 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/grape/extensions/param_builders/hash_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Extensions::Hash::ParamBuilder do 4 | describe 'deprecation' do 5 | context 'when included' do 6 | subject do 7 | Class.new(Grape::API) do 8 | include Grape::Extensions::Hash::ParamBuilder 9 | end 10 | end 11 | 12 | let(:message) do 13 | 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' 14 | end 15 | 16 | it 'raises a deprecation' do 17 | expect(Grape.deprecator).to receive(:warn).with(message).and_raise(ActiveSupport::DeprecationException, :deprecated) 18 | expect { subject }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') 19 | end 20 | end 21 | 22 | context 'when using class name' do 23 | let(:app) do 24 | Class.new(Grape::API) do 25 | params do 26 | build_with Grape::Extensions::Hash::ParamBuilder 27 | end 28 | get 29 | end 30 | end 31 | 32 | it 'raises a deprecation' do 33 | expect(Grape.deprecator).to receive(:warn).with("#{described_class} has been deprecated. Use short name :hash instead.").and_raise(ActiveSupport::DeprecationException, :deprecated) 34 | expect { get '/' }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do 4 | describe 'deprecation' do 5 | context 'when included' do 6 | subject do 7 | Class.new(Grape::API) do 8 | include Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder 9 | end 10 | end 11 | 12 | let(:message) do 13 | 'This concern has been deprecated. Use `build_with` with one of the following short_name (:hash, :hash_with_indifferent_access, :hashie_mash) instead.' 14 | end 15 | 16 | it 'raises a deprecation' do 17 | expect(Grape.deprecator).to receive(:warn).with(message).and_raise(ActiveSupport::DeprecationException, :deprecated) 18 | expect { subject }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') 19 | end 20 | end 21 | end 22 | 23 | context 'when using class name' do 24 | let(:app) do 25 | Class.new(Grape::API) do 26 | params do 27 | build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder 28 | end 29 | get 30 | end 31 | end 32 | 33 | it 'raises a deprecation' do 34 | expect(Grape.deprecator).to receive(:warn).with("#{described_class} has been deprecated. Use short name :hash_with_indifferent_access instead.").and_raise(ActiveSupport::DeprecationException, :deprecated) 35 | expect { get '/' }.to raise_error(ActiveSupport::DeprecationException, 'deprecated') 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/grape/integration/global_namespace_function_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # see https://github.com/ruby-grape/grape/issues/1348 4 | 5 | def namespace 6 | raise 7 | end 8 | 9 | describe Grape::API do 10 | subject do 11 | Class.new(Grape::API) do 12 | format :json 13 | get do 14 | { ok: true } 15 | end 16 | end 17 | end 18 | 19 | def app 20 | subject 21 | end 22 | 23 | context 'with a global namespace function' do 24 | it 'works' do 25 | get '/' 26 | expect(last_response.status).to eq 200 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/grape/integration/rack_sendfile_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Rack::Sendfile do 4 | subject do 5 | content_object = file_object 6 | app = Class.new(Grape::API) do 7 | use Rack::Sendfile 8 | format :json 9 | get do 10 | if content_object.is_a?(String) 11 | sendfile content_object 12 | else 13 | stream content_object 14 | end 15 | end 16 | end 17 | 18 | options = { 19 | method: Rack::GET, 20 | 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', 21 | 'HTTP_X_ACCEL_MAPPING' => '/accel/mapping/=/replaced/' 22 | } 23 | env = Rack::MockRequest.env_for('/', options) 24 | app.call(env) 25 | end 26 | 27 | context 'when calling sendfile' do 28 | let(:file_object) do 29 | '/accel/mapping/some/path' 30 | end 31 | 32 | it 'contains Sendfile headers' do 33 | headers = subject[1] 34 | expect(headers).to include('X-Accel-Redirect') 35 | end 36 | end 37 | 38 | context 'when streaming non file content' do 39 | let(:file_object) do 40 | double(:file_object, each: nil) 41 | end 42 | 43 | it 'not contains Sendfile headers' do 44 | headers = subject[1] 45 | expect(headers).not_to include('X-Accel-Redirect') 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/grape/integration/rack_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Rack do 4 | describe 'from a Tempfile' do 5 | subject { last_response.body } 6 | 7 | let(:app) do 8 | Class.new(Grape::API) do 9 | format :json 10 | 11 | params do 12 | requires :file, type: File 13 | end 14 | 15 | post do 16 | params[:file].then do |file| 17 | { 18 | filename: file[:filename], 19 | type: file[:type], 20 | content: file[:tempfile].read 21 | } 22 | end 23 | end 24 | end 25 | end 26 | 27 | let(:response_body) do 28 | { 29 | filename: File.basename(tempfile.path), 30 | type: 'text/plain', 31 | content: 'rubbish' 32 | }.to_json 33 | end 34 | 35 | let(:tempfile) do 36 | Tempfile.new.tap do |t| 37 | t.write('rubbish') 38 | t.rewind 39 | end 40 | end 41 | 42 | before do 43 | post '/', file: Rack::Test::UploadedFile.new(tempfile.path, 'text/plain') 44 | end 45 | 46 | it 'correctly populates params from a Tempfile' do 47 | expect(subject).to eq(response_body) 48 | ensure 49 | tempfile.close! 50 | end 51 | end 52 | 53 | context 'when the app is mounted' do 54 | let(:ping_mount) do 55 | Class.new(Grape::API) do 56 | get 'ping' 57 | end 58 | end 59 | 60 | let(:app) do 61 | app_to_mount = ping_mount 62 | Class.new(Grape::API) do 63 | namespace 'namespace' do 64 | mount app_to_mount 65 | end 66 | end 67 | end 68 | 69 | it 'finds the app on the namespace' do 70 | get '/namespace/ping' 71 | expect(last_response).to be_successful 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/grape/loading_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API do 4 | subject do 5 | context = self 6 | 7 | Class.new(Grape::API) do 8 | format :json 9 | mount context.combined_api => '/' 10 | end 11 | end 12 | 13 | let(:jobs_api) do 14 | Class.new(Grape::API) do 15 | namespace :one do 16 | namespace :two do 17 | namespace :three do 18 | get :one do 19 | end 20 | 21 | get :two do 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | 29 | let(:combined_api) do 30 | context = self 31 | 32 | Class.new(Grape::API) do 33 | version :v1, using: :accept_version_header, cascade: true 34 | mount context.jobs_api 35 | end 36 | end 37 | 38 | def app 39 | subject 40 | end 41 | 42 | it 'execute first request in reasonable time' do 43 | started = Time.now 44 | get '/mount1/nested/test_method' 45 | expect(Time.now - started).to be < 5 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/grape/middleware/auth/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Auth::Base do 4 | subject do 5 | Class.new(Grape::API) do 6 | http_basic realm: 'my_realm' do |user, password| 7 | user && password && user == password 8 | end 9 | get '/authorized' do 10 | 'DONE' 11 | end 12 | end 13 | end 14 | 15 | let(:app) { subject } 16 | 17 | it 'authenticates if given valid creds' do 18 | get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') 19 | expect(last_response).to be_successful 20 | expect(last_response.body).to eq('DONE') 21 | end 22 | 23 | it 'throws a 401 is wrong auth is given' do 24 | get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') 25 | expect(last_response).to be_unauthorized 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/grape/middleware/auth/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Auth::DSL do 4 | subject { Class.new(Grape::API) } 5 | 6 | let(:block) { -> {} } 7 | let(:settings) do 8 | { 9 | opaque: 'secret', 10 | proc: block, 11 | realm: 'API Authorization', 12 | type: :http_digest 13 | } 14 | end 15 | 16 | describe '.auth' do 17 | it 'sets auth parameters' do 18 | expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) 19 | 20 | subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] 21 | expect(subject.auth).to eq(settings) 22 | end 23 | 24 | it 'can be called multiple times' do 25 | expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) 26 | expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings.merge(realm: 'super_secret')) 27 | 28 | subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] 29 | first_settings = subject.auth 30 | 31 | subject.auth :http_digest, realm: 'super_secret', opaque: settings[:opaque], &settings[:proc] 32 | 33 | expect(subject.auth).to eq(settings.merge(realm: 'super_secret')) 34 | expect(subject.auth.object_id).not_to eq(first_settings.object_id) 35 | end 36 | end 37 | 38 | describe '.http_basic' do 39 | it 'sets auth parameters' do 40 | subject.http_basic realm: 'my_realm', &settings[:proc] 41 | expect(subject.auth).to eq(realm: 'my_realm', type: :http_basic, proc: block) 42 | end 43 | end 44 | 45 | describe '.http_digest' do 46 | context 'when realm is a hash' do 47 | it 'sets auth parameters' do 48 | subject.http_digest realm: { realm: 'my_realm', opaque: 'my_opaque' }, &settings[:proc] 49 | expect(subject.auth).to eq(realm: { realm: 'my_realm', opaque: 'my_opaque' }, type: :http_digest, proc: block) 50 | end 51 | end 52 | 53 | context 'when realm is not hash' do 54 | it 'sets auth parameters' do 55 | subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc] 56 | expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque') 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/grape/middleware/auth/strategies_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Auth::Strategies do 4 | describe 'Basic Auth' do 5 | let(:app) do 6 | proc = ->(u, p) { u && p && u == p } 7 | Rack::Builder.app do 8 | use Grape::Middleware::Error 9 | use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc) 10 | run ->(_env) { [200, {}, ['Hello there.']] } 11 | end 12 | end 13 | 14 | it 'throws a 401 if no auth is given' do 15 | get '/whatever' 16 | expect(last_response).to be_unauthorized 17 | end 18 | 19 | it 'authenticates if given valid creds' do 20 | get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') 21 | expect(last_response).to be_successful 22 | expect(last_response.body).to eq('Hello there.') 23 | end 24 | 25 | it 'throws a 401 is wrong auth is given' do 26 | get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') 27 | expect(last_response).to be_unauthorized 28 | end 29 | end 30 | 31 | describe 'Unknown Auth' do 32 | context 'when type is not register' do 33 | let(:app) do 34 | Class.new(Grape::API) do 35 | use Grape::Middleware::Auth::Base, type: :unknown 36 | get('/whatever') { 'Hello there.' } 37 | end 38 | end 39 | 40 | it 'throws a 401' do 41 | expect { get '/whatever' }.to raise_error(Grape::Exceptions::UnknownAuthStrategy, 'unknown auth strategy: unknown') 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/grape/middleware/error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Error do 4 | let(:error_entity) do 5 | Class.new(Grape::Entity) do 6 | expose :code 7 | expose :static 8 | 9 | def static 10 | 'static text' 11 | end 12 | end 13 | end 14 | let(:err_app) do 15 | Class.new do 16 | class << self 17 | attr_accessor :error, :format 18 | 19 | def call(_env) 20 | throw :error, error 21 | end 22 | end 23 | end 24 | end 25 | let(:options) { { default_message: 'Aww, hamburgers.' } } 26 | 27 | let(:app) do 28 | opts = options 29 | context = self 30 | Rack::Builder.app do 31 | use Spec::Support::EndpointFaker 32 | use Grape::Middleware::Error, **opts # rubocop:disable RSpec/DescribedClass 33 | run context.err_app 34 | end 35 | end 36 | 37 | it 'sets the status code appropriately' do 38 | err_app.error = { status: 410 } 39 | get '/' 40 | expect(last_response.status).to eq(410) 41 | end 42 | 43 | it 'sets the status code based on the rack util status code symbol' do 44 | err_app.error = { status: :gone } 45 | get '/' 46 | expect(last_response.status).to eq(410) 47 | end 48 | 49 | it 'sets the error message appropriately' do 50 | err_app.error = { message: 'Awesome stuff.' } 51 | get '/' 52 | expect(last_response.body).to eq('Awesome stuff.') 53 | end 54 | 55 | it 'defaults to a 500 status' do 56 | err_app.error = {} 57 | get '/' 58 | expect(last_response).to be_server_error 59 | end 60 | 61 | it 'has a default message' do 62 | err_app.error = {} 63 | get '/' 64 | expect(last_response.body).to eq('Aww, hamburgers.') 65 | end 66 | 67 | context 'with http code' do 68 | let(:options) { { default_message: 'Aww, hamburgers.' } } 69 | 70 | it 'adds the status code if wanted' do 71 | err_app.error = { message: { code: 200 } } 72 | get '/' 73 | 74 | expect(last_response.body).to eq({ code: 200 }.to_json) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/grape/middleware/globals_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Globals do 4 | subject { described_class.new(blank_app) } 5 | 6 | before { allow(subject).to receive(:dup).and_return(subject) } 7 | 8 | let(:blank_app) { ->(_env) { [200, {}, 'Hi there.'] } } 9 | 10 | it 'calls through to the app' do 11 | expect(subject.call({})).to eq([200, {}, 'Hi there.']) 12 | end 13 | 14 | context 'environment' do 15 | it 'sets the grape.request environment' do 16 | subject.call({}) 17 | expect(subject.env[Grape::Env::GRAPE_REQUEST]).to be_a(Grape::Request) 18 | end 19 | 20 | it 'sets the grape.request.headers environment' do 21 | subject.call({}) 22 | expect(subject.env[Grape::Env::GRAPE_REQUEST_HEADERS]).to be_a(Hash) 23 | end 24 | 25 | it 'sets the grape.request.params environment' do 26 | subject.call(Rack::QUERY_STRING => 'test=1', Rack::RACK_INPUT => StringIO.new) 27 | expect(subject.env[Grape::Env::GRAPE_REQUEST_PARAMS]).to be_a(Hash) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/grape/middleware/versioner/path_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Versioner::Path do 4 | subject { described_class.new(app, **options) } 5 | 6 | let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } 7 | let(:options) { {} } 8 | 9 | it 'sets the API version based on the first path' do 10 | expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') 11 | end 12 | 13 | it 'does not cut the version out of the path' do 14 | expect(subject.call(Rack::PATH_INFO => '/v1/awesome')[1][Rack::PATH_INFO]).to eq('/v1/awesome') 15 | end 16 | 17 | it 'provides a nil version if no path is given' do 18 | expect(subject.call(Rack::PATH_INFO => '/').last).to be_nil 19 | end 20 | 21 | context 'with a pattern' do 22 | let(:options) { { pattern: /v./i } } 23 | 24 | it 'sets the version if it matches' do 25 | expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') 26 | end 27 | 28 | it 'ignores the version if it fails to match' do 29 | expect(subject.call(Rack::PATH_INFO => '/awesome/radical').last).to be_nil 30 | end 31 | end 32 | 33 | [%w[v1 v2], %i[v1 v2], [:v1, 'v2'], ['v1', :v2]].each do |versions| 34 | context "with specified versions as #{versions}" do 35 | let(:options) { { versions: versions } } 36 | 37 | it 'throws an error if a non-allowed version is specified' do 38 | expect(catch(:error) { subject.call(Rack::PATH_INFO => '/v3/awesome') }[:status]).to eq(404) 39 | end 40 | 41 | it 'allows versions that have been specified' do 42 | expect(subject.call(Rack::PATH_INFO => '/v1/asoasd').last).to eq('v1') 43 | end 44 | end 45 | end 46 | 47 | context 'with prefix, but requested version is not matched' do 48 | let(:options) { { prefix: '/v1', pattern: /v./i } } 49 | 50 | it 'recognizes potential version' do 51 | expect(subject.call(Rack::PATH_INFO => '/v3/foo').last).to eq('v3') 52 | end 53 | end 54 | 55 | context 'with mount path' do 56 | let(:options) { { mount_path: '/mounted', versions: [:v1] } } 57 | 58 | it 'recognizes potential version' do 59 | expect(subject.call(Rack::PATH_INFO => '/mounted/v1/foo').last).to eq('v1') 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/grape/middleware/versioner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Middleware::Versioner do 4 | subject { described_class.using(strategy) } 5 | 6 | context 'when :path' do 7 | let(:strategy) { :path } 8 | 9 | it { is_expected.to eq(Grape::Middleware::Versioner::Path) } 10 | end 11 | 12 | context 'when :header' do 13 | let(:strategy) { :header } 14 | 15 | it { is_expected.to eq(Grape::Middleware::Versioner::Header) } 16 | end 17 | 18 | context 'when :param' do 19 | let(:strategy) { :param } 20 | 21 | it { is_expected.to eq(Grape::Middleware::Versioner::Param) } 22 | end 23 | 24 | context 'when :accept_version_header' do 25 | let(:strategy) { :accept_version_header } 26 | 27 | it { is_expected.to eq(Grape::Middleware::Versioner::AcceptVersionHeader) } 28 | end 29 | 30 | context 'when unknown' do 31 | let(:strategy) { :unknown } 32 | 33 | it 'raises an error' do 34 | expect { subject }.to raise_error Grape::Exceptions::InvalidVersionerOption, Grape::Exceptions::InvalidVersionerOption.new(strategy).message 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/grape/named_api_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API do 4 | subject(:api_name) { NamedAPI.endpoints.last.options[:for].to_s } 5 | 6 | let(:api) do 7 | Class.new(Grape::API) do 8 | get 'test' do 9 | 'response' 10 | end 11 | end 12 | end 13 | 14 | let(:name) { 'NamedAPI' } 15 | 16 | before { stub_const(name, api) } 17 | 18 | it 'can access the name of the API' do 19 | expect(api_name).to eq name 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/grape/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Parser do 4 | subject { described_class } 5 | 6 | describe '.parser_for' do 7 | let(:options) { {} } 8 | 9 | it 'returns parser correctly' do 10 | expect(subject.parser_for(:json)).to eq(Grape::Parser::Json) 11 | end 12 | 13 | context 'when parser is available' do 14 | let(:parsers) do 15 | { customized_json: Grape::Parser::Json } 16 | end 17 | 18 | it 'returns registered parser if available' do 19 | expect(subject.parser_for(:customized_json, parsers)).to eq(Grape::Parser::Json) 20 | end 21 | end 22 | 23 | context 'when parser does not exist' do 24 | it 'returns nil' do 25 | expect(subject.parser_for(:undefined)).to be_nil 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/grape/presenters/presenter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Presenters::Presenter do 4 | subject { dummy_class.new } 5 | 6 | let(:dummy_class) do 7 | Class.new do 8 | include Grape::DSL::InsideRoute 9 | 10 | attr_reader :env, :request, :new_settings 11 | 12 | def initialize 13 | @env = {} 14 | @header = {} 15 | @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } 16 | end 17 | end 18 | end 19 | 20 | describe 'represent' do 21 | let(:object_mock) do 22 | Object.new 23 | end 24 | 25 | it 'represent object' do 26 | expect(described_class.represent(object_mock)).to eq object_mock 27 | end 28 | end 29 | 30 | describe 'present' do 31 | let(:hash_mock) do 32 | { key: :value } 33 | end 34 | 35 | describe 'instance' do 36 | before do 37 | subject.present hash_mock, with: described_class 38 | end 39 | 40 | it 'presents dummy hash' do 41 | expect(subject.body).to eq hash_mock 42 | end 43 | end 44 | 45 | describe 'multiple presenter' do 46 | let(:hash_mock1) do 47 | { key1: :value1 } 48 | end 49 | 50 | let(:hash_mock2) do 51 | { key2: :value2 } 52 | end 53 | 54 | describe 'instance' do 55 | before do 56 | subject.present hash_mock1, with: described_class 57 | subject.present hash_mock2, with: described_class 58 | end 59 | 60 | it 'presents both dummy presenter' do 61 | expect(subject.body[:key1]).to eq hash_mock1[:key1] 62 | expect(subject.body[:key2]).to eq hash_mock2[:key2] 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/grape/router/greedy_route_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Grape::Router::GreedyRoute do 4 | let(:instance) { described_class.new(pattern, options) } 5 | let(:index) { 0 } 6 | let(:pattern) { :pattern } 7 | let(:params) do 8 | { a_param: 1 }.freeze 9 | end 10 | let(:options) do 11 | { params: params }.freeze 12 | end 13 | 14 | describe '#pattern' do 15 | subject { instance.pattern } 16 | 17 | it { is_expected.to eq(pattern) } 18 | end 19 | 20 | describe '#options' do 21 | subject { instance.options } 22 | 23 | it { is_expected.to eq(options) } 24 | end 25 | 26 | describe '#params' do 27 | subject { instance.params } 28 | 29 | it { is_expected.to eq(params) } 30 | end 31 | 32 | describe '#attributes' do 33 | subject { instance.attributes } 34 | 35 | it { is_expected.to eq(options) } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/grape/router_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Router do 4 | describe '.normalize_path' do 5 | subject { described_class.normalize_path(path) } 6 | 7 | context 'when no leading slash' do 8 | let(:path) { 'foo%20bar%20baz' } 9 | 10 | it { is_expected.to eq '/foo%20bar%20baz' } 11 | end 12 | 13 | context 'when path ends with slash' do 14 | let(:path) { '/foo%20bar%20baz/' } 15 | 16 | it { is_expected.to eq '/foo%20bar%20baz' } 17 | end 18 | 19 | context 'when path has recurring slashes' do 20 | let(:path) { '////foo%20bar%20baz' } 21 | 22 | it { is_expected.to eq '/foo%20bar%20baz' } 23 | end 24 | 25 | context 'when not greedy' do 26 | let(:path) { '/foo%20bar%20baz' } 27 | 28 | it { is_expected.to eq '/foo%20bar%20baz' } 29 | end 30 | 31 | context 'when encoded string in lowercase' do 32 | let(:path) { '/foo%aabar%aabaz' } 33 | 34 | it { is_expected.to eq '/foo%AAbar%AAbaz' } 35 | end 36 | 37 | context 'when nil' do 38 | let(:path) { nil } 39 | 40 | it { is_expected.to eq '/' } 41 | end 42 | 43 | context 'when empty string' do 44 | let(:path) { '' } 45 | 46 | it { is_expected.to eq '/' } 47 | end 48 | 49 | context 'when encoding is different' do 50 | subject { described_class.normalize_path(path).encoding } 51 | 52 | let(:path) { '/foo%AAbar%AAbaz'.b } 53 | 54 | it { is_expected.to eq(Encoding::BINARY) } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/grape/util/inheritable_values_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Util::InheritableValues do 4 | subject { described_class.new(parent) } 5 | 6 | let(:parent) { described_class.new } 7 | 8 | describe '#delete' do 9 | it 'deletes a key' do 10 | subject[:some_thing] = :new_foo_bar 11 | subject.delete :some_thing 12 | expect(subject[:some_thing]).to be_nil 13 | end 14 | 15 | it 'does not delete parent values' do 16 | parent[:some_thing] = :foo 17 | subject[:some_thing] = :new_foo_bar 18 | subject.delete :some_thing 19 | expect(subject[:some_thing]).to eq :foo 20 | end 21 | end 22 | 23 | describe '#[]' do 24 | it 'returns a value' do 25 | subject[:some_thing] = :foo 26 | expect(subject[:some_thing]).to eq :foo 27 | end 28 | 29 | it 'returns parent value when no value is set' do 30 | parent[:some_thing] = :foo 31 | expect(subject[:some_thing]).to eq :foo 32 | end 33 | 34 | it 'overwrites parent value with the current one' do 35 | parent[:some_thing] = :foo 36 | subject[:some_thing] = :foo_bar 37 | expect(subject[:some_thing]).to eq :foo_bar 38 | end 39 | 40 | it 'parent values are not changed' do 41 | parent[:some_thing] = :foo 42 | subject[:some_thing] = :foo_bar 43 | expect(parent[:some_thing]).to eq :foo 44 | end 45 | end 46 | 47 | describe '#[]=' do 48 | it 'sets a value' do 49 | subject[:some_thing] = :foo 50 | expect(subject[:some_thing]).to eq :foo 51 | end 52 | end 53 | 54 | describe '#to_hash' do 55 | it 'returns a Hash representation' do 56 | parent[:some_thing] = :foo 57 | subject[:some_thing_more] = :foo_bar 58 | expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar) 59 | end 60 | end 61 | 62 | describe '#clone' do 63 | let(:obj_cloned) { subject.clone } 64 | 65 | context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do 66 | let(:description) { { entity: double } } 67 | 68 | before { subject[:description] = description } 69 | 70 | it 'copies values; does not duplicate them' do 71 | expect(obj_cloned[:description]).to eq description 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/grape/util/strict_hash_configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Util::StrictHashConfiguration do 4 | subject do 5 | Class.new do 6 | include Grape::Util::StrictHashConfiguration.module(:config1, :config2, config3: [:config4], config5: [config6: %i[config7 config8]]) 7 | end 8 | end 9 | 10 | it 'set nested configs' do 11 | subject.configure do 12 | config1 'alpha' 13 | config2 'beta' 14 | 15 | config3 do 16 | config4 'gamma' 17 | end 18 | 19 | local_var = 8 20 | 21 | config5 do 22 | config6 do 23 | config7 7 24 | config8 local_var 25 | end 26 | end 27 | end 28 | 29 | expect(subject.settings).to eq(config1: 'alpha', 30 | config2: 'beta', 31 | config3: { config4: 'gamma' }, 32 | config5: { config6: { config7: 7, config8: 8 } }) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/grape/validations/multiple_attributes_iterator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::MultipleAttributesIterator do 4 | describe '#each' do 5 | subject(:iterator) { described_class.new(validator, scope, params) } 6 | 7 | let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } 8 | let(:validator) { double(attrs: %i[first second third]) } 9 | 10 | context 'when params is a hash' do 11 | let(:params) do 12 | { first: 'string', second: 'string' } 13 | end 14 | 15 | it 'yields the whole params hash without the list of attrs' do 16 | expect { |b| iterator.each(&b) }.to yield_with_args(params) 17 | end 18 | end 19 | 20 | context 'when params is an array' do 21 | let(:params) do 22 | [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] 23 | end 24 | 25 | it 'yields each element of the array without the list of attrs' do 26 | expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1]) 27 | end 28 | end 29 | 30 | context 'when params is empty optional placeholder' do 31 | let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue] } 32 | 33 | it 'does not yield it' do 34 | expect { |b| iterator.each(&b) }.to yield_successive_args 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/grape/validations/single_attribute_iterator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::SingleAttributeIterator do 4 | describe '#each' do 5 | subject(:iterator) { described_class.new(validator, scope, params) } 6 | 7 | let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } 8 | let(:validator) { double(attrs: %i[first second]) } 9 | 10 | context 'when params is a hash' do 11 | let(:params) do 12 | { first: 'string', second: 'string' } 13 | end 14 | 15 | it 'yields params and every single attribute from the list' do 16 | expect { |b| iterator.each(&b) } 17 | .to yield_successive_args([params, :first, false], [params, :second, false]) 18 | end 19 | end 20 | 21 | context 'when params is an array' do 22 | let(:params) do 23 | [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] 24 | end 25 | 26 | it 'yields every single attribute from the list for each of the array elements' do 27 | expect { |b| iterator.each(&b) }.to yield_successive_args( 28 | [params[0], :first, false], [params[0], :second, false], 29 | [params[1], :first, false], [params[1], :second, false] 30 | ) 31 | end 32 | 33 | context 'empty values' do 34 | let(:params) { [{}, '', 10] } 35 | 36 | it 'marks params with empty values' do 37 | expect { |b| iterator.each(&b) }.to yield_successive_args( 38 | [params[0], :first, true], [params[0], :second, true], 39 | [params[1], :first, true], [params[1], :second, true], 40 | [params[2], :first, false], [params[2], :second, false] 41 | ) 42 | end 43 | end 44 | 45 | context 'when missing optional value' do 46 | let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] } 47 | 48 | it 'does not yield skipped values' do 49 | expect { |b| iterator.each(&b) }.to yield_successive_args( 50 | [params[1], :first, false], [params[1], :second, false] 51 | ) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/grape/validations/types/array_coercer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::Types::ArrayCoercer do 4 | subject { described_class.new(type) } 5 | 6 | describe '#call' do 7 | context 'an array of primitives' do 8 | let(:type) { Array[String] } 9 | 10 | it 'coerces elements in the array' do 11 | expect(subject.call([10, 20])).to eq(%w[10 20]) 12 | end 13 | end 14 | 15 | context 'an array of arrays' do 16 | let(:type) { Array[Array[Integer]] } 17 | 18 | it 'coerces elements in the nested array' do 19 | expect(subject.call([%w[10 20]])).to eq([[10, 20]]) 20 | expect(subject.call([['10'], ['20']])).to eq([[10], [20]]) 21 | end 22 | end 23 | 24 | context 'an array of sets' do 25 | let(:type) { Array[Set[Integer]] } 26 | 27 | it 'coerces elements in the nested set' do 28 | expect(subject.call([%w[10 20]])).to eq([Set[10, 20]]) 29 | expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]]) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/grape/validations/types/set_coercer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::Types::SetCoercer do 4 | subject { described_class.new(type) } 5 | 6 | describe '#call' do 7 | context 'a set of primitives' do 8 | let(:type) { Set[String] } 9 | 10 | it 'coerces elements to the set' do 11 | expect(subject.call([10, 20])).to eq(Set['10', '20']) 12 | end 13 | end 14 | 15 | context 'a set of sets' do 16 | let(:type) { Set[Set[Integer]] } 17 | 18 | it 'coerces elements in the nested set' do 19 | expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]]) 20 | expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]]) 21 | end 22 | end 23 | 24 | context 'a set of sets of arrays' do 25 | let(:type) { Set[Set[Array[Integer]]] } 26 | 27 | it 'coerces elements in the nested set' do 28 | expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]]) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/grape/validations/validators/contract_scope_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::Validators::ContractScopeValidator do 4 | describe '.inherits' do 5 | subject { described_class } 6 | 7 | it { is_expected.to be < Grape::Validations::Validators::Base } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/grape/validations/validators/same_as_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Validations::Validators::SameAsValidator do 4 | let_it_be(:app) do 5 | Class.new(Grape::API) do 6 | params do 7 | requires :password 8 | requires :password_confirmation, same_as: :password 9 | end 10 | post do 11 | end 12 | 13 | params do 14 | requires :password 15 | requires :password_confirmation, same_as: { value: :password, message: 'not match' } 16 | end 17 | post '/custom-message' do 18 | end 19 | end 20 | end 21 | 22 | describe '/' do 23 | context 'is the same' do 24 | it do 25 | post '/', password: '987654', password_confirmation: '987654' 26 | expect(last_response.status).to eq(201) 27 | expect(last_response.body).to eq('') 28 | end 29 | end 30 | 31 | context 'is not the same' do 32 | it do 33 | post '/', password: '123456', password_confirmation: 'whatever' 34 | expect(last_response.status).to eq(400) 35 | expect(last_response.body).to eq('password_confirmation is not the same as password') 36 | end 37 | end 38 | end 39 | 40 | describe '/custom-message' do 41 | context 'is the same' do 42 | it do 43 | post '/custom-message', password: '987654', password_confirmation: '987654' 44 | expect(last_response.status).to eq(201) 45 | expect(last_response.body).to eq('') 46 | end 47 | end 48 | 49 | context 'is not the same' do 50 | it do 51 | post '/custom-message', password: '123456', password_confirmation: 'whatever' 52 | expect(last_response.status).to eq(400) 53 | expect(last_response.body).to eq('password_confirmation not match') 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/grape/validations/validators/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | grape: 3 | errors: 4 | format: ! '%{attributes}%{message}' 5 | attributes: 6 | age: 年龄 7 | messages: 8 | coerce: '格式不正确' 9 | presence: '请填写' 10 | regexp: '格式不正确' 11 | -------------------------------------------------------------------------------- /spec/integration/multi_json/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # grape_entity depends on multi-json and it breaks the test. 4 | describe Grape::Json, if: defined?(MultiJson) && !defined?(Grape::Entity) do 5 | subject { described_class } 6 | 7 | it { is_expected.to eq(MultiJson) } 8 | end 9 | -------------------------------------------------------------------------------- /spec/integration/multi_xml/xml_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::Xml, if: defined?(MultiXml) do 4 | subject { described_class } 5 | 6 | it { is_expected.to eq(MultiXml) } 7 | end 8 | -------------------------------------------------------------------------------- /spec/integration/rack_3_0/headers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Grape::API do 4 | subject { last_response.headers } 5 | 6 | describe 'returned headers should all be in lowercase' do 7 | context 'when setting an header in an API' do 8 | let(:app) do 9 | Class.new(described_class) do 10 | get do 11 | header['GRAPE'] = '1' 12 | return_no_content 13 | end 14 | end 15 | end 16 | 17 | before { get '/' } 18 | 19 | it { is_expected.to include('grape' => '1') } 20 | end 21 | 22 | context 'when error!' do 23 | let(:app) do 24 | Class.new(described_class) do 25 | rescue_from ArgumentError do 26 | error!('error!', 500, { 'GRAPE' => '1' }) 27 | end 28 | 29 | get { raise ArgumentError } 30 | end 31 | end 32 | 33 | before { get '/' } 34 | 35 | it { is_expected.to include('grape' => '1') } 36 | end 37 | 38 | context 'when redirect' do 39 | let(:app) do 40 | Class.new(described_class) do 41 | get do 42 | redirect 'https://www.ruby-grape.org/' 43 | end 44 | end 45 | end 46 | 47 | before { get '/' } 48 | 49 | it { is_expected.to include('location' => 'https://www.ruby-grape.org/') } 50 | end 51 | 52 | context 'when options' do 53 | let(:app) do 54 | Class.new(described_class) do 55 | get { return_no_content } 56 | end 57 | end 58 | 59 | before { options '/' } 60 | 61 | it { is_expected.to include('allow' => 'OPTIONS, GET, HEAD') } 62 | end 63 | 64 | context 'when cascade' do 65 | let(:app) do 66 | Class.new(described_class) do 67 | version 'v0', using: :path, cascade: true 68 | get { return_no_content } 69 | end 70 | end 71 | 72 | before { get '/v1' } 73 | 74 | it { is_expected.to include('x-cascade' => 'pass') } 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/integration/rack_3_0/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Rack do 4 | it { expect(Gem::Version.new(described_class.release).segments.first).to eq 3 } 5 | end 6 | -------------------------------------------------------------------------------- /spec/integration/rails/mounting_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe 'Rails', if: defined?(Rails) do 4 | context 'rails mounted' do 5 | let(:api) do 6 | Class.new(Grape::API) do 7 | lint! 8 | get('/test_grape') { 'rails mounted' } 9 | end 10 | end 11 | 12 | let(:app) do 13 | require 'rails' 14 | require 'action_controller/railtie' 15 | 16 | # https://github.com/rails/rails/issues/51784 17 | # same error as described if not redefining the following 18 | ActiveSupport::Dependencies.autoload_paths = [] 19 | ActiveSupport::Dependencies.autoload_once_paths = [] 20 | 21 | Class.new(Rails::Application) do 22 | config.eager_load = false 23 | config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 24 | config.api_only = true 25 | config.consider_all_requests_local = true 26 | config.hosts << 'example.org' 27 | 28 | routes.append do 29 | mount GrapeApi => '/' 30 | 31 | get 'up', to: lambda { |_env| 32 | [200, {}, ['hello world']] 33 | } 34 | end 35 | end 36 | end 37 | 38 | before do 39 | stub_const('GrapeApi', api) 40 | app.initialize! 41 | end 42 | 43 | it 'cascades' do 44 | get '/test_grape' 45 | expect(last_response).to be_successful 46 | expect(last_response.body).to eq('rails mounted') 47 | get '/up' 48 | expect(last_response).to be_successful 49 | expect(last_response.body).to eq('hello world') 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/integration/rails/railtie_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(Rails) && ActiveSupport.gem_version >= Gem::Version.new('7.1') 4 | describe Grape::Railtie do 5 | describe '.railtie' do 6 | subject { test_app.deprecators[:grape] } 7 | 8 | let(:test_app) do 9 | # https://github.com/rails/rails/issues/51784 10 | # same error as described if not redefining the following 11 | ActiveSupport::Dependencies.autoload_paths = [] 12 | ActiveSupport::Dependencies.autoload_once_paths = [] 13 | 14 | Class.new(Rails::Application) do 15 | config.eager_load = false 16 | config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 17 | end 18 | end 19 | 20 | before { test_app.initialize! } 21 | 22 | it { is_expected.to be(Grape.deprecator) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/shared/deprecated_class_examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples 'deprecated class' do 4 | subject { deprecated_class.new } 5 | 6 | around do |example| 7 | old_deprec_behavior = Grape.deprecator.behavior 8 | Grape.deprecator.behavior = :raise 9 | example.run 10 | Grape.deprecator.behavior = old_deprec_behavior 11 | end 12 | 13 | it 'raises an ActiveSupport::DeprecationException' do 14 | expect { subject }.to raise_error(ActiveSupport::DeprecationException) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | require 'rubygems' 5 | require 'bundler' 6 | Bundler.require :default, :test 7 | 8 | Grape.deprecator.behavior = :raise 9 | 10 | %w[config support].each do |dir| 11 | Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].sort.each do |file| 12 | require file 13 | end 14 | end 15 | 16 | Grape.config.lint = true # lint all apis by default 17 | Grape::Util::Registry.include(Deregister) 18 | # issue with ruby 2.7 with ^. We need to extend it again 19 | Grape::Validations.extend(Grape::Util::Registry) if Gem::Version.new(RUBY_VERSION).release < Gem::Version.new('3.0') 20 | 21 | # The default value for this setting is true in a standard Rails app, 22 | # so it should be set to true here as well to reflect that. 23 | I18n.enforce_available_locales = true 24 | 25 | RSpec.configure do |config| 26 | config.include Rack::Test::Methods 27 | config.include Spec::Support::Helpers 28 | config.raise_errors_for_deprecations! 29 | config.filter_run_when_matching :focus 30 | 31 | config.before(:all) { Grape::Util::InheritableSetting.reset_global! } 32 | config.before { Grape::Util::InheritableSetting.reset_global! } 33 | 34 | # Enable flags like --only-failures and --next-failure 35 | config.example_status_persistence_file_path = '.rspec_status' 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/basic_auth_encode_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spec 4 | module Support 5 | module Helpers 6 | def encode_basic_auth(username, password) 7 | "Basic #{Base64.encode64("#{username}:#{password}")}" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/chunked_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # this is a copy of Rack::Chunked which has been removed in rack > 3.0 4 | 5 | class ChunkedResponse 6 | class Body 7 | TERM = "\r\n" 8 | TAIL = "0#{TERM}" 9 | 10 | # Store the response body to be chunked. 11 | def initialize(body) 12 | @body = body 13 | end 14 | 15 | # For each element yielded by the response body, yield 16 | # the element in chunked encoding. 17 | def each(&block) 18 | term = TERM 19 | @body.each do |chunk| 20 | size = chunk.bytesize 21 | next if size == 0 22 | 23 | yield [size.to_s(16), term, chunk.b, term].join 24 | end 25 | yield TAIL 26 | yield_trailers(&block) 27 | yield term 28 | end 29 | 30 | # Close the response body if the response body supports it. 31 | def close 32 | @body.try(:close) 33 | end 34 | 35 | private 36 | 37 | # Do nothing as this class does not support trailer headers. 38 | def yield_trailers; end 39 | end 40 | 41 | class TrailerBody < Body 42 | private 43 | 44 | # Yield strings for each trailer header. 45 | def yield_trailers 46 | @body.trailers.each_pair do |k, v| 47 | yield "#{k}: #{v}\r\n" 48 | end 49 | end 50 | end 51 | 52 | def initialize(app) 53 | @app = app 54 | end 55 | 56 | def call(env) 57 | status, headers, body = response = @app.call(env) 58 | 59 | if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && 60 | !headers[Rack::CONTENT_LENGTH] && 61 | !headers['Transfer-Encoding'] 62 | 63 | headers['Transfer-Encoding'] = 'chunked' 64 | response[2] = if headers['trailer'] 65 | TrailerBody.new(body) 66 | else 67 | Body.new(body) 68 | end 69 | end 70 | 71 | response 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/support/content_type_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spec 4 | module Support 5 | module Helpers 6 | %w[put patch post delete].each do |method| 7 | define_method :"#{method}_with_json" do |uri, params = {}, env = {}, &block| 8 | params = params.to_json 9 | env['CONTENT_TYPE'] ||= 'application/json' 10 | send(method, uri, params, env, &block) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/cookie_jar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uri' 4 | module Spec 5 | module Support 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie 7 | class CookieJar 8 | attr_reader :attributes 9 | 10 | def initialize(raw) 11 | @attributes = raw.split(/;\s*/).flat_map.with_index do |attribute, i| 12 | attribute, value = attribute.split('=', 2) 13 | if i.zero? 14 | [['name', attribute], ['value', unescape(value)]] 15 | else 16 | [[attribute.downcase, parse_value(attribute, value)]] 17 | end 18 | end.to_h.freeze 19 | end 20 | 21 | def to_h 22 | @attributes.dup 23 | end 24 | 25 | def to_s 26 | @attributes.to_s 27 | end 28 | 29 | private 30 | 31 | def unescape(value) 32 | URI.decode_www_form_component(value, Encoding::UTF_8) 33 | end 34 | 35 | def parse_value(attribute, value) 36 | case attribute 37 | when 'expires' 38 | Time.parse(value) 39 | when 'max-age' 40 | value.to_i 41 | when 'secure', 'httponly', 'partitioned' 42 | true 43 | else 44 | unescape(value) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | module Rack 52 | class MockResponse 53 | def cookie_jar 54 | @cookie_jar ||= Array(headers[Rack::SET_COOKIE]).flat_map { |h| h.split("\n") }.map { |c| Spec::Support::CookieJar.new(c).to_h } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/support/deprecated_warning_handlers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Warning[:deprecated] = true 4 | 5 | module DeprecatedWarningHandler 6 | class DeprecationWarning < StandardError; end 7 | 8 | DEPRECATION_REGEX = /is deprecated/.freeze 9 | 10 | def warn(message) 11 | return super unless message.match?(DEPRECATION_REGEX) 12 | 13 | exception = DeprecationWarning.new(message) 14 | exception.set_backtrace(caller) 15 | raise exception 16 | end 17 | end 18 | 19 | Warning.singleton_class.prepend(DeprecatedWarningHandler) 20 | -------------------------------------------------------------------------------- /spec/support/deregister.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Deregister 4 | def deregister(key) 5 | registry.delete(key) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/endpoint_faker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spec 4 | module Support 5 | class EndpointFaker 6 | class FakerAPI < Grape::API 7 | get('/') 8 | end 9 | 10 | def initialize(app, endpoint = FakerAPI.endpoints.first) 11 | @app = app 12 | @endpoint = endpoint 13 | end 14 | 15 | def call(env) 16 | @endpoint.instance_exec do 17 | @request = Grape::Request.new(env.dup) 18 | end 19 | 20 | @app.call(env.merge(Grape::Env::API_ENDPOINT => @endpoint)) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/file_streamer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FileStreamer 4 | def initialize(file_path) 5 | @file_path = file_path 6 | end 7 | 8 | def each(&blk) 9 | File.open(@file_path, 'rb') do |file| 10 | file.each(10, &blk) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/integer_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spec 4 | module Support 5 | module Helpers 6 | INTEGER_CLASS_NAME = 0.to_i.class.to_s.freeze 7 | 8 | def integer_class_name 9 | INTEGER_CLASS_NAME 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/versioned_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Versioning 4 | module Spec 5 | module Support 6 | module Helpers 7 | # Returns the path with options[:version] prefixed if options[:using] is :path. 8 | # Returns normal path otherwise. 9 | def versioned_path(options) 10 | case options[:using] 11 | when :path 12 | File.join('/', options[:prefix] || '', options[:version], options[:path]) 13 | when :param, :header, :accept_version_header 14 | File.join('/', options[:prefix] || '', options[:path]) 15 | else 16 | raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") 17 | end 18 | end 19 | 20 | def versioned_headers(options) 21 | case options[:using] 22 | when :path, :param 23 | {} 24 | when :header 25 | { 26 | 'HTTP_ACCEPT' => [ 27 | "application/vnd.#{options[:vendor]}-#{options[:version]}", 28 | options[:format] 29 | ].compact.join('+') 30 | } 31 | when :accept_version_header 32 | { 33 | 'HTTP_ACCEPT_VERSION' => options[:version].to_s 34 | } 35 | else 36 | raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") 37 | end 38 | end 39 | 40 | def versioned_get(path, version_name, version_options) 41 | path = versioned_path(version_options.merge(version: version_name, path: path)) 42 | headers = versioned_headers(version_options.merge(version: version_name)) 43 | params = {} 44 | params = { version_options[:parameter] => version_name } if version_options[:using] == :param 45 | get path, params, headers 46 | end 47 | end 48 | end 49 | end 50 | --------------------------------------------------------------------------------