Repository: ruby-grape/grape Branch: master Commit: 1d8e5e98839c Files: 334 Total size: 1.8 MB Directory structure: gitextract_tszz0ncs/ ├── .coveralls.yml ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── danger-comment.yml │ ├── 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 │ ├── issue_mounting.rb │ ├── large_model.rb │ ├── nested_params.rb │ ├── remounting.rb │ ├── resource/ │ │ └── vrp_example.json │ └── simple.rb ├── docker/ │ ├── Dockerfile │ └── entrypoint.sh ├── docker-compose.yml ├── gemfiles/ │ ├── dry_validation.gemfile │ ├── grape_entity.gemfile │ ├── hashie.gemfile │ ├── multi_json.gemfile │ ├── multi_xml.gemfile │ ├── rack_2_2.gemfile │ ├── rack_3_0.gemfile │ ├── rack_3_1.gemfile │ ├── rack_3_2.gemfile │ ├── rack_edge.gemfile │ ├── rails_7_2.gemfile │ ├── rails_8_0.gemfile │ ├── rails_8_1.gemfile │ └── rails_edge.gemfile ├── grape.gemspec ├── lib/ │ ├── grape/ │ │ ├── api/ │ │ │ └── instance.rb │ │ ├── api.rb │ │ ├── content_types.rb │ │ ├── cookies.rb │ │ ├── declared_params_handler.rb │ │ ├── dry_types.rb │ │ ├── dsl/ │ │ │ ├── callbacks.rb │ │ │ ├── declared.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/ │ │ │ ├── base.rb │ │ │ ├── json.rb │ │ │ ├── serializable_hash.rb │ │ │ ├── txt.rb │ │ │ └── xml.rb │ │ ├── error_formatter.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_vendor_option.rb │ │ │ ├── too_deep_parameters.rb │ │ │ ├── too_many_multipart_files.rb │ │ │ ├── unknown_auth_strategy.rb │ │ │ ├── unknown_parameter.rb │ │ │ ├── unknown_params_builder.rb │ │ │ ├── unknown_validator.rb │ │ │ ├── unsupported_group_type.rb │ │ │ ├── validation.rb │ │ │ ├── validation_array_errors.rb │ │ │ └── validation_errors.rb │ │ ├── formatter/ │ │ │ ├── base.rb │ │ │ ├── json.rb │ │ │ ├── serializable_hash.rb │ │ │ ├── txt.rb │ │ │ └── xml.rb │ │ ├── formatter.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/ │ │ │ │ ├── accept_version_header.rb │ │ │ │ ├── base.rb │ │ │ │ ├── header.rb │ │ │ │ ├── param.rb │ │ │ │ └── path.rb │ │ │ └── versioner.rb │ │ ├── namespace.rb │ │ ├── params_builder/ │ │ │ ├── base.rb │ │ │ ├── hash.rb │ │ │ ├── hash_with_indifferent_access.rb │ │ │ └── hashie_mash.rb │ │ ├── params_builder.rb │ │ ├── parser/ │ │ │ ├── base.rb │ │ │ ├── json.rb │ │ │ └── xml.rb │ │ ├── parser.rb │ │ ├── path.rb │ │ ├── presenters/ │ │ │ └── presenter.rb │ │ ├── railtie.rb │ │ ├── request.rb │ │ ├── router/ │ │ │ ├── base_route.rb │ │ │ ├── greedy_route.rb │ │ │ ├── pattern.rb │ │ │ └── route.rb │ │ ├── router.rb │ │ ├── serve_stream/ │ │ │ ├── file_body.rb │ │ │ ├── sendfile_response.rb │ │ │ └── stream_response.rb │ │ ├── util/ │ │ │ ├── api_description.rb │ │ │ ├── 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 │ │ │ └── translation.rb │ │ ├── validations/ │ │ │ ├── attributes_iterator.rb │ │ │ ├── contract_scope.rb │ │ │ ├── multiple_attributes_iterator.rb │ │ │ ├── param_scope_tracker.rb │ │ │ ├── params_documentation.rb │ │ │ ├── params_scope.rb │ │ │ ├── single_attribute_iterator.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 │ │ │ ├── types.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 │ │ │ ├── mutually_exclusive_validator.rb │ │ │ ├── presence_validator.rb │ │ │ ├── regexp_validator.rb │ │ │ ├── same_as_validator.rb │ │ │ └── values_validator.rb │ │ ├── validations.rb │ │ ├── version.rb │ │ └── xml.rb │ └── grape.rb └── spec/ ├── 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 │ │ ├── mounted_helpers_inheritance_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 │ │ └── validations_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 │ │ ├── unknown_validator_spec.rb │ │ ├── unsupported_group_type_spec.rb │ │ ├── validation_errors_spec.rb │ │ └── validation_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 │ │ ├── registry_spec.rb │ │ ├── reverse_stackable_values_spec.rb │ │ ├── stackable_values_spec.rb │ │ └── translation_spec.rb │ ├── validations/ │ │ ├── multiple_attributes_iterator_spec.rb │ │ ├── param_scope_tracker_spec.rb │ │ ├── params_documentation_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 │ │ ├── base_i18n_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 │ │ ├── mutually_exclusive_spec.rb │ │ ├── presence_validator_spec.rb │ │ ├── regexp_validator_spec.rb │ │ ├── same_as_validator_spec.rb │ │ └── values_validator_spec.rb │ └── 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 │ └── rails/ │ ├── mounting_spec.rb │ └── railtie_spec.rb ├── shared/ │ └── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveralls.yml ================================================ service_name: github ================================================ FILE: .dockerignore ================================================ ## MAC OS .DS_Store .com.apple.timemachine.supported ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## REDCAR .redcar ## VIM *.swp *.swo ## RUBYMINE .idea ## PROJECT::GENERAL coverage doc pkg .rvmrc .ruby-version .ruby-gemset .rspec_status .bundle .byebug_history dist Gemfile.lock gemfiles/*.lock tmp .yardoc ## Rubinius .rbx ## Bundler binstubs bin ## ripper-tags and gem-ctags tags ## PROJECT::SPECIFIC .project ================================================ FILE: .github/FUNDING.yml ================================================ tidelift: "rubygems/grape" github: [dblock, ericproulx] ================================================ FILE: .github/workflows/danger-comment.yml ================================================ name: Danger Comment on: workflow_run: workflows: [Danger] types: [completed] jobs: comment: uses: numbata/danger-pr-comment/.github/workflows/danger-comment.yml@v0.1.0 secrets: inherit ================================================ FILE: .github/workflows/danger.yml ================================================ name: Danger on: pull_request: types: [opened, reopened, edited, synchronize] jobs: danger: uses: numbata/danger-pr-comment/.github/workflows/danger-run.yml@v0.1.0 secrets: inherit with: ruby-version: '3.4' bundler-cache: true ================================================ FILE: .github/workflows/edge.yml ================================================ --- name: edge on: workflow_dispatch jobs: test: strategy: fail-fast: false matrix: ruby: ['3.2', '3.3', '3.4', '4.0', ruby-head, truffleruby-head, jruby-head] gemfile: [rails_edge, rack_edge] runs-on: ubuntu-latest continue-on-error: true env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run tests run: "RUBYOPT='--enable=frozen-string-literal' bundle exec rspec" - name: Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} parallel: true finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: [push, pull_request] jobs: lint: name: RuboCop runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 bundler-cache: true rubygems: latest - name: Run RuboCop run: bundle exec rubocop test: strategy: fail-fast: false matrix: ruby: ['3.2', '3.3', '3.4', '4.0'] gemfile: - Gemfile - gemfiles/rack_2_2.gemfile - gemfiles/rack_3_0.gemfile - gemfiles/rack_3_1.gemfile - gemfiles/rack_3_2.gemfile - gemfiles/rails_7_2.gemfile - gemfiles/rails_8_0.gemfile - gemfiles/rails_8_1.gemfile specs: ['spec --exclude-pattern=spec/integration/{grape_entity,hashie,dry_validation,multi_*}/*_spec.rb'] include: - ruby: '4.0' gemfile: gemfiles/grape_entity.gemfile specs: 'spec/integration/grape_entity' - ruby: '4.0' gemfile: gemfiles/hashie.gemfile specs: 'spec/integration/hashie' - ruby: '4.0' gemfile: gemfiles/dry_validation.gemfile specs: 'spec/integration/dry_validation' - ruby: '4.0' gemfile: gemfiles/multi_json.gemfile specs: 'spec/integration/multi_json' - ruby: '4.0' gemfile: gemfiles/multi_xml.gemfile specs: 'spec/integration/multi_xml' runs-on: ubuntu-latest env: BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run Tests (${{ matrix.specs }}) run: "RUBYOPT='--enable=frozen-string-literal' bundle exec rspec ${{ matrix.specs }}" - name: Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: run-${{ matrix.ruby }}-${{ matrix.gemfile }} parallel: true finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true ================================================ FILE: .gitignore ================================================ ## MAC OS .DS_Store .com.apple.timemachine.supported ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## REDCAR .redcar ## VIM *.swp *.swo ## RUBYMINE .idea ## PROJECT::GENERAL coverage doc pkg .rvmrc .ruby-version .ruby-gemset .rspec_status .bundle .byebug_history dist Gemfile.lock gemfiles/*.lock tmp .yardoc ## Rubinius .rbx ## Bundler binstubs bin ## ripper-tags and gem-ctags tags ## PROJECT::SPECIFIC .project ================================================ FILE: .rspec ================================================ --require spec_helper --color --format=documentation --order=rand --warnings ================================================ FILE: .rubocop.yml ================================================ AllCops: NewCops: enable TargetRubyVersion: 3.2 SuggestExtensions: false Exclude: - vendor/**/* - bin/**/* plugins: - rubocop-performance - rubocop-rspec inherit_from: .rubocop_todo.yml Layout/LineLength: Max: 215 Lint/EmptyBlock: Exclude: - spec/**/*_spec.rb Style/Documentation: Enabled: false Style/MultilineIfModifier: Enabled: false Style/RaiseArgs: Enabled: false Style/RedundantArrayConstructor: Enabled: false # doesn't work well with params definition Style/Send: Enabled: true Metrics/AbcSize: Max: 80 # TODO: revert to 50 once the refactor of public api is done. Metrics/BlockLength: Max: 30 Exclude: - spec/**/*_spec.rb Metrics/ClassLength: Max: 305 Metrics/CyclomaticComplexity: Max: 15 Metrics/ParameterLists: CountKeywordArgs: false MaxOptionalParameters: 4 Metrics/MethodLength: Max: 32 Metrics/ModuleLength: Max: 220 Metrics/PerceivedComplexity: Max: 15 RSpec/ExampleLength: Max: 60 RSpec/NestedGroups: Max: 6 RSpec/SpecFilePathFormat: Enabled: false RSpec/SpecFilePathSuffix: Enabled: true RSpec/MultipleExpectations: Enabled: false RSpec/NamedSubject: Enabled: false RSpec/MultipleMemoizedHelpers: Max: 11 RSpec/ContextWording: Enabled: false RSpec/MessageSpies: EnforcedStyle: receive ================================================ FILE: .rubocop_todo.yml ================================================ # This configuration was generated by # `rubocop --auto-gen-config` # on 2026-01-31 18:13:50 UTC using RuboCop version 1.84.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: - 'lib/grape/endpoint.rb' # Offense count: 18 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 Naming/VariableNumber: Exclude: - 'spec/grape/dsl/settings_spec.rb' - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/validations_spec.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. # SupportedStyles: described_class, explicit RSpec/DescribedClass: Exclude: - 'spec/grape/middleware/exception_spec.rb' # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples. # DisallowedExamples: works RSpec/ExampleWording: Exclude: - 'spec/grape/integration/global_namespace_function_spec.rb' - 'spec/grape/validations_spec.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). RSpec/ExpectActual: Exclude: - '**/spec/routing/**/*' - 'spec/grape/middleware/exception_spec.rb' # Offense count: 1 RSpec/ExpectInHook: Exclude: - 'spec/grape/validations/validators/values_validator_spec.rb' # Offense count: 6 # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. RSpec/IndexedLet: Exclude: - 'spec/grape/exceptions/validation_errors_spec.rb' - 'spec/grape/presenters/presenter_spec.rb' - 'spec/shared/versioning_examples.rb' # Offense count: 40 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/middleware/versioner/header_spec.rb' # Offense count: 1 RSpec/LeakyLocalVariable: Exclude: - 'spec/grape/api_spec.rb' # Offense count: 1 RSpec/MessageChain: Exclude: - 'spec/grape/middleware/formatter_spec.rb' # Offense count: 10 RSpec/MissingExampleGroupArgument: Exclude: - 'spec/grape/middleware/exception_spec.rb' # Offense count: 12 RSpec/RepeatedDescription: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/validations/validators/allow_blank_validator_spec.rb' - 'spec/grape/validations/validators/values_validator_spec.rb' # Offense count: 6 RSpec/RepeatedExample: Exclude: - 'spec/grape/middleware/versioner/accept_version_header_spec.rb' - 'spec/grape/validations/validators/allow_blank_validator_spec.rb' # Offense count: 8 RSpec/RepeatedExampleGroupDescription: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/endpoint_spec.rb' - 'spec/grape/util/inheritable_setting_spec.rb' # Offense count: 2 RSpec/StubbedMock: Exclude: - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' # Offense count: 32 RSpec/SubjectStub: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/dsl/parameters_spec.rb' - 'spec/grape/dsl/routing_spec.rb' - 'spec/grape/middleware/base_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' - 'spec/grape/middleware/globals_spec.rb' - 'spec/grape/middleware/stack_spec.rb' # Offense count: 20 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/inside_route_spec.rb' - 'spec/grape/integration/rack_sendfile_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' # Offense count: 2 RSpec/VoidExpect: Exclude: - 'spec/grape/api_spec.rb' - 'spec/grape/dsl/headers_spec.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'spec/grape/endpoint_spec.rb' # Offense count: 10 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'lib/grape/dsl/parameters.rb' - 'lib/grape/endpoint.rb' - 'lib/grape/serve_stream/sendfile_response.rb' - 'lib/grape/validations/params_scope.rb' - 'lib/grape/validations/types/array_coercer.rb' - 'lib/grape/validations/types/custom_type_collection_coercer.rb' - 'lib/grape/validations/types/dry_type_coercer.rb' - 'lib/grape/validations/types/primitive_coercer.rb' - 'lib/grape/validations/types/set_coercer.rb' ================================================ FILE: .simplecov ================================================ # frozen_string_literal: true if ENV['GITHUB_USER'] # only when running CI require 'simplecov-lcov' SimpleCov::Formatter::LcovFormatter.config do |c| c.report_with_single_file = true c.single_report_path = 'coverage/lcov.info' end SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter end SimpleCov.start do enable_coverage :branch add_filter '/spec/' end ================================================ FILE: .yardopts ================================================ --asset grape.png --markup markdown ================================================ FILE: CHANGELOG.md ================================================ ### 3.2.0 (Next) #### Features * [#2662](https://github.com/ruby-grape/grape/pull/2662): Extract `Grape::Util::Translation` for shared I18n fallback logic - [@ericproulx](https://github.com/ericproulx). * [#2656](https://github.com/ruby-grape/grape/pull/2656): Remove useless instance_variable_defined? checks - [@ericproulx](https://github.com/ericproulx). * [#2619](https://github.com/ruby-grape/grape/pull/2619): Remove TOC from README.md and danger-toc check - [@alexanderadam](https://github.com/alexanderadam). * [#2663](https://github.com/ruby-grape/grape/pull/2663): Refactor `ParamsScope` and `Parameters` DSL to use named kwargs - [@ericproulx](https://github.com/ericproulx). * [#2664](https://github.com/ruby-grape/grape/pull/2664): Drop `test-prof` dependency - [@ericproulx](https://github.com/ericproulx). * [#2665](https://github.com/ruby-grape/grape/pull/2665): Pass `attrs` directly to `AttributesIterator` instead of `validator` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes * [#2655](https://github.com/ruby-grape/grape/pull/2655): Fix `before_each` method to handle `nil` parameter correctly - [@ericproulx](https://github.com/ericproulx). * [#2660](https://github.com/ruby-grape/grape/pull/2660): Fix thread safety: move mutable `ParamsScope` state (`index`, `params_meeting_dependency`) into a per-request `ParamScopeTracker` stored in `Fiber[]` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 3.1.0 (2026-01-25) #### Features * [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx). * [#2633](https://github.com/ruby-grape/grape/pull/2633): Refactor API::Instance and reorganize DSL modules - [@ericproulx](https://github.com/ericproulx). * [#2636](https://github.com/ruby-grape/grape/pull/2636): Refactor router to simplify method signatures and reduce duplication - [@ericproulx](https://github.com/ericproulx). * [#2640](https://github.com/ruby-grape/grape/pull/2640): Compute available_media_types once - [@ericproulx](https://github.com/ericproulx). * [#2637](https://github.com/ruby-grape/grape/pull/2637): Refactor declared method - [@ericproulx](https://github.com/ericproulx). * [#2639](https://github.com/ruby-grape/grape/pull/2639): Refactor mime_types_for - [@ericproulx](https://github.com/ericproulx). * [#2638](https://github.com/ruby-grape/grape/pull/2638): Remove unnecessary path string duplication - [@ericproulx](https://github.com/ericproulx). * [#2643](https://github.com/ruby-grape/grape/pull/2638): Remove `try` method in codebase - [@ericproulx](https://github.com/ericproulx). * [#2646](https://github.com/ruby-grape/grape/pull/2646): Call `valid_encoding?` before scrub - [@ericproulx](https://github.com/ericproulx). * [#2644](https://github.com/ruby-grape/grape/pull/2644): Clean useless/not valuable dependencies - [@ericproulx](https://github.com/ericproulx). * [#2649](https://github.com/ruby-grape/grape/pull/2644): Drop support Ruby 3.0 and ActiveSupport 7.0 - [@ericproulx](https://github.com/ericproulx). * [#2648](https://github.com/ruby-grape/grape/pull/2648): Remove deprecated ParamsBuilders extensions - [@ericproulx](https://github.com/ericproulx). * [#2645](https://github.com/ruby-grape/grape/pull/2645): Endpoints are compiled when API is compiled - [@ericproulx](https://github.com/ericproulx). * [#2647](https://github.com/ruby-grape/grape/pull/2647): Explicit kwargs for `namespace` and `route_param` - [@ericproulx](https://github.com/ericproulx). * [#2651](https://github.com/ruby-grape/grape/pull/2651): Migrate Danger to use danger-pr-comment workflow - [@dblock](https://github.com/dblock). #### Fixes * [#2633](https://github.com/ruby-grape/grape/pull/2633): Fix cascade reading - [@ericproulx](https://github.com/ericproulx). * [#2641](https://github.com/ruby-grape/grape/pull/2641): Restore support for `return` in endpoint blocks - [@ericproulx](https://github.com/ericproulx). * [#2642](https://github.com/ruby-grape/grape/pull/2642): Fix array allocation in base_route.rb - [@ericproulx](https://github.com/ericproulx). * Fix `before_each` method to handle `nil` parameter correctly - [@ericproulx](https://github.com/ericproulx). ### 3.0.1 (2025-11-24) #### Features * [#2625](https://github.com/ruby-grape/grape/pull/2625): Update rubocop to 1.81.7 and fix style offenses - [@ericproulx](https://github.com/ericproulx). * [#2626](https://github.com/ruby-grape/grape/pull/2626): Add rails 8.1 to CI test matrix - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2628](https://github.com/ruby-grape/grape/pull/2628): Fix helpers inheritance - [@giorni](https://github.com/giorni). ### 3.0.0 (2025-11-15) #### Features * [#2572](https://github.com/ruby-grape/grape/pull/2572): Drop support ruby 2.7 and active_support 6.1 - [@ericproulx](https://github.com/ericproulx). * [#2573](https://github.com/ruby-grape/grape/pull/2573): Clean up deprecated code - [@ericproulx](https://github.com/ericproulx). * [#2575](https://github.com/ruby-grape/grape/pull/2575): Refactor Api description class - [@ericproulx](https://github.com/ericproulx). * [#2577](https://github.com/ruby-grape/grape/pull/2577): Deprecate `return` in endpoint execution - [@ericproulx](https://github.com/ericproulx). * [#2580](https://github.com/ruby-grape/grape/pull/2580): Refactor endpoint helpers and error middleware integration - [@ericproulx](https://github.com/ericproulx). * [#2581](https://github.com/ruby-grape/grape/pull/2581): Delegate `to_s` in Grape::API::Instance - [@ericproulx](https://github.com/ericproulx). * [#2582](https://github.com/ruby-grape/grape/pull/2582): Fix leaky slash when normalizing - [@ericproulx](https://github.com/ericproulx). * [#2583](https://github.com/ruby-grape/grape/pull/2583): Optimize api parameter documentation and memory usage - [@ericproulx](https://github.com/ericproulx). * [#2589](https://github.com/ruby-grape/grape/pull/2589): Replace `send` by `__send__` in codebase - [@ericproulx](https://github.com/ericproulx). * [#2598](https://github.com/ruby-grape/grape/pull/2598): Refactor settings DSL to use explicit methods instead of dynamic generation - [@ericproulx](https://github.com/ericproulx). * [#2599](https://github.com/ruby-grape/grape/pull/2599): Simplify settings DSL get_or_set method and optimize logger implementation - [@ericproulx](https://github.com/ericproulx). * [#2600](https://github.com/ruby-grape/grape/pull/2600): Refactor versioner middleware: simplify base class and improve consistency - [@ericproulx](https://github.com/ericproulx). * [#2601](https://github.com/ruby-grape/grape/pull/2601): Refactor route_setting internal usage to use inheritable_setting.route for improved consistency and performance - [@ericproulx](https://github.com/ericproulx). * [#2602](https://github.com/ruby-grape/grape/pull/2602): Remove `namespace_reverse_stackable` from public DSL interface and use direct inheritable_setting access - [@ericproulx](https://github.com/ericproulx). * [#2603](https://github.com/ruby-grape/grape/pull/2603): Remove `namespace_stackable_with_hash` from public interface and move to internal InheritableSetting - [@ericproulx](https://github.com/ericproulx). * [#2604](https://github.com/ruby-grape/grape/pull/2604): Enable branch coverage - [@ericproulx](https://github.com/ericproulx). * [#2605](https://github.com/ruby-grape/grape/pull/2605): Add Rack 3.2 support with new gemfile and CI integration - [@ericproulx](https://github.com/ericproulx). * [#2607](https://github.com/ruby-grape/grape/pull/2607): Remove namespace_stackable and namespace_inheritable from public API - [@ericproulx](https://github.com/ericproulx). * [#2615](https://github.com/ruby-grape/grape/pull/2615): Remove manual toc and tod danger check - [@alexanderadam](https://github.com/alexanderadam). * [#2612](https://github.com/ruby-grape/grape/pull/2612): Avoid multiple mount pollution - [@alexanderadam](https://github.com/alexanderadam). * [#2617](https://github.com/ruby-grape/grape/pull/2617): Migrate from `ActiveSupport::Configurable` to `Dry::Configurable` - [@ericproulx](https://github.com/ericproulx). * [#2618](https://github.com/ruby-grape/grape/pull/2618): Modernize argument delegation for Ruby 3+ compatibility - [@ericproulx](https://github.com/ericproulx). * [#2623](https://github.com/ruby-grape/grape/pull/2623): Refactor coercer caching to use `Grape::Util::Cache` - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2586](https://github.com/ruby-grape/grape/pull/2586): Limit helpers DSL public scope - [@ericproulx](https://github.com/ericproulx). * [#2588](https://github.com/ruby-grape/grape/pull/2588): Fix defaut format regression on */* - [@ericproulx](https://github.com/ericproulx). * [#2593](https://github.com/ruby-grape/grape/pull/2593): Fix warning message when overriding global registry key - [@ericproulx](https://github.com/ericproulx). * [#2594](https://github.com/ruby-grape/grape/pull/2594): Fix routes memoization - [@ericproulx](https://github.com/ericproulx). * [#2595](https://github.com/ruby-grape/grape/pull/2595): Keep `within_namespace` as part of our internal api - [@ericproulx](https://github.com/ericproulx). * [#2596](https://github.com/ruby-grape/grape/pull/2596): Remove `namespace_reverse_stackable_with_hash` from public scope - [@ericproulx](https://github.com/ericproulx). * [#2621](https://github.com/ruby-grape/grape/pull/2621): Update upgrading notes regarding `return` usage and simplify endpoint execution - [@ericproulx](https://github.com/ericproulx). * [#2622](https://github.com/ruby-grape/grape/pull/2622): Use `require_relative` instead of `$LOAD_PATH` in gemspec - [@ericproulx](https://github.com/ericproulx). ### 2.4.0 (2025-06-18) #### Features * [#2532](https://github.com/ruby-grape/grape/pull/2532): Update RuboCop 1.71.2 - [@ericproulx](https://github.com/ericproulx). * [#2535](https://github.com/ruby-grape/grape/pull/2535): Delegate calls to inner objects - [@ericproulx](https://github.com/ericproulx). * [#2537](https://github.com/ruby-grape/grape/pull/2537): Use activesupport `try` pattern - [@ericproulx](https://github.com/ericproulx). * [#2536](https://github.com/ruby-grape/grape/pull/2536): Update normalize_path like Rails - [@ericproulx](https://github.com/ericproulx). * [#2540](https://github.com/ruby-grape/grape/pull/2540): Introduce params builder with symbolized short name - [@ericproulx](https://github.com/ericproulx). * [#2550](https://github.com/ruby-grape/grape/pull/2550): Drop ActiveSupport 6.0 - [@ericproulx](https://github.com/ericproulx). * [#2549](https://github.com/ruby-grape/grape/pull/2549): Delegate cookies management to `Grape::Request` - [@ericproulx](https://github.com/ericproulx). * [#2554](https://github.com/ruby-grape/grape/pull/2554): Remove `Grape::Http::Headers` and `Grape::Util::Lazy::Object` - [@ericproulx](https://github.com/ericproulx). * [#2556](https://github.com/ruby-grape/grape/pull/2556): Remove unused `Grape::Request::DEFAULT_PARAMS_BUILDER` constant - [@eriklovmo](https://github.com/eriklovmo). * [#2558](https://github.com/ruby-grape/grape/pull/2558): Add Ruby's option `enable_frozen_string_literal` in CI - [@ericproulx](https://github.com/ericproulx). * [#2557](https://github.com/ruby-grape/grape/pull/2557): Add `lint!` - [@ericproulx](https://github.com/ericproulx). * [#2561](https://github.com/ruby-grape/grape/pull/2561): Optimize hash alloc for middleware's default options - [@ericproulx](https://github.com/ericproulx). * [#2563](https://github.com/ruby-grape/grape/pull/2563): Update `Grape::Middleware::Auth::Base` - [@ericproulx](https://github.com/ericproulx). * [#2571](https://github.com/ruby-grape/grape/pull/2571): Update RuboCop 1.75.8 - [@pieterocp](https://github.com/pieterocp). #### Fixes * [#2538](https://github.com/ruby-grape/grape/pull/2538): Fix validating nested json array params - [@mohammednasser-32](https://github.com/mohammednasser-32). * [#2543](https://github.com/ruby-grape/grape/pull/2543): Fix array allocation on mount - [@ericproulx](https://github.com/ericproulx). * [#2546](https://github.com/ruby-grape/grape/pull/2546): Fix middleware with keywords - [@ericproulx](https://github.com/ericproulx). * [#2547](https://github.com/ruby-grape/grape/pull/2547): Remove jsonapi related code - [@ericproulx](https://github.com/ericproulx). * [#2548](https://github.com/ruby-grape/grape/pull/2548): Formatting from header acts like versioning from header - [@ericproulx](https://github.com/ericproulx). * [#2552](https://github.com/ruby-grape/grape/pull/2552): Fix declared params optional array - [@ericproulx](https://github.com/ericproulx). * [#2553](https://github.com/ruby-grape/grape/pull/2553): Improve performance of query params parsing - [@ericproulx](https://github.com/ericproulx). ### 2.3.0 (2025-02-08) #### Features * [#2497](https://github.com/ruby-grape/grape/pull/2497): Update RuboCop to 1.66.1 - [@ericproulx](https://github.com/ericproulx). * [#2500](https://github.com/ruby-grape/grape/pull/2500): Remove deprecated `file` method - [@ericproulx](https://github.com/ericproulx). * [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx). * [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx). * [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx). * [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx). * [#2514](https://github.com/ruby-grape/grape/pull/2514): Add rails 8.0 to CI - [@ericproulx](https://github.com/ericproulx). * [#2516](https://github.com/ruby-grape/grape/pull/2516): Dynamic registration for parsers, formatters, versioners - [@ericproulx](https://github.com/ericproulx). * [#2518](https://github.com/ruby-grape/grape/pull/2518): Add ruby 3.4 to CI - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2504](https://github.com/ruby-grape/grape/pull/2504): Fix leaky modules in specs - [@ericproulx](https://github.com/ericproulx). * [#2506](https://github.com/ruby-grape/grape/pull/2506): Fix fetch_formatter api_format - [@ericproulx](https://github.com/ericproulx). * [#2507](https://github.com/ruby-grape/grape/pull/2507): Fix type: Set with values - [@nikolai-b](https://github.com/nikolai-b). * [#2510](https://github.com/ruby-grape/grape/pull/2510): Fix ContractScope's validator inheritance - [@ericproulx](https://github.com/ericproulx). * [#2521](https://github.com/ruby-grape/grape/pull/2521): Fixed typo in README - [@datpmt](https://github.com/datpmt). * [#2525](https://github.com/ruby-grape/grape/pull/2525): Require logger before active_support - [@ericproulx](https://github.com/ericproulx). * [#2524](https://github.com/ruby-grape/grape/pull/2524): Fix validators bad encoding - [@ericproulx](https://github.com/ericproulx). * [#2530](https://github.com/ruby-grape/grape/pull/2530): Fix endpoint's status when rescue_from without a block - [@ericproulx](https://github.com/ericproulx). * [#2529](https://github.com/ruby-grape/grape/pull/2529): Fix missing settings on mounted routes (when settings are identical) - [@Haerezis](https://github.com/Haerezis). ### 2.2.0 (2024-09-14) #### Features * [#2475](https://github.com/ruby-grape/grape/pull/2475): Remove Grape::Util::Registrable - [@ericproulx](https://github.com/ericproulx). * [#2484](https://github.com/ruby-grape/grape/pull/2484): Refactor versioner middlewares - [@ericproulx](https://github.com/ericproulx). * [#2489](https://github.com/ruby-grape/grape/pull/2489): Add Rails 7.2 in CI workflow - [@ericproulx](https://github.com/ericproulx). * [#2493](https://github.com/ruby-grape/grape/pull/2493): MFA required when releasing - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2471](https://github.com/ruby-grape/grape/pull/2471): Fix absence of original_exception and/or backtrace even if passed in error! - [@numbata](https://github.com/numbata). * [#2478](https://github.com/ruby-grape/grape/pull/2478): Fix rescue_from with invalid response - [@ericproulx](https://github.com/ericproulx). * [#2480](https://github.com/ruby-grape/grape/pull/2480): Fix rescue_from ValidationErrors exception - [@numbata](https://github.com/numbata). * [#2464](https://github.com/ruby-grape/grape/pull/2464): The `length` validator only takes effect for parameters with types that support `#length` method - [@OuYangJinTing](https://github.com/OuYangJinTing). * [#2485](https://github.com/ruby-grape/grape/pull/2485): Add `is:` param to length validator - [@dakad](https://github.com/dakad). * [#2492](https://github.com/ruby-grape/grape/pull/2492): Fix `Grape::Endpoint#inspect` method - [@ericproulx](https://github.com/ericproulx). * [#2496](https://github.com/ruby-grape/grape/pull/2496): Reduce object allocation when compiling - [@ericproulx](https://github.com/ericproulx). ### 2.1.3 (2024-07-13) #### Fixes * [#2467](https://github.com/ruby-grape/grape/pull/2467): Fix repo coverage - [@ericproulx](https://github.com/ericproulx). * [#2468](https://github.com/ruby-grape/grape/pull/2468): Align `error!` method signatures across different places - [@numbata](https://github.com/numbata). * [#2469](https://github.com/ruby-grape/grape/pull/2469): Fix full path building for lateral scopes - [@numbata](https://github.com/numbata). ### 2.1.2 (2024-06-28) #### Fixes * [#2459](https://github.com/ruby-grape/grape/pull/2459): Autocorrect cops - [@ericproulx](https://github.com/ericproulx). * [#3458](https://github.com/ruby-grape/grape/pull/2458): Remove unused Grape::Util::Accept::Header - [@ericproulx](https://github.com/ericproulx). * [#2463](https://github.com/ruby-grape/grape/pull/2463): Fix error message indices - [@ericproulx](https://github.com/ericproulx). ### 2.1.1 (2024-06-22) #### Features * [#2450](https://github.com/ruby-grape/grape/pull/2450): Update RuboCop to 1.64.1 - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2453](https://github.com/ruby-grape/grape/pull/2453): Fix context in rescue_from - [@ericproulx](https://github.com/ericproulx). * [#2455](https://github.com/ruby-grape/grape/pull/2455): Fix default response headers to work with Rack 3 - [@ericproulx](https://github.com/ericproulx). ### 2.1.0 (2024/06/15) #### Features * [#2432](https://github.com/ruby-grape/grape/pull/2432): Deep merge for group parameter attributes - [@numbata](https://github.com/numbata). * [#2419](https://github.com/ruby-grape/grape/pull/2419): Add the `contract` DSL - [@dgutov](https://github.com/dgutov). * [#2371](https://github.com/ruby-grape/grape/pull/2371): Use a param value as the `default` value of other param - [@jcagarcia](https://github.com/jcagarcia). * [#2377](https://github.com/ruby-grape/grape/pull/2377): Allow to use instance variables values inside `rescue_from` - [@jcagarcia](https://github.com/jcagarcia). * [#2379](https://github.com/ruby-grape/grape/pull/2379): Take into account the `route_param` type in `recognize_path` - [@jcagarcia](https://github.com/jcagarcia). * [#2383](https://github.com/ruby-grape/grape/pull/2383): Use regex block instead of if - [@ericproulx](https://github.com/ericproulx). * [#2384](https://github.com/ruby-grape/grape/pull/2384): Allow to use `before/after/rescue_from` methods in any order when using `mount` - [@jcagarcia](https://github.com/jcagarcia). * [#2390](https://github.com/ruby-grape/grape/pull/2390): Drop support for Ruby 2.6 and Rails 5 - [@ericproulx](https://github.com/ericproulx). * [#2393](https://github.com/ruby-grape/grape/pull/2393): Optimize AttributeTranslator - [@ericproulx](https://github.com/ericproulx). * [#2395](https://github.com/ruby-grape/grape/pull/2395): Set `max-age` to 0 when `cookies.delete` - [@ericproulx](https://github.com/ericproulx). * [#2397](https://github.com/ruby-grape/grape/pull/2397): Add support for ruby 3.3 - [@ericproulx](https://github.com/ericproulx). * [#2399](https://github.com/ruby-grape/grape/pull/2399): Update `rubocop` to 1.59.0, `rubocop-performance` to 1.20.1 and `rubocop-rspec` to 2.25.0 - [@ericproulx](https://github.com/ericproulx). * [#2402](https://github.com/ruby-grape/grape/pull/2402): Grape::Deprecations will be raised when running specs - [@ericproulx](https://github.com/ericproulx). * [#2406](https://github.com/ruby-grape/grape/pull/2406): Remove mime-types dependency in specs - [@ericproulx](https://github.com/ericproulx). * [#2408](https://github.com/ruby-grape/grape/pull/2408): Fix params method redefined warnings - [@ericproulx](https://github.com/ericproulx). * [#2410](https://github.com/ruby-grape/grape/pull/2410): Gem deprecations will raise a DeprecationWarning in specs - [@ericproulx](https://github.com/ericproulx). * [#2389](https://github.com/ruby-grape/grape/pull/2389): Remove rack-accept dependency - [@ericproulx](https://github.com/ericproulx). * [#2426](https://github.com/ruby-grape/grape/pull/2426): Drop support for rack 1.x series - [@ericproulx](https://github.com/ericproulx). * [#2427](https://github.com/ruby-grape/grape/pull/2427): Use `rack-contrib` jsonp instead of rack-jsonp - [@ericproulx](https://github.com/ericproulx). * [#2363](https://github.com/ruby-grape/grape/pull/2363): Replace autoload by zeitwerk - [@ericproulx](https://github.com/ericproulx). * [#2425](https://github.com/ruby-grape/grape/pull/2425): Replace `{}` with `Rack::Header` or `Rack::Utils::HeaderHash` - [@dhruvCW](https://github.com/dhruvCW). * [#2430](https://github.com/ruby-grape/grape/pull/2430): Isolate extensions within specific gemfile - [@ericproulx](https://github.com/ericproulx). * [#2431](https://github.com/ruby-grape/grape/pull/2431): Drop appraisals in favor of eval_gemfile - [@ericproulx](https://github.com/ericproulx). * [#2435](https://github.com/ruby-grape/grape/pull/2435): Use rack constants - [@ericproulx](https://github.com/ericproulx). * [#2436](https://github.com/ruby-grape/grape/pull/2436): Update coverallsapp github-action - [@ericproulx](https://github.com/ericproulx). * [#2434](https://github.com/ruby-grape/grape/pull/2434): Implement nested `with` support in parameter dsl - [@numbata](https://github.com/numbata). * [#2438](https://github.com/ruby-grape/grape/pull/2438): Fix some Rack::Lint - [@ericproulx](https://github.com/ericproulx). * [#2437](https://github.com/ruby-grape/grape/pull/2437): Add length validator - [@dhruvCW](https://github.com/dhruvCW). * [#2445](https://github.com/ruby-grape/grape/pull/2445): Remove builder as a dependency - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2375](https://github.com/ruby-grape/grape/pull/2375): Fix setter methods for `Grape::Router::AttributeTranslator` - [@Jell](https://github.com/Jell). * [#2370](https://github.com/ruby-grape/grape/pull/2370): Remove route_xyz method_missing deprecation - [@ericproulx](https://github.com/ericproulx). * [#2372](https://github.com/ruby-grape/grape/pull/2372): Fix `declared` method for hash params with overlapping names - [@jcagarcia](https://github.com/jcagarcia). * [#2373](https://github.com/ruby-grape/grape/pull/2373): Fix markdown files for following 1-line format - [@jcagarcia](https://github.com/jcagarcia). * [#2382](https://github.com/ruby-grape/grape/pull/2382): Fix values validator for params wrapped in `with` block - [@numbata](https://github.com/numbata). * [#2387](https://github.com/ruby-grape/grape/pull/2387): Fix rubygems version within workflows - [@ericproulx](https://github.com/ericproulx). * [#2405](https://github.com/ruby-grape/grape/pull/2405): Fix edge workflow - [@ericproulx](https://github.com/ericproulx). * [#2414](https://github.com/ruby-grape/grape/pull/2414): Fix Rack::Lint missing content-type - [@ericproulx](https://github.com/ericproulx). * [#2378](https://github.com/ruby-grape/grape/pull/2378): Do not overwrite `route_param` with a regular one if they share same name - [@arg](https://github.com/arg). * [#2444](https://github.com/ruby-grape/grape/pull/2444): Replace method_missing in endpoint - [@ericproulx](https://github.com/ericproulx). * [#2441](https://github.com/ruby-grape/grape/pull/2441): Optimize memory alloc and retained - [@ericproulx](https://github.com/ericproulx). * [#2449](https://github.com/ruby-grape/grape/pull/2449): Rack 3.1 fixes - [@ericproulx](https://github.com/ericproulx). ### 2.0.0 (2023/11/11) #### Features * [#2353](https://github.com/ruby-grape/grape/pull/2353): Added Rails 7.1 support - [@ericproulx](https://github.com/ericproulx). * [#2355](https://github.com/ruby-grape/grape/pull/2355): Set response headers based on Rack version - [@schinery](https://github.com/schinery). * [#2360](https://github.com/ruby-grape/grape/pull/2360): Reduce gem size by removing specs - [@ericproulx](https://github.com/ericproulx). * [#2361](https://github.com/ruby-grape/grape/pull/2361): Remove `Rack::Auth::Digest` - [@ninoseki](https://github.com/ninoseki). #### Fixes * [#2364](https://github.com/ruby-grape/grape/pull/2364): Add missing requires - [@ericproulx](https://github.com/ericproulx). * [#2366](https://github.com/ruby-grape/grape/pull/2366): Default quality to 1.0 in the `Accept` header when omitted - [@hiddewie](https://github.com/hiddewie). * [#2368](https://github.com/ruby-grape/grape/pull/2368): Stripping the internals of `Grape::Endpoint` when `NoMethodError` is raised - [@jcagarcia](https://github.com/jcagarcia). ### 1.8.0 (2023/08/30) #### Features * [#2326](https://github.com/ruby-grape/grape/pull/2326): Use ActiveSupport extensions - [@ericproulx](https://github.com/ericproulx). * [#2327](https://github.com/ruby-grape/grape/pull/2327): Use ActiveSupport deprecation - [@ericproulx](https://github.com/ericproulx). * [#2330](https://github.com/ruby-grape/grape/pull/2330): Use ActiveSupport inflector - [@ericproulx](https://github.com/ericproulx). * [#2331](https://github.com/ruby-grape/grape/pull/2331): Memory optimization when running validators - [@ericproulx](https://github.com/ericproulx). * [#2332](https://github.com/ruby-grape/grape/pull/2332): Use ActiveSupport configurable - [@ericproulx](https://github.com/ericproulx). * [#2333](https://github.com/ruby-grape/grape/pull/2333): Use custom messages in parameter validation with arity 1 - [@thedevjoao](https://github.com/TheDevJoao). * [#2341](https://github.com/ruby-grape/grape/pull/2341): Stop yielding skip value - [@ericproulx](https://github.com/ericproulx). * [#2342](https://github.com/ruby-grape/grape/pull/2342): Allow specifying a handler for grape_exceptions - [@mscrivo](https://github.com/mscrivo). * [#2338](https://github.com/ruby-grape/grape/pull/2338): Fix unknown validator when using requires/optional with entity - [@mscrivo](https://github.com/mscrivo). #### Fixes * [#2339](https://github.com/ruby-grape/grape/pull/2339): Documentation and specs for remountable configuration in params - [@myxoh](https://github.com/myxoh). * [#2328](https://github.com/ruby-grape/grape/pull/2328): Don't cache Class.instance_methods - [@byroot](https://github.com/byroot). * [#2337](https://github.com/ruby-grape/grape/pull/2337): Fix: allow custom validators that do not end with _validator - [@ericproulx](https://github.com/ericproulx). * [#2346](https://github.com/ruby-grape/grape/pull/2346): Adjust test expectations to conform to rack 3 - [@kbarrette](https://github.com/kbarrette). ## 1.7.1 (2023/05/14) #### Features * [#2288](https://github.com/ruby-grape/grape/pull/2288): Dropped support for Ruby 2.5 - [@ericproulx](https://github.com/ericproulx). * [#2288](https://github.com/ruby-grape/grape/pull/2288): Updated rubocop to 1.41.0 - [@ericproulx](https://github.com/ericproulx). * [#2296](https://github.com/ruby-grape/grape/pull/2296): Fix cops and enables some - [@ericproulx](https://github.com/ericproulx). * [#2302](https://github.com/ruby-grape/grape/pull/2302): Rack < 3 and update rack-test - [@ericproulx](https://github.com/ericproulx). * [#2303](https://github.com/ruby-grape/grape/pull/2302): Rack >= 1.3.0 - [@ericproulx](https://github.com/ericproulx). * [#2301](https://github.com/ruby-grape/grape/pull/2301): Revisit GH workflows - [@ericproulx](https://github.com/ericproulx). * [#2311](https://github.com/ruby-grape/grape/pull/2311): Fix tests by pinning rack-test to < 2.1 - [@duffn](https://github.com/duffn). * [#2310](https://github.com/ruby-grape/grape/pull/2310): Fix YARD docs markdown rendering - [@duffn](https://github.com/duffn). * [#2317](https://github.com/ruby-grape/grape/pull/2317): Remove maruku and rubocop-ast as direct development/testing dependencies - [@ericproulx](https://github.com/ericproulx). * [#2292](https://github.com/ruby-grape/grape/pull/2292): Introduce Docker to local development - [@ericproulx](https://github.com/ericproulx). * [#2325](https://github.com/ruby-grape/grape/pull/2325): Change edge test workflows only run on demand - [@dblock](https://github.com/dblock). * [#2324](https://github.com/ruby-grape/grape/pull/2324): Expose default in the description dsl - [@dhruvCW](https://github.com/dhruvCW). #### Fixes * [#2299](https://github.com/ruby-grape/grape/pull/2299): Fix, do not use kwargs for empty args - [@dm1try](https://github.com/dm1try). * [#2307](https://github.com/ruby-grape/grape/pull/2307): Fixed autoloading of InvalidValue - [@fixlr](https://github.com/fixlr). * [#2315](https://github.com/ruby-grape/grape/pull/2315): Update rspec - [@ericproulx](https://github.com/ericproulx). * [#2319](https://github.com/ruby-grape/grape/pull/2319): Update rubocop - [@ericproulx](https://github.com/ericproulx). * [#2323](https://github.com/ruby-grape/grape/pull/2323): Fix using endless ranges for values parameter - [@dhruvCW](https://github.com/dhruvCW). ### 1.7.0 (2022/12/20) #### Features * [#2233](https://github.com/ruby-grape/grape/pull/2233): Added `do_not_document!` for disabling documentation to internal APIs - [@dnesteryuk](https://github.com/dnesteryuk). * [#2235](https://github.com/ruby-grape/grape/pull/2235): Add support for Ruby 3.1 - [@petergoldstein](https://github.com/petergoldstein). * [#2248](https://github.com/ruby-grape/grape/pull/2248): Upgraded to rspec 3.11.0 - [@dblock](https://github.com/dblock). * [#2249](https://github.com/ruby-grape/grape/pull/2249): Split CI matrix, extract edge - [@dblock](https://github.com/dblock). * [#2249](https://github.com/ruby-grape/grape/pull/2251): Upgraded to RuboCop 1.25.1 - [@dblock](https://github.com/dblock). * [#2271](https://github.com/ruby-grape/grape/pull/2271): Fixed validation regression on Numeric type introduced in 1.3 - [@vasfed](https://github.com/Vasfed). * [#2267](https://github.com/ruby-grape/grape/pull/2267): Standardized English error messages - [@dblock](https://github.com/dblock). * [#2272](https://github.com/ruby-grape/grape/pull/2272): Added error on param init when provided type does not have `[]` coercion method, previously validation silently failed for any value - [@vasfed](https://github.com/Vasfed). * [#2274](https://github.com/ruby-grape/grape/pull/2274): Error middleware support using rack util's symbols as status - [@dhruvCW](https://github.com/dhruvCW). * [#2276](https://github.com/ruby-grape/grape/pull/2276): Fix exception super - [@ericproulx](https://github.com/ericproulx). * [#2285](https://github.com/ruby-grape/grape/pull/2285), [#2287](https://github.com/ruby-grape/grape/pull/2287): Added :evaluate_given to declared(params) - [@zysend](https://github.com/zysend). #### Fixes * [#2263](https://github.com/ruby-grape/grape/pull/2263): Explicitly require `bigdecimal` and `date` - [@dblock](https://github.com/dblock). * [#2222](https://github.com/ruby-grape/grape/pull/2222): Autoload types and validators - [@ericproulx](https://github.com/ericproulx). * [#2232](https://github.com/ruby-grape/grape/pull/2232): Fix kwargs support in shared params definition - [@dm1try](https://github.com/dm1try). * [#2229](https://github.com/ruby-grape/grape/pull/2229): Do not collect params in route settings - [@dnesteryuk](https://github.com/dnesteryuk). * [#2234](https://github.com/ruby-grape/grape/pull/2234): Remove non-UTF8 characters from format before generating JSON error - [@bschmeck](https://github.com/bschmeck). * [#2227](https://github.com/ruby-grape/grape/pull/2222): Rename `MissingGroupType` and `UnsupportedGroupType` exceptions - [@ericproulx](https://github.com/ericproulx). * [#2244](https://github.com/ruby-grape/grape/pull/2244): Fix a breaking change in `Grape::Validations` provided in 1.6.1 - [@dm1try](https://github.com/dm1try). * [#2250](https://github.com/ruby-grape/grape/pull/2250): Add deprecation warning for `UnsupportedGroupTypeError` and `MissingGroupTypeError` - [@ericproulx](https://github.com/ericproulx). * [#2256](https://github.com/ruby-grape/grape/pull/2256): Raise `Grape::Exceptions::MultipartPartLimitError` from Rack when too many files are uploaded - [@bschmeck](https://github.com/bschmeck). * [#2266](https://github.com/ruby-grape/grape/pull/2266): Fix code coverage - [@duffn](https://github.com/duffn). * [#2284](https://github.com/ruby-grape/grape/pull/2284): Fix an unexpected backtick - [@zysend](https://github.com/zysend). ### 1.6.2 (2021/12/30) #### Fixes * [#2219](https://github.com/ruby-grape/grape/pull/2219): Revert the changes for autoloading provided in 1.6.1 - [@dm1try](https://github.com/dm1try). ### 1.6.1 (2021/12/28) #### Features * [#2196](https://github.com/ruby-grape/grape/pull/2196): Add support for `passwords_hashed` param for `digest_auth` - [@lHydra](https://github.com/lhydra). * [#2208](https://github.com/ruby-grape/grape/pull/2208): Added Rails 7 support - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2206](https://github.com/ruby-grape/grape/pull/2206): Require main active_support lib before any of its extension definitions - [@annih](https://github.com/Annih). * [#2193](https://github.com/ruby-grape/grape/pull/2193): Fixed the broken ruby-head NoMethodError spec - [@Jack12816](https://github.com/Jack12816). * [#2192](https://github.com/ruby-grape/grape/pull/2192): Memoize the result of Grape::Middleware::Base#response - [@Jack12816](https://github.com/Jack12816). * [#2200](https://github.com/ruby-grape/grape/pull/2200): Add validators module to all validators - [@ericproulx](https://github.com/ericproulx). * [#2202](https://github.com/ruby-grape/grape/pull/2202): Fix random mock spec error - [@ericproulx](https://github.com/ericproulx). * [#2203](https://github.com/ruby-grape/grape/pull/2203): Add rubocop-rspec - [@ericproulx](https://github.com/ericproulx). * [#2207](https://github.com/ruby-grape/grape/pull/2207): Autoload Validations/Validators - [@ericproulx](https://github.com/ericproulx). * [#2209](https://github.com/ruby-grape/grape/pull/2209): Autoload Validations/Types - [@ericproulx](https://github.com/ericproulx). ### 1.6.0 (2021/10/04) #### Features * [#2190](https://github.com/ruby-grape/grape/pull/2190): Upgrade dev deps & drop Ruby 2.4.x support - [@dnesteryuk](https://github.com/dnesteryuk). #### Fixes * [#2176](https://github.com/ruby-grape/grape/pull/2176): Fix: OPTIONS fails if matching all routes - [@myxoh](https://github.com/myxoh). * [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko). * [#2180](https://github.com/ruby-grape/grape/pull/2180): Call `super` in `API.inherited` - [@yogeshjain999](https://github.com/yogeshjain999). * [#2189](https://github.com/ruby-grape/grape/pull/2189): Fix: rename parameters when using `:as` (behaviour and grape-swagger documentation) - [@Jack12816](https://github.com/Jack12816). ### 1.5.3 (2021/03/07) #### Fixes * [#2161](https://github.com/ruby-grape/grape/pull/2157): Handle EOFError from Rack when given an empty multipart body - [@bschmeck](https://github.com/bschmeck). * [#2162](https://github.com/ruby-grape/grape/pull/2162): Corrected a hash modification while iterating issue - [@Jack12816](https://github.com/Jack12816). * [#2164](https://github.com/ruby-grape/grape/pull/2164): Fix: `coerce_with` is now called for params with `nil` value - [@braktar](https://github.com/braktar). ### 1.5.2 (2021/02/06) #### Features * [#2157](https://github.com/ruby-grape/grape/pull/2157): Custom types can set a message to be used in the response when invalid - [@dnesteryuk](https://github.com/dnesteryuk). * [#2145](https://github.com/ruby-grape/grape/pull/2145): Ruby 3.0 compatibility - [@ericproulx](https://github.com/ericproulx). * [#2143](https://github.com/ruby-grape/grape/pull/2143): Enable GitHub Actions with updated RuboCop and Danger - [@anakinj](https://github.com/anakinj). #### Fixes * [#2144](https://github.com/ruby-grape/grape/pull/2144): Fix compatibility issue with activesupport 6.1 and XML serialization of arrays - [@anakinj](https://github.com/anakinj). * [#2137](https://github.com/ruby-grape/grape/pull/2137): Fix typos - [@johnny-miyake](https://github.com/johnny-miyake). * [#2131](https://github.com/ruby-grape/grape/pull/2131): Fix Ruby 2.7 keyword deprecation warning in validators/coerce - [@K0H205](https://github.com/K0H205). * [#2132](https://github.com/ruby-grape/grape/pull/2132): Use #ruby2_keywords for correct delegation on Ruby <= 2.6, 2.7 and 3 - [@eregon](https://github.com/eregon). * [#2152](https://github.com/ruby-grape/grape/pull/2152): Fix configuration method inside namespaced params - [@fsainz](https://github.com/fsainz). ### 1.5.1 (2020/11/15) #### Fixes * [#2129](https://github.com/ruby-grape/grape/pull/2129): Fix validation error when Required Array nested inside an optional array, for Multiparam validators - [@dwhenry](https://github.com/dwhenry). * [#2128](https://github.com/ruby-grape/grape/pull/2128): Fix validation error when Required Array nested inside an optional array - [@dwhenry](https://github.com/dwhenry). * [#2127](https://github.com/ruby-grape/grape/pull/2127): Fix a performance issue with dependent params - [@dnesteryuk](https://github.com/dnesteryuk). * [#2126](https://github.com/ruby-grape/grape/pull/2126): Fix warnings about redefined attribute accessors in `AttributeTranslator` - [@samsonjs](https://github.com/samsonjs). * [#2121](https://github.com/ruby-grape/grape/pull/2121): Fix 2.7 deprecation warning in validator_factory - [@Legogris](https://github.com/Legogris). * [#2115](https://github.com/ruby-grape/grape/pull/2115): Fix declared_params regression with multiple allowed types - [@stanhu](https://github.com/stanhu). * [#2123](https://github.com/ruby-grape/grape/pull/2123): Fix 2.7 deprecation warning in middleware/stack - [@Legogris](https://github.com/Legogris). ### 1.5.0 (2020/10/05) #### Fixes * [#2104](https://github.com/ruby-grape/grape/pull/2104): Fix Ruby 2.7 keyword deprecation warning - [@stanhu](https://github.com/stanhu). * [#2103](https://github.com/ruby-grape/grape/pull/2103): Ensure complete declared params structure is present - [@tlconnor](https://github.com/tlconnor). * [#2099](https://github.com/ruby-grape/grape/pull/2099): Added truffleruby to Travis-CI - [@gogainda](https://github.com/gogainda). * [#2089](https://github.com/ruby-grape/grape/pull/2089): Specify order of mounting Grape with Rack::Cascade in README - [@jonmchan](https://github.com/jonmchan). * [#2088](https://github.com/ruby-grape/grape/pull/2088): Set `Cache-Control` header only for streamed responses - [@stanhu](https://github.com/stanhu). * [#2092](https://github.com/ruby-grape/grape/pull/2092): Correct an example params in Include Missing doc - [@huyvohcmc](https://github.com/huyvohcmc). * [#2091](https://github.com/ruby-grape/grape/pull/2091): Fix ruby 2.7 keyword deprecations - [@dim](https://github.com/dim). * [#2097](https://github.com/ruby-grape/grape/pull/2097): Skip to set default value unless `meets_dependency?` - [@wanabe](https://github.com/wanabe). * [#2096](https://github.com/ruby-grape/grape/pull/2096): Fix redundant dependency check - [@braktar](https://github.com/braktar). * [#2096](https://github.com/ruby-grape/grape/pull/2098): Fix nested coercion - [@braktar](https://github.com/braktar). * [#2102](https://github.com/ruby-grape/grape/pull/2102): Fix retaining setup blocks when remounting APIs - [@jylamont](https://github.com/jylamont). ### 1.4.0 (2020/07/10) #### Features * [#1520](https://github.com/ruby-grape/grape/pull/1520): Un-deprecate stream-like objects - [@urkle](https://github.com/urkle). * [#2060](https://github.com/ruby-grape/grape/pull/2060): Drop support for Ruby 2.4 - [@dblock](https://github.com/dblock). * [#2060](https://github.com/ruby-grape/grape/pull/2060): Upgraded Rubocop to 0.84.0 - [@dblock](https://github.com/dblock). * [#2077](https://github.com/ruby-grape/grape/pull/2077): Simplify logic for defining declared params - [@dnesteryuk](https://github.com/dnesteryuk). * [#2076](https://github.com/ruby-grape/grape/pull/2076): Make route information available for hooks when the automatically generated endpoints are invoked - [@anakinj](https://github.com/anakinj). #### Fixes * [#2067](https://github.com/ruby-grape/grape/pull/2067): Coerce empty String to `nil` for all primitive types except `String` - [@petekinnecom](https://github.com/petekinnecom). * [#2064](https://github.com/ruby-grape/grape/pull/2064): Fix Ruby 2.7 deprecation warning in `Grape::Middleware::Base#initialize` - [@skarger](https://github.com/skarger). * [#2072](https://github.com/ruby-grape/grape/pull/2072): Fix `Grape.eager_load!` and `compile!` - [@stanhu](https://github.com/stanhu). * [#2084](https://github.com/ruby-grape/grape/pull/2084): Fix memory leak in path normalization - [@fcheung](https://github.com/fcheung). ### 1.3.3 (2020/05/23) #### Features * [#2048](https://github.com/ruby-grape/grape/issues/2034): Grape Enterprise support is now available [via TideLift](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) - [@dblock](https://github.com/dblock). * [#2039](https://github.com/ruby-grape/grape/pull/2039): Travis - update rails versions - [@ericproulx](https://github.com/ericproulx). * [#2038](https://github.com/ruby-grape/grape/pull/2038): Travis - update ruby versions - [@ericproulx](https://github.com/ericproulx). * [#2050](https://github.com/ruby-grape/grape/pull/2050): Refactor route public_send to AttributeTranslator - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2049](https://github.com/ruby-grape/grape/pull/2049): Coerce an empty string to nil in case of the bool type - [@dnesteryuk](https://github.com/dnesteryuk). * [#2043](https://github.com/ruby-grape/grape/pull/2043): Modify declared for nested array and hash - [@kadotami](https://github.com/kadotami). * [#2040](https://github.com/ruby-grape/grape/pull/2040): Fix a regression with Array of type nil - [@ericproulx](https://github.com/ericproulx). * [#2054](https://github.com/ruby-grape/grape/pull/2054): Coercing of nested arrays - [@dnesteryuk](https://github.com/dnesteryuk). * [#2050](https://github.com/ruby-grape/grape/pull/2053): Fix broken multiple mounts - [@Jack12816](https://github.com/Jack12816). ### 1.3.2 (2020/04/12) #### Features * [#2020](https://github.com/ruby-grape/grape/pull/2020): Reduce array allocation - [@ericproulx](https://github.com/ericproulx). * [#2015](https://github.com/ruby-grape/grape/pull/2014): Reduce MatchData allocation - [@ericproulx](https://github.com/ericproulx). * [#2014](https://github.com/ruby-grape/grape/pull/2014): Reduce total allocated arrays - [@ericproulx](https://github.com/ericproulx). * [#2011](https://github.com/ruby-grape/grape/pull/2011): Reduce total retained regexes - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2033](https://github.com/ruby-grape/grape/pull/2033): Ensure `Float` params are correctly coerced to `BigDecimal` - [@tlconnor](https://github.com/tlconnor). * [#2031](https://github.com/ruby-grape/grape/pull/2031): Fix a regression with an array of a custom type - [@dnesteryuk](https://github.com/dnesteryuk). * [#2026](https://github.com/ruby-grape/grape/pull/2026): Fix a regression in `coerce_with` when coercion returns `nil` - [@misdoro](https://github.com/misdoro). * [#2025](https://github.com/ruby-grape/grape/pull/2025): Fix Decimal type category - [@kdoya](https://github.com/kdoya). * [#2019](https://github.com/ruby-grape/grape/pull/2019): Avoid coercing parameter with multiple types to an empty Array - [@stanhu](https://github.com/stanhu). ### 1.3.1 (2020/03/11) #### Features * [#2005](https://github.com/ruby-grape/grape/pull/2005): Content types registrable - [@ericproulx](https://github.com/ericproulx). * [#2003](https://github.com/ruby-grape/grape/pull/2003): Upgraded Rubocop to 0.80.1 - [@ericproulx](https://github.com/ericproulx). * [#2002](https://github.com/ruby-grape/grape/pull/2002): Objects allocation optimization (lazy_lookup) - [@ericproulx](https://github.com/ericproulx). #### Fixes * [#2006](https://github.com/ruby-grape/grape/pull/2006): Fix explicit rescue StandardError - [@ericproulx](https://github.com/ericproulx). * [#2004](https://github.com/ruby-grape/grape/pull/2004): Rubocop fixes - [@ericproulx](https://github.com/ericproulx). * [#1995](https://github.com/ruby-grape/grape/pull/1995): Fix: "undefined instance variables" and "method redefined" warnings - [@nbeyer](https://github.com/nbeyer). * [#1994](https://github.com/ruby-grape/grape/pull/1993): Fix typos in README - [@bellmyer](https://github.com/bellmyer). * [#1993](https://github.com/ruby-grape/grape/pull/1993): Lazy join allow header - [@ericproulx](https://github.com/ericproulx). * [#1987](https://github.com/ruby-grape/grape/pull/1987): Re-add exactly_one_of mutually exclusive error message - [@ZeroInputCtrl](https://github.com/ZeroInputCtrl). * [#1977](https://github.com/ruby-grape/grape/pull/1977): Skip validation for a file if it is optional and nil - [@dnesteryuk](https://github.com/dnesteryuk). * [#1976](https://github.com/ruby-grape/grape/pull/1976): Ensure classes/modules listed for autoload really exist - [@dnesteryuk](https://github.com/dnesteryuk). * [#1971](https://github.com/ruby-grape/grape/pull/1971): Fix BigDecimal coercion - [@FlickStuart](https://github.com/FlickStuart). * [#1968](https://github.com/ruby-grape/grape/pull/1968): Fix args forwarding in Grape::Middleware::Stack#merge_with for ruby 2.7.0 - [@dm1try](https://github.com/dm1try). * [#1988](https://github.com/ruby-grape/grape/pull/1988): Refactor the full_messages method and stop overriding full_message - [@hosseintoussi](https://github.com/hosseintoussi). * [#1956](https://github.com/ruby-grape/grape/pull/1956): Comply with Rack spec, fix `undefined method [] for nil:NilClass` error when upgrading Rack - [@ioquatix](https://github.com/ioquatix). ### 1.3.0 (2020/01/11) #### Features * [#1949](https://github.com/ruby-grape/grape/pull/1949): Add support for Ruby 2.7 - [@nbulaj](https://github.com/nbulaj). * [#1948](https://github.com/ruby-grape/grape/pull/1948): Relax `dry-types` dependency version - [@nbulaj](https://github.com/nbulaj). * [#1944](https://github.com/ruby-grape/grape/pull/1944): Reduces `attribute_translator` string allocations - [@ericproulx](https://github.com/ericproulx). * [#1943](https://github.com/ruby-grape/grape/pull/1943): Reduces number of regex string allocations - [@ericproulx](https://github.com/ericproulx). * [#1942](https://github.com/ruby-grape/grape/pull/1942): Optimizes retained memory methods - [@ericproulx](https://github.com/ericproulx). * [#1941](https://github.com/ruby-grape/grape/pull/1941): Adds frozen string literal - [@ericproulx](https://github.com/ericproulx). * [#1940](https://github.com/ruby-grape/grape/pull/1940): Gets rid of a needless step in `HashWithIndifferentAccess` - [@dnesteryuk](https://github.com/dnesteryuk). * [#1938](https://github.com/ruby-grape/grape/pull/1938): Adds project metadata to the gemspec - [@orien](https://github.com/orien). * [#1920](https://github.com/ruby-grape/grape/pull/1920): Replaces Virtus with dry-types - [@dnesteryuk](https://github.com/dnesteryuk). * [#1930](https://github.com/ruby-grape/grape/pull/1930): Moves block call to separate method so it can be spied on - [@estolfo](https://github.com/estolfo). #### Fixes * [#1965](https://github.com/ruby-grape/grape/pull/1965): Fix typos in README - [@davidalee](https://github.com/davidalee). * [#1963](https://github.com/ruby-grape/grape/pull/1963): The values validator must properly work with booleans - [@dnesteryuk](https://github.com/dnesteryuk). * [#1950](https://github.com/ruby-grape/grape/pull/1950): Consider the allow_blank option in the values validator - [@dnesteryuk](https://github.com/dnesteryuk). * [#1947](https://github.com/ruby-grape/grape/pull/1947): Careful check for empty params - [@dnesteryuk](https://github.com/dnesteryuk). * [#1931](https://github.com/ruby-grape/grape/pull/1946): Fixes issue when using namespaces in `Grape::API::Instance` mounted directly - [@myxoh](https://github.com/myxoh). ### 1.2.5 (2019/12/01) #### Features * [#1931](https://github.com/ruby-grape/grape/pull/1931): Introduces LazyBlock to generate expressions that will executed at mount time - [@myxoh](https://github.com/myxoh). * [#1918](https://github.com/ruby-grape/grape/pull/1918): Helper methods to access controller context from middleware - [@NikolayRys](https://github.com/NikolayRys). * [#1915](https://github.com/ruby-grape/grape/pull/1915): Micro optimizations in allocating hashes and arrays - [@dnesteryuk](https://github.com/dnesteryuk). * [#1904](https://github.com/ruby-grape/grape/pull/1904): Allows Grape to load files on startup rather than on the first call - [@myxoh](https://github.com/myxoh). * [#1907](https://github.com/ruby-grape/grape/pull/1907): Adds outside configuration to Grape with `configure` - [@unleashy](https://github.com/unleashy). * [#1914](https://github.com/ruby-grape/grape/pull/1914): Run specs in random order - [@splattael](https://github.com/splattael). #### Fixes * [#1917](https://github.com/ruby-grape/grape/pull/1917): Update access to rack constant - [@NikolayRys](https://github.com/NikolayRys). * [#1916](https://github.com/ruby-grape/grape/pull/1916): Drop old appraisals - [@NikolayRys](https://github.com/NikolayRys). * [#1911](https://github.com/ruby-grape/grape/pull/1911): Make sure `Grape::Valiations::AtLeastOneOfValidator` properly treats nested params in errors - [@dnesteryuk](https://github.com/dnesteryuk). * [#1893](https://github.com/ruby-grape/grape/pull/1893): Allows `Grape::API` to behave like a Rack::app in some instances where it was misbehaving - [@myxoh](https://github.com/myxoh). * [#1898](https://github.com/ruby-grape/grape/pull/1898): Refactor `ValidatorFactory` to improve memory allocation - [@Bhacaz](https://github.com/Bhacaz). * [#1900](https://github.com/ruby-grape/grape/pull/1900): Define boolean for `Grape::Api::Instance` - [@Bhacaz](https://github.com/Bhacaz). * [#1903](https://github.com/ruby-grape/grape/pull/1903): Allow nested params renaming (Hash/Array) - [@bikolya](https://github.com/bikolya). * [#1913](https://github.com/ruby-grape/grape/pull/1913): Fix multiple params validators to return correct messages for nested params - [@bikolya](https://github.com/bikolya). * [#1926](https://github.com/ruby-grape/grape/pull/1926): Fixes configuration within given or mounted blocks - [@myxoh](https://github.com/myxoh). * [#1937](https://github.com/ruby-grape/grape/pull/1937): Fix bloat in released gem - [@dblock](https://github.com/dblock). ### 1.2.4 (2019/06/13) #### Features * [#1888](https://github.com/ruby-grape/grape/pull/1888): Makes the `configuration` hash widely available - [@myxoh](https://github.com/myxoh). * [#1864](https://github.com/ruby-grape/grape/pull/1864): Adds `finally` on the API - [@myxoh](https://github.com/myxoh). * [#1869](https://github.com/ruby-grape/grape/pull/1869): Fix issue with empty headers after `error!` method call - [@anaumov](https://github.com/anaumov). #### Fixes * [#1868](https://github.com/ruby-grape/grape/pull/1868): Fix NoMethodError with none hash params - [@ksss](https://github.com/ksss). * [#1876](https://github.com/ruby-grape/grape/pull/1876): Fix const errors being hidden by bug in `const_missing` - [@dandehavilland](https://github.com/dandehavilland). ### 1.2.3 (2019/01/16) #### Features * [#1850](https://github.com/ruby-grape/grape/pull/1850): Adds `same_as` validator - [@glaucocustodio](https://github.com/glaucocustodio). * [#1833](https://github.com/ruby-grape/grape/pull/1833): Allows to set the `ParamBuilder` globally - [@myxoh](https://github.com/myxoh). #### Fixes * [#1852](https://github.com/ruby-grape/grape/pull/1852): `allow_blank` called after `as` when the original param is not blank - [@glaucocustodio](https://github.com/glaucocustodio). * [#1844](https://github.com/ruby-grape/grape/pull/1844): Enforce `:tempfile` to be a `Tempfile` object in `File` validator - [@Nyangawa](https://github.com/Nyangawa). ### 1.2.2 (2018/12/07) #### Features * [#1832](https://github.com/ruby-grape/grape/pull/1832): Support `body_name` in `desc` block - [@fotos](https://github.com/fotos). * [#1831](https://github.com/ruby-grape/grape/pull/1831): Support `security` in `desc` block - [@fotos](https://github.com/fotos). #### Fixes * [#1836](https://github.com/ruby-grape/grape/pull/1836): Fix: memory leak not releasing `call` method calls from setup - [@myxoh](https://github.com/myxoh). * [#1830](https://github.com/ruby-grape/grape/pull/1830), [#1829](https://github.com/ruby-grape/grape/issues/1829): Restores `self` sanity - [@myxoh](https://github.com/myxoh). ### 1.2.1 (2018/11/28) #### Fixes * [#1825](https://github.com/ruby-grape/grape/pull/1825): `to_s` on a mounted class now responses with the API name - [@myxoh](https://github.com/myxoh). ### 1.2.0 (2018/11/26) #### Features * [#1813](https://github.com/ruby-grape/grape/pull/1813): Add ruby 2.5 support, drop 2.2. Update rails version in travis - [@darren987469](https://github.com/darren987469). * [#1803](https://github.com/ruby-grape/grape/pull/1803): Adds the ability to re-mount all endpoints in any location - [@myxoh](https://github.com/myxoh). * [#1795](https://github.com/ruby-grape/grape/pull/1795): Fix vendor/subtype parsing of an invalid Accept header - [@bschmeck](https://github.com/bschmeck). * [#1791](https://github.com/ruby-grape/grape/pull/1791): Support `summary`, `hidden`, `deprecated`, `is_array`, `nickname`, `produces`, `consumes`, `tags` options in `desc` block - [@darren987469](https://github.com/darren987469). #### Fixes * [#1796](https://github.com/ruby-grape/grape/pull/1796): Fix crash when available locales are enforced but fallback locale unavailable - [@Morred](https://github.com/Morred). * [#1776](https://github.com/ruby-grape/grape/pull/1776): Validate response returned by the exception handler - [@darren987469](https://github.com/darren987469). * [#1787](https://github.com/ruby-grape/grape/pull/1787): Add documented but not implemented ability to `.insert` a middleware in the stack - [@michaellennox](https://github.com/michaellennox). * [#1788](https://github.com/ruby-grape/grape/pull/1788): Fix route requirements bug - [@darren987469](https://github.com/darren987469), [@darrellnash](https://github.com/darrellnash). * [#1810](https://github.com/ruby-grape/grape/pull/1810): Fix support in `given` for aliased params - [@darren987469](https://github.com/darren987469). * [#1811](https://github.com/ruby-grape/grape/pull/1811): Support nested dependent parameters - [@darren987469](https://github.com/darren987469), [@andreacfm](https://github.com/andreacfm). * [#1822](https://github.com/ruby-grape/grape/pull/1822): Raise validation error when optional hash type parameter is received string type value and exactly_one_of be used - [@woshidan](https://github.com/woshidan). ### 1.1.0 (2018/8/4) #### Features * [#1759](https://github.com/ruby-grape/grape/pull/1759): Instrument serialization as `'format_response.grape'` - [@zvkemp](https://github.com/zvkemp). #### Fixes * [#1762](https://github.com/ruby-grape/grape/pull/1763): Fix unsafe HTML rendering on errors - [@ctennis](https://github.com/ctennis). * [#1759](https://github.com/ruby-grape/grape/pull/1759): Update appraisal for rails_edge - [@zvkemp](https://github.com/zvkemp). * [#1758](https://github.com/ruby-grape/grape/pull/1758): Fix expanding load_path in gemspec - [@2maz](https://github.com/2maz). * [#1765](https://github.com/ruby-grape/grape/pull/1765): Use 415 when request body is of an unsupported media type - [@jdmurphy](https://github.com/jdmurphy). * [#1771](https://github.com/ruby-grape/grape/pull/1771): Fix param aliases with 'given' blocks - [@jereynolds](https://github.com/jereynolds). ### 1.0.3 (2018/4/23) #### Fixes * [#1755](https://github.com/ruby-grape/grape/pull/1755): Fix shared params with exactly_one_of - [@milgner](https://github.com/milgner). * [#1740](https://github.com/ruby-grape/grape/pull/1740): Fix dependent parameter validation using `given` when parameter is a `Hash` - [@jvortmann](https://github.com/jvortmann). * [#1737](https://github.com/ruby-grape/grape/pull/1737): Fix translating error when passing symbols as params in custom validations - [@mlzhuyi](https://github.com/mlzhuyi). * [#1749](https://github.com/ruby-grape/grape/pull/1749): Allow rescue from non-`StandardError` exceptions - [@dm1try](https://github.com/dm1try). * [#1750](https://github.com/ruby-grape/grape/pull/1750): Fix a circular dependency warning due to router being loaded by API - [@salasrod](https://github.com/salasrod). * [#1752](https://github.com/ruby-grape/grape/pull/1752): Fix `include_missing` behavior for aliased parameters - [@jonasoberschweiber](https://github.com/jonasoberschweiber). * [#1754](https://github.com/ruby-grape/grape/pull/1754): Allow rescue from non-`StandardError` exceptions to use default error handling - [@jelkster](https://github.com/jelkster). * [#1756](https://github.com/ruby-grape/grape/pull/1756): Allow custom Grape exception handlers when the built-in exception handling is enabled - [@soylent](https://github.com/soylent). ### 1.0.2 (2018/1/10) #### Features * [#1686](https://github.com/ruby-grape/grape/pull/1686): Avoid coercion of a value if it is valid - [@timothysu](https://github.com/timothysu). * [#1688](https://github.com/ruby-grape/grape/pull/1688): Removes yard docs - [@ramkumar-kr](https://github.com/ramkumar-kr). * [#1702](https://github.com/ruby-grape/grape/pull/1702): Added danger-toc, verify correct TOC in README - [@dblock](https://github.com/dblock). * [#1711](https://github.com/ruby-grape/grape/pull/1711): Automatically coerce arrays and sets of types that implement a `parse` method - [@dslh](https://github.com/dslh). #### Fixes * [#1710](https://github.com/ruby-grape/grape/pull/1710): Fix wrong transformation of empty Array in declared params - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1722](https://github.com/ruby-grape/grape/pull/1722): Fix catch-all hiding multiple versions of an endpoint after the first definition - [@zherr](https://github.com/zherr). * [#1724](https://github.com/ruby-grape/grape/pull/1724): Optional nested array validation - [@ericproulx](https://github.com/ericproulx). * [#1725](https://github.com/ruby-grape/grape/pull/1725): Fix `rescue_from :all` documentation - [@Jelkster](https://github.com/Jelkster). * [#1726](https://github.com/ruby-grape/grape/pull/1726): Improved startup performance during API method generation - [@jkowens](https://github.com/jkowens). * [#1727](https://github.com/ruby-grape/grape/pull/1727): Fix infinite loop when mounting endpoint with same superclass - [@jkowens](https://github.com/jkowens). ### 1.0.1 (2017/9/8) #### Features * [#1652](https://github.com/ruby-grape/grape/pull/1652): Add the original exception to the error_formatter the original exception - [@dcsg](https://github.com/dcsg). * [#1665](https://github.com/ruby-grape/grape/pull/1665): Make helpers available in subclasses - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1674](https://github.com/ruby-grape/grape/pull/1674): Add parameter alias (`as`) - [@glaucocustodio](https://github.com/glaucocustodio). #### Fixes * [#1652](https://github.com/ruby-grape/grape/pull/1652): Fix missing backtrace that was not being bubbled up to the `error_formatter` - [@dcsg](https://github.com/dcsg). * [#1661](https://github.com/ruby-grape/grape/pull/1661): Handle deeply-nested dependencies correctly - [@rnubel](https://github.com/rnubel), [@jnardone](https://github.com/jnardone). * [#1679](https://github.com/ruby-grape/grape/pull/1679): Treat StandardError from explicit values validator proc as false - [@jlfaber](https://github.com/jlfaber). ### 1.0.0 (2017/7/3) #### Features * [#1594](https://github.com/ruby-grape/grape/pull/1594): Replace `Hashie::Mash` parameters with `ActiveSupport::HashWithIndifferentAccess` - [@james2m](https://github.com/james2m), [@dblock](https://github.com/dblock). * [#1622](https://github.com/ruby-grape/grape/pull/1622): Add `except_values` validator to replace `except` option of `values` validator - [@jlfaber](https://github.com/jlfaber). * [#1635](https://github.com/ruby-grape/grape/pull/1635): Instrument validators with ActiveSupport::Notifications - [@ktimothy](https://github.com/ktimothy). * [#1646](https://github.com/ruby-grape/grape/pull/1646): Add ability to include an array of modules as helpers - [@pablonahuelgomez](https://github.com/pablonahuelgomez). * [#1623](https://github.com/ruby-grape/grape/pull/1623): Removed `multi_json` and `multi_xml` dependencies - [@dblock](https://github.com/dblock). * [#1650](https://github.com/ruby-grape/grape/pull/1650): Add extra specs for Boolean type field - [@tiarly](https://github.com/tiarly). #### Fixes * [#1648](https://github.com/ruby-grape/grape/pull/1631): Declared now returns declared options using the class that params is set to use - [@thogg4](https://github.com/thogg4). * [#1632](https://github.com/ruby-grape/grape/pull/1632): Silence warnings - [@thogg4](https://github.com/thogg4). * [#1615](https://github.com/ruby-grape/grape/pull/1615): Fix default and type validator when values is a Hash with no value attribute - [@jlfaber](https://github.com/jlfaber). * [#1625](https://github.com/ruby-grape/grape/pull/1625): Handle `given` correctly when nested in Array params - [@rnubel](https://github.com/rnubel), [@avellable](https://github.com/avellable). * [#1649](https://github.com/ruby-grape/grape/pull/1649): Don't share validator instances between requests - [@anakinj](https://github.com/anakinj). ### 0.19.2 (2017/4/12) #### Features * [#1555](https://github.com/ruby-grape/grape/pull/1555): Added code coverage w/Coveralls - [@dblock](https://github.com/dblock). * [#1568](https://github.com/ruby-grape/grape/pull/1568): Add `proc` option to `values` validator to allow custom checks - [@jlfaber](https://github.com/jlfaber). * [#1575](https://github.com/ruby-grape/grape/pull/1575): Include nil values for missing nested params in declared - [@thogg4](https://github.com/thogg4). * [#1585](https://github.com/ruby-grape/grape/pull/1585): Bugs in declared method - make sure correct options var is used and respect include missing for non children params - [@thogg4](https://github.com/thogg4). #### Fixes * [#1570](https://github.com/ruby-grape/grape/pull/1570): Make versioner consider the mount destination path - [@namusyaka](https://github.com/namusyaka). * [#1579](https://github.com/ruby-grape/grape/pull/1579): Fix delete status with a return value - [@eproulx-petalmd](https://github.com/eproulx-petalmd). * [#1559](https://github.com/ruby-grape/grape/pull/1559): You can once again pass `nil` to optional attributes with `values` validation set - [@ghiculescu](https://github.com/ghiculescu). * [#1562](https://github.com/ruby-grape/grape/pull/1562): Fix rainbow gem installation failure above ruby 2.3.3 on travis-ci - [@brucehsu](https://github.com/brucehsu). * [#1561](https://github.com/ruby-grape/grape/pull/1561): Fix performance issue introduced by duplicated calls in StackableValue#[] - [@brucehsu](https://github.com/brucehsu). * [#1564](https://github.com/ruby-grape/grape/pull/1564): Fix declared params bug with nested namespaces - [@bmarini](https://github.com/bmarini). * [#1567](https://github.com/ruby-grape/grape/pull/1567): Fix values validator when value is empty array and apply except to input array - [@jlfaber](https://github.com/jlfaber). * [#1569](https://github.com/ruby-grape/grape/pull/1569), [#1511](https://github.com/ruby-grape/grape/issues/1511): Upgrade mustermann-grape to 1.0.0 - [@namusyaka](https://github.com/namusyaka). * [#1589](https://github.com/ruby-grape/grape/pull/1589): [#726](https://github.com/ruby-grape/grape/issues/726): Use default_format when Content-type is missing and respond with 406 when Content-type is invalid - [@inclooder](https://github.com/inclooder). ### 0.19.1 (2017/1/9) #### Features * [#1536](https://github.com/ruby-grape/grape/pull/1536): Updated `invalid_versioner_option` translation - [@Lavode](https://github.com/Lavode). * [#1543](https://github.com/ruby-grape/grape/pull/1543): Added support for ruby 2.4 - [@LeFnord](https://github.com/LeFnord), [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1548](https://github.com/ruby-grape/grape/pull/1548): Fix: avoid failing even if given path does not match with prefix - [@thomas-peyric](https://github.com/thomas-peyric), [@namusyaka](https://github.com/namusyaka). * [#1550](https://github.com/ruby-grape/grape/pull/1550): Fix: return 200 as default status for DELETE - [@jthornec](https://github.com/jthornec). ### 0.19.0 (2016/12/18) #### Features * [#1503](https://github.com/ruby-grape/grape/pull/1503): Allowed use of regexp validator with arrays - [@akoltun](https://github.com/akoltun). * [#1507](https://github.com/ruby-grape/grape/pull/1507): Added group attributes for parameter definitions - [@304](https://github.com/304). * [#1532](https://github.com/ruby-grape/grape/pull/1532): Set 204 as default status for DELETE - [@LeFnord](https://github.com/LeFnord). #### Fixes * [#1505](https://github.com/ruby-grape/grape/pull/1505): Run `before` and `after` callbacks, but skip the rest when handling OPTIONS - [@jlfaber](https://github.com/jlfaber). * [#1517](https://github.com/ruby-grape/grape/pull/1517), [#1089](https://github.com/ruby-grape/grape/pull/1089): Fix: priority of ANY routes - [@namusyaka](https://github.com/namusyaka), [@wagenet](https://github.com/wagenet). * [#1512](https://github.com/ruby-grape/grape/pull/1512): Fix: deeply nested parameters are included within `#declared(params)` - [@krbs](https://github.com/krbs). * [#1510](https://github.com/ruby-grape/grape/pull/1510): Fix: inconsistent validation for multiple parameters - [@dgasper](https://github.com/dgasper). * [#1526](https://github.com/ruby-grape/grape/pull/1526): Reduced warnings caused by instance variables not initialized - [@cpetschnig](https://github.com/cpetschnig). ### 0.18.0 (2016/10/7) #### Features * [#1480](https://github.com/ruby-grape/grape/pull/1480): Used the ruby-grape-danger gem for PR linting - [@dblock](https://github.com/dblock). * [#1486](https://github.com/ruby-grape/grape/pull/1486): Implemented except in values validator - [@jonmchan](https://github.com/jonmchan). * [#1470](https://github.com/ruby-grape/grape/pull/1470): Dropped support for Ruby 2.0 - [@namusyaka](https://github.com/namusyaka). * [#1490](https://github.com/ruby-grape/grape/pull/1490): Switched to Ruby-2.x+ syntax - [@namusyaka](https://github.com/namusyaka). * [#1499](https://github.com/ruby-grape/grape/pull/1499): Support `fail_fast` param validation option - [@dgasper](https://github.com/dgasper). #### Fixes * [#1498](https://github.com/ruby-grape/grape/pull/1498): Fix: skip validations in inactive given blocks - [@jlfaber](https://github.com/jlfaber). * [#1479](https://github.com/ruby-grape/grape/pull/1479): Fix: support inserting middleware before/after anonymous classes in the middleware stack - [@rosa](https://github.com/rosa). * [#1488](https://github.com/ruby-grape/grape/pull/1488): Fix: ensure calling before filters when receiving OPTIONS request - [@namusyaka](https://github.com/namusyaka), [@jlfaber](https://github.com/jlfaber). * [#1493](https://github.com/ruby-grape/grape/pull/1493): Fix: coercion and lambda fails params validation - [@jonmchan](https://github.com/jonmchan). ### 0.17.0 (2016/7/29) #### Features * [#1393](https://github.com/ruby-grape/grape/pull/1393): Middleware can be inserted before or after default Grape middleware - [@ridiculous](https://github.com/ridiculous). * [#1390](https://github.com/ruby-grape/grape/pull/1390): Allowed inserting middleware at arbitrary points in the middleware stack - [@rosa](https://github.com/rosa). * [#1366](https://github.com/ruby-grape/grape/pull/1366): Stored `message_key` on `Grape::Exceptions::Validation` - [@mkou](https://github.com/mkou). * [#1398](https://github.com/ruby-grape/grape/pull/1398): Added `rescue_from :grape_exceptions` - allow Grape to use the built-in `Grape::Exception` handing and use `rescue :all` behavior for everything else - [@mmclead](https://github.com/mmclead). * [#1443](https://github.com/ruby-grape/grape/pull/1443): Extended `given` to receive a `Proc` - [@glaucocustodio](https://github.com/glaucocustodio). * [#1455](https://github.com/ruby-grape/grape/pull/1455): Added an automated PR linter - [@orta](https://github.com/orta). #### Fixes * [#1463](https://github.com/ruby-grape/grape/pull/1463): Fix array indicies in error messages - [@ffloyd](https://github.com/ffloyd). * [#1465](https://github.com/ruby-grape/grape/pull/1465): Fix 'before' being called twice when using not allowed method - [@jsteinberg](https://github.com/jsteinberg). * [#1446](https://github.com/ruby-grape/grape/pull/1446): Fix for `env` inside `before` when using not allowed method - [@leifg](https://github.com/leifg). * [#1438](https://github.com/ruby-grape/grape/pull/1439): Try to dup non-frozen default params with each use - [@jlfaber](https://github.com/jlfaber). * [#1430](https://github.com/ruby-grape/grape/pull/1430): Fix for `declared(params)` inside `route_param` - [@Arkanain](https://github.com/Arkanain). * [#1405](https://github.com/ruby-grape/grape/pull/1405): Fix priority of `rescue_from` clauses applying - [@hedgesky](https://github.com/hedgesky). * [#1365](https://github.com/ruby-grape/grape/pull/1365): Fix finding exception handler in error middleware - [@ktimothy](https://github.com/ktimothy). * [#1380](https://github.com/ruby-grape/grape/pull/1380): Fix `allow_blank: false` for `Time` attributes with valid values causes `NoMethodError` - [@ipkes](https://github.com/ipkes). * [#1384](https://github.com/ruby-grape/grape/pull/1384): Fix parameter validation with an empty optional nested `Array` - [@ipkes](https://github.com/ipkes). * [#1414](https://github.com/ruby-grape/grape/pull/1414): Fix multiple version definitions for path versioning - [@304](https://github.com/304). * [#1415](https://github.com/ruby-grape/grape/pull/1415): Fix `declared(params, include_parent_namespaces: false)` - [@304](https://github.com/304). * [#1421](https://github.com/ruby-grape/grape/pull/1421): Avoid polluting `Grape::Middleware::Error` - [@namusyaka](https://github.com/namusyaka). * [#1422](https://github.com/ruby-grape/grape/pull/1422): Concat parent declared params with current one - [@plukevdh](https://github.com/plukevdh), [@rnubel](https://github.com/rnubel), [@namusyaka](https://github.com/namusyaka). ### 0.16.2 (2016/4/12) #### Features * [#1348](https://github.com/ruby-grape/grape/pull/1348): Fix global functions polluting Grape::API scope - [@dblock](https://github.com/dblock). * [#1357](https://github.com/ruby-grape/grape/pull/1357): Expose Route#options - [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1357](https://github.com/ruby-grape/grape/pull/1357): Don't include fixed named captures as route params - [@namusyaka](https://github.com/namusyaka). * [#1359](https://github.com/ruby-grape/grape/pull/1359): Avoid evaluating the same route twice - [@namusyaka](https://github.com/namusyaka), [@dblock](https://github.com/dblock). * [#1361](https://github.com/ruby-grape/grape/pull/1361): Return 405 correctly even if version is using as header and wrong request method - [@namusyaka](https://github.com/namusyaka), [@dblock](https://github.com/dblock). ### 0.16.1 (2016/4/3) #### Features * [#1276](https://github.com/ruby-grape/grape/pull/1276): Replace rack-mount with new router - [@namusyaka](https://github.com/namusyaka). * [#1321](https://github.com/ruby-grape/grape/pull/1321): Serve files without using FileStreamer-like object - [@lfidnl](https://github.com/lfidnl). * [#1339](https://github.com/ruby-grape/grape/pull/1339): Implement Grape::API.recognize_path - [@namusyaka](https://github.com/namusyaka). #### Fixes * [#1325](https://github.com/ruby-grape/grape/pull/1325): Params: Fix coerce_with helper with Array types - [@ngonzalez](https://github.com/ngonzalez). * [#1326](https://github.com/ruby-grape/grape/pull/1326): Fix wrong behavior for OPTIONS and HEAD requests with catch-all - [@ekampp](https://github.com/ekampp), [@namusyaka](https://github.com/namusyaka). * [#1330](https://github.com/ruby-grape/grape/pull/1330): Add `register` keyword for adding customized parsers and formatters - [@namusyaka](https://github.com/namusyaka). * [#1336](https://github.com/ruby-grape/grape/pull/1336): Do not modify Hash argument to `error!` - [@tjwp](https://github.com/tjwp). ### 0.15.0 (2016/3/8) #### Features * [#1227](https://github.com/ruby-grape/grape/pull/1227): Store `message_key` on `Grape::Exceptions::Validation` - [@stjhimy](https://github.com/sthimy). * [#1232](https://github.com/ruby-grape/grape/pull/1232): Helpers are now available inside `rescue_from` - [@namusyaka](https://github.com/namusyaka). * [#1237](https://github.com/ruby-grape/grape/pull/1237): Allow multiple parameters in `given`, which behaves as if the scopes were nested in the inputted order - [@ochagata](https://github.com/ochagata). * [#1238](https://github.com/ruby-grape/grape/pull/1238): Call `after` of middleware on error - [@namusyaka](https://github.com/namusyaka). * [#1243](https://github.com/ruby-grape/grape/pull/1243): Add `header` support for middleware - [@namusyaka](https://github.com/namusyaka). * [#1252](https://github.com/ruby-grape/grape/pull/1252): Allow default to be a subset or equal to allowed values without raising IncompatibleOptionValues - [@jeradphelps](https://github.com/jeradphelps). * [#1255](https://github.com/ruby-grape/grape/pull/1255): Allow param type definition in `route_param` - [@namusyaka](https://github.com/namusyaka). * [#1257](https://github.com/ruby-grape/grape/pull/1257): Allow Proc, Symbol or String in `rescue_from with: ...` - [@namusyaka](https://github.com/namusyaka). * [#1280](https://github.com/ruby-grape/grape/pull/1280): Support `Rack::Sendfile` middleware - [@lfidnl](https://github.com/lfidnl). * [#1285](https://github.com/ruby-grape/grape/pull/1285): Add a warning for errors appearing in `after` callbacks - [@gregormelhorn](https://github.com/gregormelhorn). * [#1295](https://github.com/ruby-grape/grape/pull/1295): Add custom validation messages for parameter exceptions - [@railsmith](https://github.com/railsmith). #### Fixes * [#1216](https://github.com/ruby-grape/grape/pull/1142): Fix JSON error response when calling `error!` with non-Strings - [@jrforrest](https://github.com/jrforrest). * [#1225](https://github.com/ruby-grape/grape/pull/1225): Fix `given` with nested params not returning correct declared params - [@JanStevens](https://github.com/JanStevens). * [#1249](https://github.com/ruby-grape/grape/pull/1249): Don't fail even if invalid type value is passed to default validator - [@namusyaka](https://github.com/namusyaka). * [#1266](https://github.com/ruby-grape/grape/pull/1266): Fix `Allow` header including `OPTIONS` when `do_not_route_options!` is active - [@arempe93](https://github.com/arempe93). * [#1270](https://github.com/ruby-grape/grape/pull/1270): Fix `param` versioning with a custom parameter - [@wshatch](https://github.com/wshatch). * [#1282](https://github.com/ruby-grape/grape/pull/1282): Fix specs circular dependency - [@304](https://github.com/304). * [#1283](https://github.com/ruby-grape/grape/pull/1283): Fix 500 error for xml format when method is not allowed - [@304](https://github.com/304). * [#1197](https://github.com/ruby-grape/grape/pull/1290): Fix using JSON and Array[JSON] as groups when parameter is optional - [@lukeivers](https://github.com/lukeivers). ### 0.14.0 (2015/12/07) #### Features * [#1218](https://github.com/ruby-grape/grape/pull/1218): Provide array index context in errors - [@towanda](https://github.com/towanda). * [#1196](https://github.com/ruby-grape/grape/pull/1196): Allow multiple `before_each` blocks - [@huynhquancam](https://github.com/huynhquancam). * [#1190](https://github.com/ruby-grape/grape/pull/1190): Bypass formatting for statuses with no entity-body - [@tylerdooling](https://github.com/tylerdooling). * [#1188](https://github.com/ruby-grape/grape/pull/1188): Allow parameters with more than one type - [@dslh](https://github.com/dslh). * [#1179](https://github.com/ruby-grape/grape/pull/1179): Allow all RFC6838 valid characters in header vendor - [@suan](https://github.com/suan). * [#1170](https://github.com/ruby-grape/grape/pull/1170): Allow dashes and periods in header vendor - [@suan](https://github.com/suan). * [#1167](https://github.com/ruby-grape/grape/pull/1167): Convenience wrapper `type: File` for validating multipart file parameters - [@dslh](https://github.com/dslh). * [#1167](https://github.com/ruby-grape/grape/pull/1167): Refactor and extend coercion and type validation system - [@dslh](https://github.com/dslh). * [#1163](https://github.com/ruby-grape/grape/pull/1163): First-class `JSON` parameter type - [@dslh](https://github.com/dslh). * [#1161](https://github.com/ruby-grape/grape/pull/1161): Custom parameter coercion using `coerce_with` - [@dslh](https://github.com/dslh). #### Fixes * [#1194](https://github.com/ruby-grape/grape/pull/1194): Redirect as plain text with message - [@tylerdooling](https://github.com/tylerdooling). * [#1185](https://github.com/ruby-grape/grape/pull/1185): Use formatters for custom vendored content types - [@tylerdooling](https://github.com/tylerdooling). * [#1156](https://github.com/ruby-grape/grape/pull/1156): Fixed `no implicit conversion of Symbol into Integer` with nested `values` validation - [@quickpay](https://github.com/quickpay). * [#1153](https://github.com/ruby-grape/grape/pull/1153): Fixes boolean declaration in an external file - [@towanda](https://github.com/towanda). * [#1142](https://github.com/ruby-grape/grape/pull/1142): Makes #declared unavailable to before filters - [@jrforrest](https://github.com/jrforrest). * [#1114](https://github.com/ruby-grape/grape/pull/1114): Fix regression which broke identical endpoints with different versions - [@suan](https://github.com/suan). * [#1109](https://github.com/ruby-grape/grape/pull/1109): Memoize Virtus attribute and fix memory leak - [@marshall-lee](https://github.com/marshall-lee). * [#1101](https://github.com/ruby-grape/grape/pull/1101): Fix: Incorrect media-type `Accept` header now correctly returns 406 with `strict: true` - [@elliotlarson](https://github.com/elliotlarson). * [#1108](https://github.com/ruby-grape/grape/pull/1039): Raise a warning when `desc` is called with options hash and block - [@rngtng](https://github.com/rngtng). ### 0.13.0 (2015/8/10) #### Features * [#1039](https://github.com/ruby-grape/grape/pull/1039): Added support for custom parameter types - [@rnubel](https://github.com/rnubel). * [#1047](https://github.com/ruby-grape/grape/pull/1047): Adds `given` to DSL::Parameters, allowing for dependent params - [@rnubel](https://github.com/rnubel). * [#1064](https://github.com/ruby-grape/grape/pull/1064): Add public `Grape::Exception::ValidationErrors#full_messages` - [@romanlehnert](https://github.com/romanlehnert). * [#1079](https://github.com/ruby-grape/grape/pull/1079): Added `stream` method to take advantage of `Rack::Chunked` - [@zbelzer](https://github.com/zbelzer). * [#1086](https://github.com/ruby-grape/grape/pull/1086): Added `ActiveSupport::Notifications` instrumentation - [@wagenet](https://github.com/wagenet). #### Fixes * [#1062](https://github.com/ruby-grape/grape/issues/1062): Fix: `Grape::Exceptions::ValidationErrors` will include headers set by `header` - [@yairgo](https://github.com/yairgo). * [#1038](https://github.com/ruby-grape/grape/pull/1038): Avoid dup-ing the `String` class when used in inherited params - [@rnubel](https://github.com/rnubel). * [#1042](https://github.com/ruby-grape/grape/issues/1042): Fix coercion of complex arrays - [@dim](https://github.com/dim). * [#1045](https://github.com/ruby-grape/grape/pull/1045): Do not convert `Rack::Response` to `Rack::Response` in middleware - [@dmitry](https://github.com/dmitry). * [#1048](https://github.com/ruby-grape/grape/pull/1048): Only dup `InheritableValues`, remove support for `deep_dup` - [@toddmazierski](https://github.com/toddmazierski). * [#1052](https://github.com/ruby-grape/grape/pull/1052): Reset `description[:params]` when resetting validations - [@marshall-lee](https://github.com/marshall-lee). * [#1088](https://github.com/ruby-grape/grape/pull/1088): Support ActiveSupport 3.x by explicitly requiring `Hash#except` - [@wagenet](https://github.com/wagenet). * [#1096](https://github.com/ruby-grape/grape/pull/1096): Fix coercion on booleans - [@towanda](https://github.com/towanda). ### 0.12.0 (2015/6/18) #### Features * [#995](https://github.com/ruby-grape/grape/issues/995): Added support for coercion to Set or Set[Other] - [@jordansexton](https://github.com/jordansexton) [@u2](https://github.com/u2). * [#980](https://github.com/ruby-grape/grape/issues/980): Grape is now eager-loaded - [@u2](https://github.com/u2). * [#956](https://github.com/ruby-grape/grape/issues/956): Support `present` with `Grape::Presenters::Presenter` - [@u2](https://github.com/u2). * [#974](https://github.com/ruby-grape/grape/pull/974): Added `error!` to `rescue_from` blocks - [@whatasunnyday](https://github.com/whatasunnyday). * [#950](https://github.com/ruby-grape/grape/pull/950): Status method can now accept one of Rack::Utils status code symbols (:ok, :found, :bad_request, etc.) - [@dabrorius](https://github.com/dabrorius). * [#952](https://github.com/ruby-grape/grape/pull/952): Status method now raises error when called with invalid status code - [@dabrorius](https://github.com/dabrorius). * [#957](https://github.com/ruby-grape/grape/pull/957): Regexp validator now supports `allow_blank`, `nil` value behavior changed - [@calfzhou](https://github.com/calfzhou). * [#962](https://github.com/ruby-grape/grape/pull/962): The `default` attribute with `false` value is documented now - [@ajvondrak](https://github.com/ajvondrak). * [#1026](https://github.com/ruby-grape/grape/pull/1026): Added `file` method, explicitly setting a file-like response object - [@dblock](https://github.com/dblock). #### Fixes * [#994](https://github.com/ruby-grape/grape/pull/994): Fixed optional Array params default to Hash - [@u2](https://github.com/u2). * [#988](https://github.com/ruby-grape/grape/pull/988): Fixed duplicate identical endpoints - [@u2](https://github.com/u2). * [#936](https://github.com/ruby-grape/grape/pull/936): Fixed default params processing for optional groups - [@dm1try](https://github.com/dm1try). * [#942](https://github.com/ruby-grape/grape/pull/942): Fixed forced presence for optional params when based on a reused entity that was also required in another context - [@croeck](https://github.com/croeck). * [#1001](https://github.com/ruby-grape/grape/pull/1001): Fixed calling endpoint with specified format with format in its path - [@hodak](https://github.com/hodak). * [#1005](https://github.com/ruby-grape/grape/pull/1005): Fixed the Grape::Middleware::Globals - [@urkle](https://github.com/urkle). * [#1012](https://github.com/ruby-grape/grape/pull/1012): Fixed `allow_blank: false` with a Boolean value of `false` - [@mfunaro](https://github.com/mfunaro). * [#1023](https://github.com/ruby-grape/grape/issues/1023): Fixes unexpected behavior with `present` and an object that responds to `merge` but isn't a Hash - [@dblock](https://github.com/dblock). * [#1017](https://github.com/ruby-grape/grape/pull/1017): Fixed `undefined method stringify_keys` with nested mutual exclusive params - [@quickpay](https://github.com/quickpay). ### 0.11.0 (2015/2/23) * [#925](https://github.com/ruby-grape/grape/pull/925): Fixed `toplevel constant DateTime referenced by Virtus::Attribute::DateTime` - [@u2](https://github.com/u2). * [#916](https://github.com/ruby-grape/grape/pull/916): Added `DateTime/Date/Numeric/Boolean` type support `allow_blank` - [@u2](https://github.com/u2). * [#871](https://github.com/ruby-grape/grape/pull/871): Fixed `Grape::Middleware::Base#response` - [@galathius](https://github.com/galathius). * [#559](https://github.com/ruby-grape/grape/issues/559): Added support for Rack 1.6.0, which parses requests larger than 128KB - [@myitcv](https://github.com/myitcv). * [#876](https://github.com/ruby-grape/grape/pull/876): Call to `declared(params)` now returns a `Hashie::Mash` - [@rodzyn](https://github.com/rodzyn). * [#879](https://github.com/ruby-grape/grape/pull/879): The `route_info` value is no longer included in `params` Hash - [@rodzyn](https://github.com/rodzyn). * [#881](https://github.com/ruby-grape/grape/issues/881): Fixed `Grape::Validations::ValuesValidator` support for `Range` type - [@ajvondrak](https://github.com/ajvondrak). * [#901](https://github.com/ruby-grape/grape/pull/901): Fix: callbacks defined in a version block are only called for the routes defined in that block - [@kushkella](https://github.com/kushkella). * [#886](https://github.com/ruby-grape/grape/pull/886): Group of parameters made to require an explicit type of Hash or Array - [@jrichter1](https://github.com/jrichter1). * [#912](https://github.com/ruby-grape/grape/pull/912): Extended the `:using` feature for param documentation to `optional` fields - [@croeck](https://github.com/croeck). * [#906](https://github.com/ruby-grape/grape/pull/906): Fix: invalid body parse errors are not rescued by handlers - [@croeck](https://github.com/croeck). * [#913](https://github.com/ruby-grape/grape/pull/913): Fix: Invalid accept headers are not processed by rescue handlers - [@croeck](https://github.com/croeck). * [#913](https://github.com/ruby-grape/grape/pull/913): Fix: Invalid accept headers cause internal processing errors (500) when http_codes are defined - [@croeck](https://github.com/croeck). * [#917](https://github.com/ruby-grape/grape/pull/917): Use HTTPS for rubygems.org - [@O-I](https://github.com/O-I). ### 0.10.1 (2014/12/28) * [#868](https://github.com/ruby-grape/grape/pull/868), [#862](https://github.com/ruby-grape/grape/pull/862), [#861](https://github.com/ruby-grape/grape/pull/861): Fixed `version`, `prefix`, and other settings being overridden or changing scope when mounting API - [@yesmeck](https://github.com/yesmeck). * [#864](https://github.com/ruby-grape/grape/pull/864): Fixed `declared(params, include_missing: false)` now returning attributes with `nil` and `false` values - [@ppadron](https://github.com/ppadron). ### 0.10.0 (2014/12/19) * [#803](https://github.com/ruby-grape/grape/pull/803), [#820](https://github.com/ruby-grape/grape/pull/820): Added `all_or_none_of` parameter validator - [@loveltyoic](https://github.com/loveltyoic), [@natecj](https://github.com/natecj). * [#774](https://github.com/ruby-grape/grape/pull/774): Extended `mutually_exclusive`, `exactly_one_of`, `at_least_one_of` to work inside any kind of group: `requires` or `optional`, `Hash` or `Array` - [@ShPakvel](https://github.com/ShPakvel). * [#743](https://github.com/ruby-grape/grape/pull/743): Added `allow_blank` parameter validator to validate non-empty strings - [@elado](https://github.com/elado). * [#745](https://github.com/ruby-grape/grape/pull/745): Removed `atom+xml`, `rss+xml`, and `jsonapi` content-types - [@akabraham](https://github.com/akabraham). * [#745](https://github.com/ruby-grape/grape/pull/745): Added `:binary, application/octet-stream` content-type - [@akabraham](https://github.com/akabraham). * [#757](https://github.com/ruby-grape/grape/pull/757): Changed `desc` can now be used with a block syntax - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#779](https://github.com/ruby-grape/grape/pull/779): Fixed using `values` with a `default` proc - [@ShPakvel](https://github.com/ShPakvel). * [#799](https://github.com/ruby-grape/grape/pull/799): Fixed custom validators with required `Hash`, `Array` types - [@bwalex](https://github.com/bwalex). * [#784](https://github.com/ruby-grape/grape/pull/784): Fixed `present` to not overwrite the previously added contents of the response body whebn called more than once - [@mfunaro](https://github.com/mfunaro). * [#809](https://github.com/ruby-grape/grape/pull/809): Removed automatic `(.:format)` suffix on paths if you're using only one format (e.g., with `format :json`, `/path` will respond with JSON but `/path.xml` will be a 404) - [@ajvondrak](https://github.com/ajvondrak). * [#816](https://github.com/ruby-grape/grape/pull/816): Added ability to filter out missing params if params is a nested hash with `declared(params, include_missing: false)` - [@georgimitev](https://github.com/georgimitev). * [#819](https://github.com/ruby-grape/grape/pull/819): Allowed both `desc` and `description` in the params DSL - [@mzikherman](https://github.com/mzikherman). * [#821](https://github.com/ruby-grape/grape/pull/821): Fixed passing string value when hash is expected in params - [@rebelact](https://github.com/rebelact). * [#824](https://github.com/ruby-grape/grape/pull/824): Validate array params against list of acceptable values - [@dnd](https://github.com/dnd). * [#813](https://github.com/ruby-grape/grape/pull/813): Routing methods dsl refactored to get rid of explicit `paths` parameter - [@AlexYankee](https://github.com/AlexYankee). * [#826](https://github.com/ruby-grape/grape/pull/826): Find `coerce_type` for `Array` when not specified - [@manovotn](https://github.com/manovotn). * [#645](https://github.com/ruby-grape/grape/issues/645): Invoking `body false` will return `204 No Content` - [@dblock](https://github.com/dblock). * [#801](https://github.com/ruby-grape/grape/issues/801): Only evaluate permitted parameter `values` and `default` lazily on each request when declared as a proc - [@dblock](https://github.com/dblock). * [#679](https://github.com/ruby-grape/grape/issues/679): Fixed `OPTIONS` method returning 404 when combined with `prefix` - [@dblock](https://github.com/dblock). * [#679](https://github.com/ruby-grape/grape/issues/679): Fixed unsupported methods returning 404 instead of 405 when combined with `prefix` - [@dblock](https://github.com/dblock). ### 0.9.0 (2014/8/27) #### Features * [#691](https://github.com/ruby-grape/grape/issues/691): Added `at_least_one_of` parameter validator - [@dblock](https://github.com/dblock). * [#698](https://github.com/ruby-grape/grape/pull/698): `error!` sets `status` for `Endpoint` too - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Added support for Auth-Middleware extension - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::Basic` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::Digest` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#703](https://github.com/ruby-grape/grape/pull/703): Removed `Grape::Middleware::Auth::OAuth2` - [@dspaeth-faber](https://github.com/dspaeth-faber). * [#719](https://github.com/ruby-grape/grape/pull/719): Allow passing options hash to a custom validator - [@elado](https://github.com/elado). * [#716](https://github.com/ruby-grape/grape/pull/716): Calling `content-type` will now return the current content-type - [@dblock](https://github.com/dblock). * [#705](https://github.com/ruby-grape/grape/pull/705): Errors can now be presented with a `Grape::Entity` class - [@dspaeth-faber](https://github.com/dspaeth-faber). #### Fixes * [#687](https://github.com/ruby-grape/grape/pull/687): Fix: `mutually_exclusive` and `exactly_one_of` validation error messages now label parameters as strings, consistently with `requires` and `optional` - [@dblock](https://github.com/dblock). ### 0.8.0 (2014/7/10) #### Features * [#639](https://github.com/ruby-grape/grape/pull/639): Added support for blocks with reusable params - [@mibon](https://github.com/mibon). * [#637](https://github.com/ruby-grape/grape/pull/637): Added support for `exactly_one_of` parameter validation - [@Morred](https://github.com/Morred). * [#626](https://github.com/ruby-grape/grape/pull/626): Added support for `mutually_exclusive` parameters - [@oliverbarnes](https://github.com/oliverbarnes). * [#617](https://github.com/ruby-grape/grape/pull/617): Running tests on Ruby 2.1.1, Rubinius 2.1 and 2.2, Ruby and JRuby HEAD - [@dblock](https://github.com/dblock). * [#397](https://github.com/ruby-grape/grape/pull/397): Adds `Grape::Endpoint.before_each` to allow easy helper stubbing - [@mbleigh](https://github.com/mbleigh). * [#673](https://github.com/ruby-grape/grape/pull/673): Avoid requiring non-existent fields when using Grape::Entity documentation - [@qqshfox](https://github.com/qqshfox). #### Fixes * [#671](https://github.com/ruby-grape/grape/pull/671): Allow required param with predefined set of values to be nil inside optional group - [@dm1try](https://github.com/dm1try). * [#651](https://github.com/ruby-grape/grape/pull/651): The `rescue_from` keyword now properly defaults to rescuing subclasses of exceptions - [@xevix](https://github.com/xevix). * [#614](https://github.com/ruby-grape/grape/pull/614): Params with `nil` value are now refused by `RegexpValidator` - [@dm1try](https://github.com/dm1try). * [#494](https://github.com/ruby-grape/grape/issues/494): Fixed performance issue with requests carrying a large payload - [@dblock](https://github.com/dblock). * [#619](https://github.com/ruby-grape/grape/pull/619): Convert specs to RSpec 3 syntax with Transpec - [@danielspector](https://github.com/danielspector). * [#632](https://github.com/ruby-grape/grape/pull/632): `Grape::Endpoint#present` causes ActiveRecord to make an extra query during entity's detection - [@fixme](https://github.com/fixme). ### 0.7.0 (2014/4/2) #### Features * [#558](https://github.com/ruby-grape/grape/pull/558): Support lambda-based values for params - [@wpschallenger](https://github.com/wpschallenger). * [#510](https://github.com/ruby-grape/grape/pull/510): Support lambda-based default values for params - [@myitcv](https://github.com/myitcv). * [#511](https://github.com/ruby-grape/grape/pull/511): Added `required` option for OAuth2 middleware - [@bcm](https://github.com/bcm). * [#520](https://github.com/ruby-grape/grape/pull/520): Use `default_error_status` to specify the default status code returned from `error!` - [@salimane](https://github.com/salimane). * [#525](https://github.com/ruby-grape/grape/pull/525): The default status code returned from `error!` has been changed from 403 to 500 - [@dblock](https://github.com/dblock). * [#526](https://github.com/ruby-grape/grape/pull/526): Allowed specifying headers in `error!` - [@dblock](https://github.com/dblock). * [#527](https://github.com/ruby-grape/grape/pull/527): The `before_validation` callback is now a distinct one - [@myitcv](https://github.com/myitcv). * [#530](https://github.com/ruby-grape/grape/pull/530): Added ability to restrict `declared(params)` to the local endpoint with `include_parent_namespaces: false` - [@myitcv](https://github.com/myitcv). * [#531](https://github.com/ruby-grape/grape/pull/531): Helpers are now available to auth middleware, executing in the context of the endpoint - [@joelvh](https://github.com/joelvh). * [#540](https://github.com/ruby-grape/grape/pull/540): Ruby 2.1.0 is now supported - [@salimane](https://github.com/salimane). * [#544](https://github.com/ruby-grape/grape/pull/544): The `rescue_from` keyword now handles subclasses of exceptions by default - [@xevix](https://github.com/xevix). * [#545](https://github.com/ruby-grape/grape/pull/545): Added `type` (`Array` or `Hash`) support to `requires`, `optional` and `group` - [@bwalex](https://github.com/bwalex). * [#550](https://github.com/ruby-grape/grape/pull/550): Added possibility to define reusable params - [@dm1try](https://github.com/dm1try). * [#560](https://github.com/ruby-grape/grape/pull/560): Use `Grape::Entity` documentation to define required and optional parameters with `requires using:` - [@reynardmh](https://github.com/reynardmh). * [#572](https://github.com/ruby-grape/grape/pull/572): Added `documentation` support to `requires`, `optional` and `group` parameters - [@johnallen3d](https://github.com/johnallen3d). #### Fixes * [#600](https://github.com/ruby-grape/grape/pull/600): Don't use an `Entity` constant that is available in the namespace as presenter - [@fuksito](https://github.com/fuksito). * [#590](https://github.com/ruby-grape/grape/pull/590): Fix issue where endpoint param of type `Integer` cannot set values array - [@xevix](https://github.com/xevix). * [#586](https://github.com/ruby-grape/grape/pull/586): Do not repeat the same validation error messages - [@kiela](https://github.com/kiela). * [#508](https://github.com/ruby-grape/grape/pull/508): Allow parameters, such as content encoding, in `content_type` - [@dm1try](https://github.com/dm1try). * [#492](https://github.com/ruby-grape/grape/pull/492): Don't allow to have nil value when a param is required and has a list of allowed values - [@Antti](https://github.com/Antti). * [#495](https://github.com/ruby-grape/grape/pull/495): Fixed `ParamsScope#params` for parameters nested inside arrays - [@asross](https://github.com/asross). * [#498](https://github.com/ruby-grape/grape/pull/498): Dry'ed up options and headers logic, allow headers to be passed to OPTIONS requests - [@karlfreeman](https://github.com/karlfreeman). * [#500](https://github.com/ruby-grape/grape/pull/500): Skip entity auto-detection when explicitly passed - [@yaneq](https://github.com/yaneq). * [#503](https://github.com/ruby-grape/grape/pull/503): Calling declared(params) from child namespace fails to include parent namespace defined params - [@myitcv](https://github.com/myitcv). * [#512](https://github.com/ruby-grape/grape/pull/512): Don't create `Grape::Request` multiple times - [@dblock](https://github.com/dblock). * [#538](https://github.com/ruby-grape/grape/pull/538): Fixed default values for grouped params - [@dm1try](https://github.com/dm1try). * [#549](https://github.com/ruby-grape/grape/pull/549): Fixed handling of invalid version headers to return 406 if a header cannot be parsed - [@bwalex](https://github.com/bwalex). * [#557](https://github.com/ruby-grape/grape/pull/557): Pass `content_types` option to `Grape::Middleware::Error` to fix the content-type header for custom formats - [@bernd](https://github.com/bernd). * [#585](https://github.com/ruby-grape/grape/pull/585): Fix after boot thread-safety issue - [@etehtsea](https://github.com/etehtsea). * [#587](https://github.com/ruby-grape/grape/pull/587): Fix oauth2 middleware compatibility with [draft-ietf-oauth-v2-31](http://tools.ietf.org/html/draft-ietf-oauth-v2-31) spec - [@etehtsea](https://github.com/etehtsea). * [#610](https://github.com/ruby-grape/grape/pull/610): Fixed group keyword was not working with type parameter - [@klausmeyer](https://github.com/klausmeyer). ### 0.6.1 (2013/10/19) #### Features * [#475](https://github.com/ruby-grape/grape/pull/475): Added support for the `:jsonapi`, `application/vnd.api+json` media type registered at http://jsonapi.org - [@bcm](https://github.com/bcm). * [#471](https://github.com/ruby-grape/grape/issues/471): Added parameter validator for a list of allowed values - [@vickychijwani](https://github.com/vickychijwani). * [#488](https://github.com/ruby-grape/grape/issues/488): Upgraded to Virtus 1.0 - [@dblock](https://github.com/dblock). #### Fixes * [#477](https://github.com/ruby-grape/grape/pull/477): Fixed `default_error_formatter` which takes a format symbol - [@vad4msiu](https://github.com/vad4msiu). #### Development * Implemented Rubocop, a Ruby code static code analyzer - [@dblock](https://github.com/dblock). ### 0.6.0 (2013/9/16) #### Features * Grape is no longer tested against Ruby 1.8.7 - [@dblock](https://github.com/dblock). * [#442](https://github.com/ruby-grape/grape/issues/442): Enable incrementally building on top of a previous API version - [@dblock](https://github.com/dblock). * [#442](https://github.com/ruby-grape/grape/issues/442): API `version` can now take an array of multiple versions - [@dblock](https://github.com/dblock). * [#444](https://github.com/ruby-grape/grape/issues/444): Added `:en` as fallback locale for I18n - [@aew](https://github.com/aew). * [#448](https://github.com/ruby-grape/grape/pull/448): Adding POST style parameters for DELETE requests - [@dquimper](https://github.com/dquimper). * [#450](https://github.com/ruby-grape/grape/pull/450): Added option to pass an exception handler lambda as an argument to `rescue_from` - [@robertopedroso](https://github.com/robertopedroso). * [#443](https://github.com/ruby-grape/grape/pull/443): Let `requires` and `optional` take blocks that initialize new scopes - [@asross](https://github.com/asross). * [#452](https://github.com/ruby-grape/grape/pull/452): Added `with` as a hash option to specify handlers for `rescue_from` and `error_formatter` - [@robertopedroso](https://github.com/robertopedroso). * [#433](https://github.com/ruby-grape/grape/issues/433), [#462](https://github.com/ruby-grape/grape/issues/462): Validation errors are now collected and `Grape::Exceptions::ValidationErrors` is raised - [@stevschmid](https://github.com/stevschmid). #### Fixes * [#428](https://github.com/ruby-grape/grape/issues/428): Removes memoization from `Grape::Request` params to prevent middleware from freezing parameter values before `Formatter` can get them - [@mbleigh](https://github.com/mbleigh). ### 0.5.0 (2013/6/14) #### Features * [#344](https://github.com/ruby-grape/grape/pull/344): Added `parser :type, nil` which disables input parsing for a given content-type - [@dblock](https://github.com/dblock). * [#381](https://github.com/ruby-grape/grape/issues/381): Added `cascade false` option at API level to remove the `X-Cascade: true` header from the API response - [@dblock](https://github.com/dblock). * [#392](https://github.com/ruby-grape/grape/pull/392): Extracted headers and params from `Endpoint` to `Grape::Request` - [@niedhui](https://github.com/niedhui). * [#376](https://github.com/ruby-grape/grape/pull/376): Added `route_param`, syntax sugar for quick declaration of route parameters - [@mbleigh](https://github.com/mbleigh). * [#390](https://github.com/ruby-grape/grape/pull/390): Added default value for an `optional` parameter - [@oivoodoo](https://github.com/oivoodoo). * [#403](https://github.com/ruby-grape/grape/pull/403): Added support for versioning using the `Accept-Version` header - [@politician](https://github.com/politician). * [#407](https://github.com/ruby-grape/grape/issues/407): Specifying `default_format` will also set the default POST/PUT data parser to the given format - [@dblock](https://github.com/dblock). * [#241](https://github.com/ruby-grape/grape/issues/241): Present with multiple entities using an optional Symbol - [@niedhui](https://github.com/niedhui). #### Fixes * [#378](https://github.com/ruby-grape/grape/pull/378): Fix: stop rescuing all exceptions during formatting - [@kbarrette](https://github.com/kbarrette). * [#380](https://github.com/ruby-grape/grape/pull/380): Fix: `Formatter#read_body_input` when transfer encoding is chunked - [@paulnicholon](https://github.com/paulnicholson). * [#347](https://github.com/ruby-grape/grape/issues/347): Fix: handling non-hash body params - [@paulnicholon](https://github.com/paulnicholson). * [#394](https://github.com/ruby-grape/grape/pull/394): Fix: path version no longer overwrites a `version` parameter - [@tmornini](https://github.com/tmornini). * [#412](https://github.com/ruby-grape/grape/issues/412): Fix: specifying `content_type` will also override the selection of the data formatter - [@dblock](https://github.com/dblock). * [#383](https://github.com/ruby-grape/grape/issues/383): Fix: Mounted APIs aren't inheriting settings (including `before` and `after` filters) - [@seanmoon](https://github.com/seanmoon). * [#408](https://github.com/ruby-grape/grape/pull/408): Fix: Goliath passes request header keys as symbols not strings - [@bobek](https://github.com/bobek). * [#417](https://github.com/ruby-grape/grape/issues/417): Fix: Rails 4 does not rewind input, causes POSTed data to be empty - [@dblock](https://github.com/dblock). * [#423](https://github.com/ruby-grape/grape/pull/423): Fix: `Grape::Endpoint#declared` now correctly handles nested params (ie. declared with `group`) - [@jbarreneche](https://github.com/jbarreneche). * [#427](https://github.com/ruby-grape/grape/issues/427): Fix: `declared(params)` breaks when `params` contains array - [@timhabermaas](https://github.com/timhabermaas). ### 0.4.1 (2013/4/1) * [#375](https://github.com/ruby-grape/grape/pull/375): Fix: throwing an `:error` inside a middleware doesn't respect the `format` settings - [@dblock](https://github.com/dblock). ### 0.4.0 (2013/3/17) * [#356](https://github.com/ruby-grape/grape/pull/356): Fix: presenting collections other than `Array` (eg. `ActiveRecord::Relation`) - [@zimbatm](https://github.com/zimbatm). * [#352](https://github.com/ruby-grape/grape/pull/352): Fix: using `Rack::JSONP` with `Grape::Entity` responses - [@deckchair](https://github.com/deckchair). * [#347](https://github.com/ruby-grape/grape/issues/347): Grape will accept any valid JSON as PUT or POST, including strings, symbols and arrays - [@qqshfox](https://github.com/qqshfox), [@dblock](https://github.com/dblock). * [#347](https://github.com/ruby-grape/grape/issues/347): JSON format APIs always return valid JSON, eg. strings are now returned as `"string"` and no longer `string` - [@dblock](https://github.com/dblock). * Raw body input from POST and PUT requests (`env['rack.input'].read`) is now available in `api.request.input` - [@dblock](https://github.com/dblock). * Parsed body input from POST and PUT requests is now available in `api.request.body` - [@dblock](https://github.com/dblock). * [#343](https://github.com/ruby-grape/grape/pull/343): Fix: return `Content-Type: text/plain` with error 405 - [@gustavosaume](https://github.com/gustavosaume), [@wyattisimo](https://github.com/wyattisimo). * [#357](https://github.com/ruby-grape/grape/pull/357): Grape now requires Rack 1.3.0 or newer - [@jhecking](https://github.com/jhecking). * [#320](https://github.com/ruby-grape/grape/issues/320): API `namespace` now supports `requirements` - [@niedhui](https://github.com/niedhui). * [#353](https://github.com/ruby-grape/grape/issues/353): Revert to standard Ruby logger formatter, `require active_support/all` if you want old behavior - [@rhunter](https://github.com/rhunter), [@dblock](https://github.com/dblock). * Fix: `undefined method 'call' for nil:NilClass` for an API method implementation without a block, now returns an empty string - [@dblock](https://github.com/dblock). ### 0.3.2 (2013/2/28) * [#355](https://github.com/ruby-grape/grape/issues/355): Relax dependency constraint on Hashie - [@reset](https://github.com/reset). ### 0.3.1 (2013/2/25) * [#351](https://github.com/ruby-grape/grape/issues/351): Compatibility with Ruby 2.0 - [@mbleigh](https://github.com/mbleigh). ### 0.3.0 (2013/02/21) * [#294](https://github.com/ruby-grape/grape/issues/294): Extracted `Grape::Entity` into a [grape-entity](https://github.com/agileanimal/grape-entity) gem - [@agileanimal](https://github.com/agileanimal). * [#340](https://github.com/ruby-grape/grape/pull/339), [#342](https://github.com/ruby-grape/grape/pull/342): Added `:cascade` option to `version` to allow disabling of rack/mount cascade behavior - [@dieb](https://github.com/dieb). * [#333](https://github.com/ruby-grape/grape/pull/333): Added support for validation of arrays in `params` - [@flyerhzm](https://github.com/flyerhzm). * [#306](https://github.com/ruby-grape/grape/issues/306): Added I18n support for all Grape exceptions - [@niedhui](https://github.com/niedhui). * [#309](https://github.com/ruby-grape/grape/pull/309): Added XML support to the entity presenter - [@johnnyiller](https://github.com/johnnyiller), [@dblock](https://github.com/dblock). * [#131](https://github.com/ruby-grape/grape/issues/131): Added instructions for Grape API reloading in Rails - [@jyn](https://github.com/jyn), [@dblock](https://github.com/dblock). * [#317](https://github.com/ruby-grape/grape/issues/317): Added `headers` that returns a hash of parsed HTTP request headers - [@dblock](https://github.com/dblock). * [#332](https://github.com/ruby-grape/grape/pull/332): `Grape::Exceptions::Validation` now contains full nested parameter names - [@alovak](https://github.com/alovak). * [#328](https://github.com/ruby-grape/grape/issues/328): API version can now be specified as both String and Symbol - [@dblock](https://github.com/dblock). * [#190](https://github.com/ruby-grape/grape/issues/190): When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!` - [@dblock](https://github.com/dblock). * Added `do_not_route_options!`, which disables the automatic creation of the `OPTIONS` route - [@dblock](https://github.com/dblock). * [#309](https://github.com/ruby-grape/grape/pull/309): An XML format API will return an error instead of returning a string representation of the response if the latter cannot be converted to XML - [@dblock](https://github.com/dblock). * A formatter that raises an exception will cause the API to return a 500 error - [@dblock](https://github.com/dblock). * [#322](https://github.com/ruby-grape/grape/issues/322): When returning a 406 status, Grape will include the requested format or content-type in the response body - [@dblock](https://github.com/dblock). * [#60](https://github.com/ruby-grape/grape/issues/60): Fix: mounting of a Grape API onto a path - [@dblock](https://github.com/dblock). * [#335](https://github.com/ruby-grape/grape/pull/335): Fix: request body parameters from a `PATCH` request not available in `params` - [@FreakenK](https://github.com/FreakenK). ### 0.2.6 (2013/01/11) * Fix: support content-type with character set when parsing POST and PUT input - [@dblock](https://github.com/dblock). * Fix: CVE-2013-0175, multi_xml parse vulnerability, require multi_xml 0.5.2 - [@dblock](https://github.com/dblock). ### 0.2.5 (2013/01/10) * Added support for custom parsers via `parser`, in addition to built-in multipart, JSON and XML parsers - [@dblock](https://github.com/dblock). * Removed `body_params`, data sent via a POST or PUT with a supported content-type is merged into `params` - [@dblock](https://github.com/dblock). * Setting `format` will automatically remove other content-types by calling `content_type` - [@dblock](https://github.com/dblock). * Setting `content_type` will prevent any input data other than the matching content-type or any Rack-supported form and parseable media types (`application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`) from being parsed - [@dblock](https://github.com/dblock). * [#305](https://github.com/ruby-grape/grape/issues/305): Fix: presenting arrays of objects via `represent` or when auto-detecting an `Entity` constant in the objects being presented - [@brandonweiss](https://github.com/brandonweiss). * [#306](https://github.com/ruby-grape/grape/issues/306): Added i18n support for validation error messages - [@niedhui](https://github.com/niedhui). ### 0.2.4 (2013/01/06) * [#297](https://github.com/ruby-grape/grape/issues/297): Added `default_error_formatter` - [@dblock](https://github.com/dblock). * [#297](https://github.com/ruby-grape/grape/issues/297): Setting `format` will automatically set `default_error_formatter` - [@dblock](https://github.com/dblock). * [#295](https://github.com/ruby-grape/grape/issues/295): Storing original API source block in endpoint's `source` attribute - [@dblock](https://github.com/dblock). * [#293](https://github.com/ruby-grape/grape/pull/293): Added options to `cookies.delete`, enables passing a path - [@inst](https://github.com/inst). * [#174](https://github.com/ruby-grape/grape/issues/174): The value of `env['PATH_INFO']` is no longer altered with `path` versioning - [@dblock](https://github.com/dblock). * [#296](https://github.com/ruby-grape/grape/issues/296): Fix: ArgumentError with default error formatter - [@dblock](https://github.com/dblock). * [#298](https://github.com/ruby-grape/grape/pull/298): Fix: subsequent calls to `body_params` would fail due to IO read - [@justinmcp](https://github.com/justinmcp). * [#301](https://github.com/ruby-grape/grape/issues/301): Fix: symbol memory leak in cookie and formatter middleware - [@dblock](https://github.com/dblock). * [#300](https://github.com/ruby-grape/grape/issues/300): Fix `Grape::API.routes` to include mounted api routes - [@aiwilliams](https://github.com/aiwilliams). * [#302](https://github.com/ruby-grape/grape/pull/302): Fix: removed redundant `autoload` entries - [@ugisozols](https://github.com/ugisozols). * [#172](https://github.com/ruby-grape/grape/issues/172): Fix: MultiJson deprecated methods warnings - [@dblock](https://github.com/dblock). * [#133](https://github.com/ruby-grape/grape/issues/133): Fix: header-based versioning with use of `prefix` - [@seanmoon](https://github.com/seanmoon), [@dblock](https://github.com/dblock). * [#280](https://github.com/ruby-grape/grape/issues/280): Fix: grouped parameters mangled in `route_params` hash - [@marcusg](https://github.com/marcusg), [@dblock](https://github.com/dblock). * [#304](https://github.com/ruby-grape/grape/issues/304): Fix: `present x, :with => Entity` returns class references with `format :json` - [@dblock](https://github.com/dblock). * [#196](https://github.com/ruby-grape/grape/issues/196): Fix: root requests don't work with `prefix` - [@dblock](https://github.com/dblock). ### 0.2.3 (2012/12/24) * [#179](https://github.com/ruby-grape/grape/issues/178): Using `content_type` will remove all default content-types - [@dblock](https://github.com/dblock). * [#265](https://github.com/ruby-grape/grape/issues/264): Fix: Moved `ValidationError` into `Grape::Exceptions` - [@thepumpkin1979](https://github.com/thepumpkin1979). * [#269](https://github.com/ruby-grape/grape/pull/269): Fix: `LocalJumpError` will not be raised when using explict return in API methods - [@simulacre](https://github.com/simulacre). * [#86](https://github.com/ruby-grape/grape/issues/275): Fix Path-based versioning not recognizing `/` route - [@walski](https://github.com/walski). * [#273](https://github.com/ruby-grape/grape/pull/273): Disabled formatting via `serializable_hash` and added support for `format :serializable_hash` - [@dblock](https://github.com/dblock). * [#277](https://github.com/ruby-grape/grape/pull/277): Added a DSL to declare `formatter` in API settings - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#284](https://github.com/ruby-grape/grape/pull/284): Added a DSL to declare `error_formatter` in API settings - [@dblock](https://github.com/dblock). * [#285](https://github.com/ruby-grape/grape/pull/285): Removed `error_format` from API settings, now matches request format - [@dblock](https://github.com/dblock). * [#290](https://github.com/ruby-grape/grape/pull/290): The default error format for XML is now `error/message` instead of `hash/error` - [@dpsk](https://github.com/dpsk). * [#44](https://github.com/ruby-grape/grape/issues/44): Pass `env` into formatters to enable templating - [@dblock](https://github.com/dblock). ### 0.2.2 (2012/12/10) #### Features * [#201](https://github.com/ruby-grape/grape/pull/201), [#236](https://github.com/ruby-grape/grape/pull/236), [#221](https://github.com/ruby-grape/grape/pull/221): Added coercion and validations support to `params` DSL - [@schmurfy](https://github.com/schmurfy), [@tim-vandecasteele](https://github.com/tim-vandecasteele), [@adamgotterer](https://github.com/adamgotterer). * [#204](https://github.com/ruby-grape/grape/pull/204): Added ability to declare shared `params` at `namespace` level - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#234](https://github.com/ruby-grape/grape/pull/234): Added a DSL for creating entities via mixin - [@mbleigh](https://github.com/mbleigh). * [#240](https://github.com/ruby-grape/grape/pull/240): Define API response format from a query string `format` parameter, if specified - [@neetiraj](https://github.com/neetiraj). * Adds Endpoint#declared to easily filter out unexpected params - [@mbleigh](https://github.com/mbleigh). #### Fixes * [#248](https://github.com/ruby-grape/grape/pull/248): Fix: API `version` returns last version set - [@narkoz](https://github.com/narkoz). * [#242](https://github.com/ruby-grape/grape/issues/242): Fix: permanent redirect status should be `301`, was `304` - [@adamgotterer](https://github.com/adamgotterer). * [#211](https://github.com/ruby-grape/grape/pull/211): Fix: custom validations are no longer triggered when optional and parameter is not present - [@adamgotterer](https://github.com/adamgotterer). * [#210](https://github.com/ruby-grape/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer). * [#205](https://github.com/ruby-grape/grape/pull/205): Fix: Corrected parsing of empty JSON body on POST/PUT - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#181](https://github.com/ruby-grape/grape/pull/181): Fix: Corrected JSON serialization of nested hashes containing `Grape::Entity` instances - [@benrosenblum](https://github.com/benrosenblum). * [#203](https://github.com/ruby-grape/grape/pull/203): Added a check to `Entity#serializable_hash` that verifies an entity exists on an object - [@adamgotterer](https://github.com/adamgotterer). * [#208](https://github.com/ruby-grape/grape/pull/208): `Entity#serializable_hash` must also check if attribute is generated by a user supplied block - [@ppadron](https://github.com/ppadron). * [#252](https://github.com/ruby-grape/grape/pull/252): Resources that don't respond to a requested HTTP method return 405 (Method Not Allowed) instead of 404 (Not Found) - [@simulacre](https://github.com/simulacre). ### 0.2.1 (2012/7/11) * [#186](https://github.com/ruby-grape/grape/issues/186): Fix: helpers allow multiple calls with modules and blocks - [@ppadron](https://github.com/ppadron). * [#188](https://github.com/ruby-grape/grape/pull/188): Fix: multi-method routes append '(.:format)' only once - [@kainosnoema](https://github.com/kainosnoema). * [#64](https://github.com/ruby-grape/grape/issues/64), [#180](https://github.com/ruby-grape/grape/pull/180): Added support to `GET` request bodies as parameters - [@bobbytables](https://github.com/bobbytables). * [#175](https://github.com/ruby-grape/grape/pull/175): Added support for API versioning based on a request parameter - [@jackcasey](https://github.com/jackcasey). * [#168](https://github.com/ruby-grape/grape/pull/168): Fix: Formatter can parse symbol keys in the headers hash - [@netmask](https://github.com/netmask). * [#169](https://github.com/ruby-grape/grape/pull/169): Silence multi_json deprecation warnings - [@whiteley](https://github.com/whiteley). * [#166](https://github.com/ruby-grape/grape/pull/166): Added support for `redirect`, including permanent and temporary - [@allenwei](https://github.com/allenwei). * [#159](https://github.com/ruby-grape/grape/pull/159): Added `:requirements` to routes, allowing to use reserved characters in paths - [@gaiottino](https://github.com/gaiottino). * [#156](https://github.com/ruby-grape/grape/pull/156): Added support for adding formatters to entities - [@bobbytables](https://github.com/bobbytables). * [#183](https://github.com/ruby-grape/grape/pull/183): Added ability to include documentation in entities - [@flah00](https://github.com/flah00). * [#189](https://github.com/ruby-grape/grape/pull/189): `HEAD` requests no longer return a body - [@stephencelis](https://github.com/stephencelis). * [#97](https://github.com/ruby-grape/grape/issues/97): Allow overriding `Content-Type` - [@dblock](https://github.com/dblock). ### 0.2.0 (2012/3/28) * Added support for inheriting exposures from entities - [@bobbytables](https://github.com/bobbytables). * Extended formatting with `default_format` - [@dblock](https://github.com/dblock). * Added support for cookies - [@lukaszsliwa](https://github.com/lukaszsliwa). * Added support for declaring additional content-types - [@joeyAghion](https://github.com/joeyAghion). * Added support for HTTP PATCH - [@LTe](https://github.com/LTe). * Added support for describing, documenting and reflecting APIs - [@dblock](https://github.com/dblock). * Added support for anchoring and vendoring - [@jwkoelewijn](https://github.com/jwkoelewijn). * Added support for HTTP OPTIONS - [@grimen](https://github.com/grimen). * Added support for silencing logger - [@evansj](https://github.com/evansj). * Added support for helper modules - [@freelancing-god](https://github.com/freelancing-god). * Added support for Accept header-based versioning - [@jch](https://github.com/jch), [@rodzyn](https://github.com/rodzyn). * Added support for mounting APIs and other Rack applications within APIs - [@mbleigh](https://github.com/mbleigh). * Added entities, multiple object representations - [@mbleigh](https://github.com/mbleigh). * Added ability to handle XML in the incoming request body - [@jwillis](https://github.com/jwillis). * Added support for a configurable logger - [@mbleigh](https://github.com/mbleigh). * Added support for before and after filters - [@mbleigh](https://github.com/mbleigh). * Extended `rescue_from`, which can now take a block - [@dblock](https://github.com/dblock). ### 0.1.5 (2011/6/14) * Extended exception handling to all exceptions - [@dblock](https://github.com/dblock). * Added support for returning JSON objects from within error blocks - [@dblock](https://github.com/dblock). * Added support for handling incoming JSON in body - [@tedkulp](https://github.com/tedkulp). * Added support for HTTP digest authentication - [@daddz](https://github.com/daddz). ### 0.1.4 (2011/4/8) * Allow multiple definitions of the same endpoint under multiple versions - [@chrisrhoden](https://github.com/chrisrhoden). * Added support for multipart URL parameters - [@mcastilho](https://github.com/mcastilho). * Added support for custom formatters - [@spraints](https://github.com/spraints). ### 0.1.3 (2011/1/10) * Added support for JSON format in route matching - [@aiwilliams](https://github.com/aiwilliams). * Added suport for custom middleware - [@mbleigh](https://github.com/mbleigh). ### 0.1.1 (2010/11/14) * Endpoints properly reset between each request - [@mbleigh](https://github.com/mbleigh). ### 0.1.0 (2010/11/13) * Initial public release - [@mbleigh](https://github.com/mbleigh). ================================================ FILE: CONTRIBUTING.md ================================================ Contributing to Grape ===================== Grape is work of [hundreds of contributors](https://github.com/ruby-grape/grape/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/ruby-grape/grape/pulls), [propose features and discuss issues](https://github.com/ruby-grape/grape/issues). #### Fork the Project Fork the [project on Github](https://github.com/ruby-grape/grape) and check out your copy. ``` git clone https://github.com/contributor/grape.git cd grape git remote add upstream https://github.com/ruby-grape/grape.git ``` #### Create a Topic Branch Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. ``` git checkout master git pull upstream master git checkout -b my-feature-branch ``` ### Docker If you're familiar with [Docker](https://www.docker.com/), you can run everything through the following command: ``` docker-compose run --rm --build grape ``` About the execution process: - displays Ruby, Rubygems, Bundle and Gemfile version when starting: ``` ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux-musl] rubygems 3.4.12 Bundler version 2.4.1 (2022-12-24 commit f3175f033c) Running default Gemfile ``` - keeps the gems to the latest possible version - executes under `bundle exec` Here are some examples: - running all specs `docker-compose run --rm --build grape rspec` - running rspec on a specific file `docker-compose run --rm --build grape rspec spec/:file_path` - running task `docker-compose run --rm --build grape rake ` - running rubocop `docker-compose run --rm --build grape rubocop` - running all specs on a specific ruby version (e.g 3.4) `RUBY_VERSION=3.4 docker-compose run --rm --build grape rspec` - running specs on a specific gemfile (e.g rails_8_1.gemfile) `docker-compose run -e GEMFILE=rails_8_1 --rm --build grape rspec` #### Bundle Install and Test Ensure that you can build the project and run tests. ``` bundle install bundle exec rake ``` #### Write Tests Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/grape](spec/grape). We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. #### Write Code Implement your feature or bug fix. Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. Make sure that `bundle exec rake` completes without errors. #### Write Documentation Document any external behavior in the [README](README.md). You should also document code as necessary, using current code as examples. This project uses [YARD](https://yardoc.org/). You can run and preview the docs locally by [installing `yard`](https://yardoc.org/), running `yard server --reload` and view the docs at http://localhost:8808. #### Update Changelog Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. #### Commit Changes Make sure git knows your name and email address: ``` git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` Writing good commit logs is important. A commit log should describe what changed and why. ``` git add ... git commit ``` #### Push ``` git push origin my-feature-branch ``` #### Make a Pull Request Go to https://github.com/contributor/grape and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. #### Rebase If you've been working on a change for a while, rebase with upstream/master. ``` git fetch upstream git rebase upstream/master git push origin my-feature-branch -f ``` #### Update CHANGELOG Again Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. ``` * [#123](https://github.com/ruby-grape/grape/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). ``` Amend your previous commit and force push the changes. ``` git commit --amend git push origin my-feature-branch -f ``` #### Check on Your Pull Request Go back to your pull request after a few minutes and see whether it passed muster with CI. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang in there! #### Thank You Please do know that we really appreciate and value your time and work. We love you, really. ================================================ FILE: Dangerfile ================================================ # frozen_string_literal: true danger.import_dangerfile(gem: 'danger-pr-comment') changelog.check! ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source('https://rubygems.org') gemspec group :development, :test do gem 'builder', require: false gem 'bundler' gem 'rake' gem 'rubocop', '1.84.0', require: false gem 'rubocop-performance', '1.26.1', require: false gem 'rubocop-rspec', '3.9.0', require: false end group :development do gem 'benchmark-ips' gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' gem 'irb' end group :test do gem 'danger', require: false gem 'danger-changelog', require: false gem 'danger-pr-comment', require: false gem 'rack-contrib', require: false gem 'rack-test', '~> 2.1' gem 'rspec', '~> 3.13' gem 'simplecov', '~> 0.21', require: false gem 'simplecov-lcov', '~> 0.8', require: false end platforms :jruby do gem 'racc' end ================================================ FILE: Guardfile ================================================ # frozen_string_literal: true guard :rspec, all_on_start: true, cmd: 'bundle exec rspec' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { 'spec' } end guard :rubocop do watch(/.+\.rb$/) watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } end ================================================ FILE: LICENSE ================================================ Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![grape logo](grape.png) [![Gem Version](https://badge.fury.io/rb/grape.svg)](http://badge.fury.io/rb/grape) [![test](https://github.com/ruby-grape/grape/actions/workflows/test.yml/badge.svg)](https://github.com/ruby-grape/grape/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape?branch=master) ## What is Grape? Grape is a REST-like API framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more. ## Stable Release You're reading the documentation for the next release of Grape, which should be 3.2.0. The current stable release is [3.1.1](https://github.com/ruby-grape/grape/blob/v3.1.1/README.md). ## Project Resources * [Grape Website](http://www.ruby-grape.org) * [Documentation](http://www.rubydoc.info/gems/grape) * Need help? [Open an Issue](https://github.com/ruby-grape/grape/issues) * [Follow us on Twitter](https://twitter.com/grapeframework) ## Grape for Enterprise Available as part of the Tidelift Subscription. The maintainers of Grape are working with Tidelift to deliver commercial support and maintenance. Save time, reduce risk, and improve code health, while paying the maintainers of Grape. Click [here](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) for more details. ## Installation Ruby 3.2 or newer is required. Grape is available as a gem, to install it run: bundle add grape ## Basic Usage Grape APIs are Rack applications that are created by subclassing `Grape::API`. Below is a simple example showing some of the more common features of Grape in the context of recreating parts of the Twitter API. ```ruby module Twitter class API < Grape::API version 'v1', using: :header, vendor: 'twitter' format :json prefix :api helpers do def current_user @current_user ||= User.authorize!(env) end def authenticate! error!('401 Unauthorized', 401) unless current_user end end resource :statuses do desc 'Return a public timeline.' get :public_timeline do Status.limit(20) end desc 'Return a personal timeline.' get :home_timeline do authenticate! current_user.statuses.limit(20) end desc 'Return a status.' params do requires :id, type: Integer, desc: 'Status ID.' end route_param :id do get do Status.find(params[:id]) end end desc 'Create a status.' params do requires :status, type: String, desc: 'Your status.' end post do authenticate! Status.create!({ user: current_user, text: params[:status] }) end desc 'Update a status.' params do requires :id, type: String, desc: 'Status ID.' requires :status, type: String, desc: 'Your status.' end put ':id' do authenticate! current_user.statuses.find(params[:id]).update({ user: current_user, text: params[:status] }) end desc 'Delete a status.' params do requires :id, type: String, desc: 'Status ID.' end delete ':id' do authenticate! current_user.statuses.find(params[:id]).destroy end end end end ``` ## Rails 7.1 Grape's [deprecator](https://api.rubyonrails.org/v7.1.0/classes/ActiveSupport/Deprecation.html) will be added to your application's deprecators [automatically](lib/grape/railtie.rb) as `:grape`, so that your application's configuration can be applied to it. ## Mounting ### All By default Grape will compile the routes on the first route, but it is possible to pre-load routes using the `compile!` method. ```ruby Twitter::API.compile! ``` This can be added to your `config.ru` (if using rackup), `application.rb` (if using rails), or any file that loads your server. ### Rack The above sample creates a Rack application that can be run from a rackup `config.ru` file with `rackup`: ```ruby run Twitter::API ``` (With pre-loading you can use) ```ruby Twitter::API.compile! run Twitter::API ``` And would respond to the following routes: GET /api/statuses/public_timeline GET /api/statuses/home_timeline GET /api/statuses/:id POST /api/statuses PUT /api/statuses/:id DELETE /api/statuses/:id Grape will also automatically respond to HEAD and OPTIONS for all GET, and just OPTIONS for all other routes. ### Alongside Sinatra (or other frameworks) If you wish to mount Grape alongside another Rack framework such as Sinatra, you can do so easily using `Rack::Cascade`: ```ruby # Example config.ru require 'sinatra' require 'grape' class API < Grape::API get :hello do { hello: 'world' } end end class Web < Sinatra::Base get '/' do 'Hello world.' end end use Rack::Session::Cookie run Rack::Cascade.new [Web, API] ``` Note that order of loading apps using `Rack::Cascade` matters. The grape application must be last if you want to raise custom 404 errors from grape (such as `error!('Not Found',404)`). If the grape application is not last and returns 404 or 405 response, [cascade utilizes that as a signal to try the next app](https://www.rubydoc.info/gems/rack/Rack/Cascade). This may lead to undesirable behavior showing the [wrong 404 page from the wrong app](https://github.com/ruby-grape/grape/issues/1515). ### Rails Place API files into `app/api`. Rails expects a subdirectory that matches the name of the Ruby module and a file name that matches the name of the class. In our example, the file name location and directory for `Twitter::API` should be `app/api/twitter/api.rb`. Modify `config/routes`: ```ruby mount Twitter::API => '/' ``` #### Zeitwerk Rails's default autoloader is `Zeitwerk`. By default, it inflects `api` as `Api` instead of `API`. To make our example work, you need to uncomment the lines at the bottom of `config/initializers/inflections.rb`, and add `API` as an acronym: ```ruby ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'API' end ``` ### Modules You can mount multiple API implementations inside another one. These don't have to be different versions, but may be components of the same API. ```ruby class Twitter::API < Grape::API mount Twitter::APIv1 mount Twitter::APIv2 end ``` You can also mount on a path, which is similar to using `prefix` inside the mounted API itself. ```ruby class Twitter::API < Grape::API mount Twitter::APIv1 => '/v1' end ``` Declarations as `before/after/rescue_from` can be placed before or after `mount`. In any case they will be inherited. ```ruby class Twitter::API < Grape::API before do header 'X-Base-Header', 'will be defined for all APIs that are mounted below' end rescue_from :all do error!({ "error" => "Internal Server Error" }, 500) end mount Twitter::Users mount Twitter::Search after do clean_cache! end rescue_from ZeroDivisionError do error!({ "error" => "Not found" }, 404) end end ``` ## Remounting You can mount the same endpoints in two different locations. ```ruby class Voting::API < Grape::API namespace 'votes' do get do # Your logic end post do # Your logic end end end class Post::API < Grape::API mount Voting::API end class Comment::API < Grape::API mount Voting::API end ``` Assuming that the post and comment endpoints are mounted in `/posts` and `/comments`, you should now be able to do `get /posts/votes`, `post /posts/votes`, `get /comments/votes` and `post /comments/votes`. ### Mount Configuration You can configure remountable endpoints to change how they behave according to where they are mounted. ```ruby class Voting::API < Grape::API namespace 'votes' do desc "Vote for your #{configuration[:votable]}" get do # Your logic end end end class Post::API < Grape::API mount Voting::API, with: { votable: 'posts' } end class Comment::API < Grape::API mount Voting::API, with: { votable: 'comments' } end ``` Note that if you're passing a hash as the first parameter to `mount`, you will need to explicitly put `()` around parameters: ```ruby # good mount({ ::Some::Api => '/some/api' }, with: { condition: true }) # bad mount ::Some::Api => '/some/api', with: { condition: true } ``` You can access `configuration` on the class (to use as dynamic attributes), inside blocks (like namespace) If you want logic happening given on an `configuration`, you can use the helper `given`. ```ruby class ConditionalEndpoint::API < Grape::API given configuration[:some_setting] do get 'mount_this_endpoint_conditionally' do configuration[:configurable_response] end end end ``` If you want a block of logic running every time an endpoint is mounted (within which you can access the `configuration` Hash) ```ruby class ConditionalEndpoint::API < Grape::API mounted do YourLogger.info "This API was mounted at: #{Time.now}" get configuration[:endpoint_name] do configuration[:configurable_response] end end end ``` More complex results can be achieved by using `mounted` as an expression within which the `configuration` is already evaluated as a Hash. ```ruby class ExpressionEndpointAPI < Grape::API get(mounted { configuration[:route_name] || 'default_name' }) do # some logic end end ``` ```ruby class BasicAPI < Grape::API desc 'Statuses index' do params: (configuration[:entity] || API::Entities::Status).documentation end params do requires :all, using: (configuration[:entity] || API::Entities::Status).documentation end get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default present statuses, with: (configuration[:entity] || API::Entities::Status), type: type end end class V1 < Grape::API version 'v1' mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Entities::Status } } end class V2 < Grape::API version 'v2' mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Entities::V2::Status } } end ``` ## Versioning You have the option to provide various versions of your API by establishing a separate `Grape::API` class for each offered version and then integrating them into a primary `Grape::API` class. Ensure that newer versions are mounted before older ones. The default approach to versioning directs the request to the subsequent Rack middleware if a specific version is not found. ```ruby require 'v1' require 'v2' require 'v3' class App < Grape::API mount V3 mount V2 mount V1 end ``` To maintain the same endpoints from earlier API versions without rewriting them, you can indicate multiple versions within the previous API versions. ```ruby class V1 < Grape::API version 'v1', 'v2', 'v3' get '/foo' do # your code for GET /foo end get '/other' do # your code for GET /other end end class V2 < Grape::API version 'v2', 'v3' get '/var' do # your code for GET /var end end class V3 < Grape::API version 'v3' get '/foo' do # your new code for GET /foo end end ``` Using the example provided, the subsequent endpoints will be accessible across various versions: ```shell GET /v1/foo GET /v1/other GET /v2/foo # => Same behavior as v1 GET /v2/other # => Same behavior as v1 GET /v2/var # => New endpoint not available in v1 GET /v3/foo # => Different behavior to v1 and v2 GET /v3/other # => Same behavior as v1 and v2 GET /v3/var # => Same behavior as v2 ``` There are four strategies in which clients can reach your API's endpoints: `:path`, `:header`, `:accept_version_header` and `:param`. The default strategy is `:path`. ### Strategies #### Path ```ruby version 'v1', using: :path ``` Using this versioning strategy, clients should pass the desired version in the URL. curl http://localhost:9292/v1/statuses/public_timeline #### Header ```ruby version 'v1', using: :header, vendor: 'twitter' ``` Currently, Grape only supports versioned media types in the following format: ``` vnd.vendor-and-or-resource-v1234+format ``` Basically all tokens between the final `-` and the `+` will be interpreted as the version. Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` head. curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline By default, the first matching version is used when no `Accept` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied. When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. Grape will evaluate the relative quality preference included in Accept headers and default to a quality of 1.0 when omitted. In the following example a Grape API that supports XML and JSON in that order will return JSON: curl -H "Accept: text/xml;q=0.8, application/json;q=0.9" localhost:1234/resource #### Accept-Version Header ```ruby version 'v1', using: :accept_version_header ``` Using this versioning strategy, clients should pass the desired version in the HTTP `Accept-Version` header. curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline By default, the first matching version is used when no `Accept-Version` header is supplied. This behavior is similar to routing in Rails. To circumvent this default behavior, one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error is returned when no correct `Accept` header is supplied and the `:cascade` option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route matches. #### Param ```ruby version 'v1', using: :param ``` Using this versioning strategy, clients should pass the desired version as a request parameter, either in the URL query string or in the request body. curl http://localhost:9292/statuses/public_timeline?apiver=v1 The default name for the query parameter is 'apiver' but can be specified using the `:parameter` option. ```ruby version 'v1', using: :param, parameter: 'v' ``` curl http://localhost:9292/statuses/public_timeline?v=v1 ## Linting You can check whether your API is in conformance with the [Rack's specification](https://github.com/rack/rack/blob/main/SPEC.rdoc) by calling `lint!` at the API level or through [configuration](#configuration). ```ruby class Api < Grape::API lint! end ``` ```ruby Grape.configure do |config| config.lint = true end ``` ```ruby Grape.config.lint = true ``` ### Bug in Rack::ETag under Rack 3.X If you're using Rack 3.X and the `Rack::Etag` middleware (used by [Rails](https://guides.rubyonrails.org/rails_on_rack.html#inspecting-middleware-stack)), a [bug](https://github.com/rack/rack/pull/2324) related to linting has been fixed in [3.1.13](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3113---2025-04-13) and [3.0.15](https://github.com/rack/rack/blob/v3.1.13/CHANGELOG.md#3015---2025-04-13) respectively. ## Describing Methods You can add a description to API methods and namespaces. The description would be used by [grape-swagger][grape-swagger] to generate swagger compliant documentation. Note: Description block is only for documentation and won't affects API behavior. ```ruby desc 'Returns your public timeline.' do summary 'summary' detail 'more details' params API::Entities::Status.documentation success API::Entities::Entity failure [[401, 'Unauthorized', 'Entities::Error']] default { code: 500, message: 'InvalidRequest', model: Entities::Error } named 'My named route' headers XAuthToken: { description: 'Validates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } hidden false deprecated false is_array true nickname 'nickname' produces ['application/json'] consumes ['application/json'] tags ['tag1', 'tag2'] end get :public_timeline do Status.limit(20) end ``` * `detail`: A more enhanced description * `params`: Define parameters directly from an `Entity` * `success`: (former entity) The `Entity` to be used to present the success response for this route. * `failure`: (former http_codes) A definition of the used failure HTTP Codes and Entities. * `default`: The definition and `Entity` used to present the default response for this route. * `named`: A helper to give a route a name and find it with this name in the documentation Hash * `headers`: A definition of the used Headers * Other options can be found in [grape-swagger][grape-swagger] [grape-swagger]: https://github.com/ruby-grape/grape-swagger ## Configuration Use `Grape.configure` to set up global settings at load time. Currently the configurable settings are: * `param_builder`: Sets the [Parameter Builder](#parameters), defaults to `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder`. To change a setting value make sure that at some point during load time the following code runs ```ruby Grape.configure do |config| config.setting = value end ``` For example, for the `param_builder`, the following code could run in an initializer: ```ruby Grape.configure do |config| config.param_builder = :hashie_mash end ``` Available parameter builders are `:hash`, `:hash_with_indifferent_access`, and `:hashie_mash`. See [params_builder](lib/grape/params_builder). You can also configure a single API: ```ruby API.configure do |config| config[key] = value end ``` This will be available inside the API with `configuration`, as if it were [mount configuration](#mount-configuration). ## Parameters Request parameters are available through the `params` hash object. This includes `GET`, `POST` and `PUT` parameters, along with any named parameters you specify in your route strings. ```ruby get :public_timeline do Status.order(params[:sort_by]) end ``` Parameters are automatically populated from the request body on `POST` and `PUT` for form input, JSON and XML content-types. The request: ``` curl -d '{"text": "140 characters"}' 'http://localhost:9292/statuses' -H Content-Type:application/json -v ``` The Grape endpoint: ```ruby post '/statuses' do Status.create!(text: params[:text]) end ``` Multipart POSTs and PUTs are supported as well. The request: ``` curl --form image_file='@image.jpg;type=image/jpg' http://localhost:9292/upload ``` The Grape endpoint: ```ruby post 'upload' do # file in params[:image_file] end ``` In the case of conflict between either of: * route string parameters * `GET`, `POST` and `PUT` parameters * the contents of the request body on `POST` and `PUT` Route string parameters will have precedence. ### Params Class By default parameters are available as `ActiveSupport::HashWithIndifferentAccess`. This can be changed to, for example, Ruby `Hash` or `Hashie::Mash` for the entire API. ```ruby class API < Grape::API build_with :hashie_mash params do optional :color, type: String end get do params.color # instead of params[:color] end ``` The class can also be overridden on individual parameter blocks using `build_with` as follows. ```ruby params do build_with :hash optional :color, type: String end ``` In the example above, `params["color"]` will return `nil` since `params` is a plain `Hash`. Available parameter builders are `:hash`, `:hash_with_indifferent_access`, and `:hashie_mash`. See [params_builder](lib/grape/params_builder). ### Declared Grape allows you to access only the parameters that have been declared by your `params` block. It will: * Filter out the params that have been passed, but are not allowed. * Include any optional params that are declared but not passed. * Perform any parameter renaming on the resulting hash. Consider the following API endpoint: ````ruby format :json post 'users/signup' do { 'declared_params' => declared(params) } end ```` If you do not specify any parameters, `declared` will return an empty hash. **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": "last name"}}' ```` **Response** ````json { "declared_params": {} } ```` Once we add parameters requirements, grape will start returning only the declared parameters. ````ruby format :json params do optional :user, type: Hash do optional :first_name, type: String optional :last_name, type: String end end post 'users/signup' do { 'declared_params' => declared(params) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": "last name", "random": "never shown"}}' ```` **Response** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": "last name" } } } ```` Missing params that are declared as type `Hash` or `Array` will be included. ````ruby format :json params do optional :user, type: Hash do optional :first_name, type: String optional :last_name, type: String end optional :widgets, type: Array end post 'users/signup' do { 'declared_params' => declared(params) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{}' ```` **Response** ````json { "declared_params": { "user": { "first_name": null, "last_name": null }, "widgets": [] } } ```` The returned hash is an `ActiveSupport::HashWithIndifferentAccess`. The `#declared` method is not available to `before` filters, as those are evaluated prior to parameter coercion. ### Include Parent Namespaces By default `declared(params)` includes parameters that were defined in all parent namespaces. If you want to return only parameters from your current namespace, you can set `include_parent_namespaces` option to `false`. ````ruby format :json namespace :parent do params do requires :parent_name, type: String end namespace ':parent_name' do params do requires :child_name, type: String end get ':child_name' do { 'without_parent_namespaces' => declared(params, include_parent_namespaces: false), 'with_parent_namespaces' => declared(params, include_parent_namespaces: true), } end end end ```` **Request** ````bash curl -X GET -H "Content-Type: application/json" localhost:9292/parent/foo/bar ```` **Response** ````json { "without_parent_namespaces": { "child_name": "bar" }, "with_parent_namespaces": { "parent_name": "foo", "child_name": "bar" }, } ```` ### Include Missing By default `declared(params)` includes parameters that have `nil` values. If you want to return only the parameters that are not `nil`, you can use the `include_missing` option. By default, `include_missing` is set to `true`. Consider the following API: ````ruby format :json params do requires :user, type: Hash do requires :first_name, type: String optional :last_name, type: String end end post 'users/signup' do { 'declared_params' => declared(params, include_missing: false) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "random": "never shown"}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name" } } } ```` **Response with include_missing:true** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": null } } } ```` It also works on nested hashes: ````ruby format :json params do requires :user, type: Hash do requires :first_name, type: String optional :last_name, type: String requires :address, type: Hash do requires :city, type: String optional :region, type: String end end end post 'users/signup' do { 'declared_params' => declared(params, include_missing: false) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "random": "never shown", "address": { "city": "SF"}}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name", "address": { "city": "SF" } } } } ```` **Response with include_missing:true** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": null, "address": { "city": "Zurich", "region": null } } } } ```` Note that an attribute with a `nil` value is not considered *missing* and will also be returned when `include_missing` is set to `false`: **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{"user": {"first_name":"first name", "last_name": null, "address": { "city": "SF"}}}' ```` **Response with include_missing:false** ````json { "declared_params": { "user": { "first_name": "first name", "last_name": null, "address": { "city": "SF"} } } } ```` ### Evaluate Given By default `declared(params)` will not evaluate `given` and return all parameters. Use `evaluate_given` to evaluate all `given` blocks and return only parameters that satisfy `given` conditions. Consider the following API: ````ruby format :json params do optional :child_id, type: Integer given :child_id do requires :father_id, type: Integer end end post 'child' do { 'declared_params' => declared(params, evaluate_given: true) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"father_id": 1}' ```` **Response with evaluate_given:false** ````json { "declared_params": { "child_id": null, "father_id": 1 } } ```` **Response with evaluate_given:true** ````json { "declared_params": { "child_id": null } } ```` It also works on nested hashes: ````ruby format :json params do requires :child, type: Hash do optional :child_id, type: Integer given :child_id do requires :father_id, type: Integer end end end post 'child' do { 'declared_params' => declared(params, evaluate_given: true) } end ```` **Request** ````bash curl -X POST -H "Content-Type: application/json" localhost:9292/child -d '{"child": {"father_id": 1}}' ```` **Response with evaluate_given:false** ````json { "declared_params": { "child": { "child_id": null, "father_id": 1 } } } ```` **Response with evaluate_given:true** ````json { "declared_params": { "child": { "child_id": null } } } ```` ### Parameter Precedence Using `route_param` takes higher precedence over a regular parameter defined with same name: ```ruby params do requires :foo, type: String end route_param :foo do get do { value: params[:foo] } end end ``` **Request** ```bash curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}' ``` **Response** ```json { "value": "bar" } ``` ## Parameter Validation and Coercion You can define validations and coercion options for your parameters using a `params` block. ```ruby params do requires :id, type: Integer optional :text, type: String, regexp: /\A[a-z]+\z/ group :media, type: Hash do requires :url end optional :audio, type: Hash do requires :format, type: Symbol, values: [:mp3, :wav, :aac, :ogg], default: :mp3 end mutually_exclusive :media, :audio end put ':id' do # params[:id] is an Integer end ``` When a type is specified an implicit validation is done after the coercion to ensure the output type is the one declared. Optional parameters can have a default value. ```ruby params do optional :color, type: String, default: 'blue' optional :random_number, type: Integer, default: -> { Random.rand(1..100) } optional :non_random_number, type: Integer, default: Random.rand(1..100) end ``` Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same number for each call to the endpoint of this `params` block. To have the default evaluate lazily with each request use a lambda, like `:random_number` above. Note that default values will be passed through to any validation options specified. The following example will always fail if `:color` is not explicitly provided. ```ruby params do optional :color, type: String, default: 'blue', values: ['red', 'green'] end ``` The correct implementation is to ensure the default value passes all validations. ```ruby params do optional :color, type: String, default: 'blue', values: ['blue', 'red', 'green'] end ``` You can use the value of one parameter as the default value of some other parameter. In this case, if the `primary_color` parameter is not provided, it will have the same value as the `color` one. If both of them not provided, both of them will have `blue` value. ```ruby params do optional :color, type: String, default: 'blue' optional :primary_color, type: String, default: -> (params) { params[:color] } end ``` ### Supported Parameter Types The following are all valid types, supported out of the box by Grape: * Integer * Float * BigDecimal * Numeric * Date * DateTime * Time * Boolean * String * Symbol * Rack::Multipart::UploadedFile (alias `File`) * JSON ### Integer/Fixnum and Coercions Please be aware that the behavior differs between Ruby 2.4 and earlier versions. In Ruby 2.4, values consisting of numbers are converted to Integer, but in earlier versions it will be treated as Fixnum. ```ruby params do requires :integers, type: Hash do requires :int, coerce: Integer end end get '/int' do params[:integers][:int].class end ... get '/int' integers: { int: '45' } #=> Integer in ruby 2.4 #=> Fixnum in earlier ruby versions ``` ### Custom Types and Coercions Aside from the default set of supported types listed above, any class can be used as a type as long as an explicit coercion method is supplied. If the type implements a class-level `parse` method, Grape will use it automatically. This method must take one string argument and return an instance of the correct type, or return an instance of `Grape::Types::InvalidValue` which optionally accepts a message to be returned in the response. ```ruby class Color attr_reader :value def initialize(color) @value = color end def self.parse(value) return new(value) if %w[blue red green].include?(value) Grape::Types::InvalidValue.new('Unsupported color') end end params do requires :color, type: Color, default: Color.new('blue') requires :more_colors, type: Array[Color] # Collections work optional :unique_colors, type: Set[Color] # Duplicates discarded end get '/stuff' do # params[:color] is already a Color. params[:color].value end ``` Alternatively, a custom coercion method may be supplied for any type of parameter using `coerce_with`. Any class or object may be given that implements a `parse` or `call` method, in that order of precedence. The method must accept a single string parameter, and the return value must match the given `type`. ```ruby params do requires :passwd, type: String, coerce_with: Base64.method(:decode64) requires :loud_color, type: Color, coerce_with: ->(c) { Color.parse(c.downcase) } requires :obj, type: Hash, coerce_with: JSON do requires :words, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) } optional :time, type: Time, coerce_with: Chronic end end ``` Note that, a `nil` value will call the custom coercion method, while a missing parameter will not. Example of use of `coerce_with` with a lambda (a class with a `parse` method could also have been used) It will parse a string and return an Array of Integers, matching the `Array[Integer]` `type`. ```ruby params do requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end ``` Grape will assert that coerced values match the given `type`, and will reject the request if they do not. To override this behaviour, custom types may implement a `parsed?` method that should accept a single argument and return `true` if the value passes type validation. ```ruby class SecureUri def self.parse(value) URI.parse value end def self.parsed?(value) value.is_a? URI::HTTPS end end params do requires :secure_uri, type: SecureUri end ``` ### Multipart File Parameters Grape makes use of `Rack::Request`'s built-in support for multipart file parameters. Such parameters can be declared with `type: File`: ```ruby params do requires :avatar, type: File end post '/' do params[:avatar][:filename] # => 'avatar.png' params[:avatar][:type] # => 'image/png' params[:avatar][:tempfile] # => # end ``` ### First-Class `JSON` Types Grape supports complex parameters given as JSON-formatted strings using the special `type: JSON` declaration. JSON objects and arrays of objects are accepted equally, with nested validation rules applied to all objects in either case: ```ruby params do requires :json, type: JSON do requires :int, type: Integer, values: [1, 2, 3] end end get '/' do params[:json].inspect end client.get('/', json: '{"int":1}') # => "{:int=>1}" client.get('/', json: '[{"int":"1"}]') # => "[{:int=>1}]" client.get('/', json: '{"int":4}') # => HTTP 400 client.get('/', json: '[{"int":4}]') # => HTTP 400 ``` Additionally `type: Array[JSON]` may be used, which explicitly marks the parameter as an array of objects. If a single object is supplied it will be wrapped. ```ruby params do requires :json, type: Array[JSON] do requires :int, type: Integer end end get '/' do params[:json].each { |obj| ... } # always works end ``` For stricter control over the type of JSON structure which may be supplied, use `type: Array, coerce_with: JSON` or `type: Hash, coerce_with: JSON`. ### Multiple Allowed Types Variant-type parameters can be declared using the `types` option rather than `type`: ```ruby params do requires :status_code, types: [Integer, String, Array[Integer, String]] end get '/' do params[:status_code].inspect end client.get('/', status_code: 'OK_GOOD') # => "OK_GOOD" client.get('/', status_code: 300) # => 300 client.get('/', status_code: %w(404 NOT FOUND)) # => [404, "NOT", "FOUND"] ``` As a special case, variant-member-type collections may also be declared, by passing a `Set` or `Array` with more than one member to `type`: ```ruby params do requires :status_codes, type: Array[Integer,String] end get '/' do params[:status_codes].inspect end client.get('/', status_codes: %w(1 two)) # => [1, "two"] ``` ### Validation of Nested Parameters Parameters can be nested using `group` or by calling `requires` or `optional` with a block. In the [above example](#parameter-validation-and-coercion), this means `params[:media][:url]` is required along with `params[:id]`, and `params[:audio][:format]` is required only if `params[:audio]` is present. With a block, `group`, `requires` and `optional` accept an additional option `type` which can be either `Array` or `Hash`, and defaults to `Array`. Depending on the value, the nested parameters will be treated either as values of a hash or as values of hashes in an array. ```ruby params do optional :preferences, type: Array do requires :key requires :value end requires :name, type: Hash do requires :first_name requires :last_name end end ``` ### Dependent Parameters Suppose some of your parameters are only relevant if another parameter is given; Grape allows you to express this relationship through the `given` method in your parameters block, like so: ```ruby params do optional :shelf_id, type: Integer given :shelf_id do requires :bin_id, type: Integer end end ``` In the example above Grape will use `blank?` to check whether the `shelf_id` param is present. `given` also takes a `Proc` with custom code. Below, the param `description` is required only if the value of `category` is equal `foo`: ```ruby params do optional :category given category: ->(val) { val == 'foo' } do requires :description end end ``` You can rename parameters: ```ruby params do optional :category, as: :type given type: ->(val) { val == 'foo' } do requires :description end end ``` Note: param in `given` should be the renamed one. In the example, it should be `type`, not `category`. ### Group Options Parameters options can be grouped. It can be useful if you want to extract common validation or types for several parameters. Within these groups, individual parameters can extend or selectively override the common settings, allowing you to maintain the defaults at the group level while still applying parameter-specific rules where necessary. The example below presents a typical case when parameters share common options. ```ruby params do requires :first_name, type: String, regexp: /w+/, desc: 'First name', documentation: { in: 'body' } optional :middle_name, type: String, regexp: /w+/, desc: 'Middle name', documentation: { in: 'body', x: { nullable: true } } requires :last_name, type: String, regexp: /w+/, desc: 'Last name', documentation: { in: 'body' } end ``` Grape allows you to present the same logic through the `with` method in your parameters block, like so: ```ruby params do with(type: String, regexp: /w+/, documentation: { in: 'body' }) do requires :first_name, desc: 'First name' optional :middle_name, desc: 'Middle name', documentation: { x: { nullable: true } } requires :last_name, desc: 'Last name' end end ``` You can organize settings into layers using nested `with' blocks. Each layer can use, add to, or change the settings of the layer above it. This helps to keep complex parameters organized and consistent, while still allowing for specific customizations to be made. ```ruby params do with(documentation: { in: 'body' }) do # Applies documentation to all nested parameters with(type: String, regexp: /\w+/) do # Applies type and validation to names requires :first_name, desc: 'First name' requires :last_name, desc: 'Last name' end optional :age, type: Integer, desc: 'Age', documentation: { x: { nullable: true } } # Specific settings for 'age' end end ``` ### Renaming You can rename parameters using `as`, which can be useful when refactoring existing APIs: ```ruby resource :users do params do requires :email_address, as: :email requires :password end post do User.create!(declared(params)) # User takes email and password end end ``` The value passed to `as` will be the key when calling `declared(params)`. ### Built-in Validators #### `allow_blank` Parameters can be defined as `allow_blank`, ensuring that they contain a value. By default, `requires` only validates that a parameter was sent in the request, regardless its value. With `allow_blank: false`, empty values or whitespace only values are invalid. `allow_blank` can be combined with both `requires` and `optional`. If the parameter is required, it has to contain a value. If it's optional, it's possible to not send it in the request, but if it's being sent, it has to have some value, and not an empty string/only whitespaces. ```ruby params do requires :username, allow_blank: false optional :first_name, allow_blank: false end ``` #### `values` Parameters can be restricted to a specific set of values with the `:values` option. ```ruby params do requires :status, type: Symbol, values: [:not_started, :processing, :done] optional :numbers, type: Array[Integer], default: 1, values: [1, 2, 3, 5, 8] end ``` Supplying a range to the `:values` option ensures that the parameter is (or parameters are) included in that range (using `Range#include?`). ```ruby params do requires :latitude, type: Float, values: -90.0..+90.0 requires :longitude, type: Float, values: -180.0..+180.0 optional :letters, type: Array[String], values: 'a'..'z' end ``` Note endless ranges are also supported with ActiveSupport >= 6.0, but they require that the type be provided. ```ruby params do requires :minimum, type: Integer, values: 10.. optional :maximum, type: Integer, values: ..10 end ``` Note that *both* range endpoints have to be a `#kind_of?` your `:type` option (if you don't supply the `:type` option, it will be guessed to be equal to the class of the range's first endpoint). So the following is invalid: ```ruby params do requires :invalid1, type: Float, values: 0..10 # 0.kind_of?(Float) => false optional :invalid2, values: 0..10.0 # 10.0.kind_of?(0.class) => false end ``` The `:values` option can also be supplied with a `Proc`, evaluated lazily with each request. If the Proc has arity zero (i.e. it takes no arguments) it is expected to return either a list or a range which will then be used to validate the parameter. For example, given a status model you may want to restrict by hashtags that you have previously defined in the `HashTag` model. ```ruby params do requires :hashtag, type: String, values: -> { Hashtag.all.map(&:tag) } end ``` Alternatively, a Proc with arity one (i.e. taking one argument) can be used to explicitly validate each parameter value. In that case, the Proc is expected to return a truthy value if the parameter value is valid. The parameter will be considered invalid if the Proc returns a falsy value or if it raises a StandardError. ```ruby params do requires :number, type: Integer, values: ->(v) { v.even? && v < 25 } end ``` While Procs are convenient for single cases, consider using [Custom Validators](#custom-validators) in cases where a validation is used more than once. Note that [allow_blank](#allow_blank) validator applies while using `:values`. In the following example the absence of `:allow_blank` does not prevent `:state` from receiving blank values because `:allow_blank` defaults to `true`. ```ruby params do requires :state, type: Symbol, values: [:active, :inactive] end ``` #### `except_values` Parameters can be restricted from having a specific set of values with the `:except_values` option. The `except_values` validator behaves similarly to the `values` validator in that it accepts either an Array, a Range, or a Proc. Unlike the `values` validator, however, `except_values` only accepts Procs with arity zero. ```ruby params do requires :browser, except_values: [ 'ie6', 'ie7', 'ie8' ] requires :port, except_values: { value: 0..1024, message: 'is not allowed' } requires :hashtag, except_values: -> { Hashtag.FORBIDDEN_LIST } end ``` #### `same_as` A `same_as` option can be given to ensure that values of parameters match. ```ruby params do requires :password requires :password_confirmation, same_as: :password end ``` #### `length` Parameters with types that support `#length` method can be restricted to have a specific length with the `:length` option. The validator accepts `:min` or `:max` or both options or only `:is` to validate that the value of the parameter is within the given limits. ```ruby params do requires :code, type: String, length: { is: 2 } requires :str, type: String, length: { min: 3 } requires :list, type: [Integer], length: { min: 3, max: 5 } requires :hash, type: Hash, length: { max: 5 } end ``` #### `regexp` Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value does not match the regular expression an error will be returned. Note that this is true for both `requires` and `optional` parameters. ```ruby params do requires :email, regexp: /.+@.+/ end ``` The validator will pass if the parameter was sent without value. To ensure that the parameter contains a value, use `allow_blank: false`. ```ruby params do requires :email, allow_blank: false, regexp: /.+@.+/ end ``` #### `mutually_exclusive` Parameters can be defined as `mutually_exclusive`, ensuring that they aren't present at the same time in a request. ```ruby params do optional :beer optional :wine mutually_exclusive :beer, :wine end ``` Multiple sets can be defined: ```ruby params do optional :beer optional :wine mutually_exclusive :beer, :wine optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit end ``` **Warning**: Never define mutually exclusive sets with any required params. Two mutually exclusive required params will mean params are never valid, thus making the endpoint useless. One required param mutually exclusive with an optional param will mean the latter is never valid. #### `exactly_one_of` Parameters can be defined as 'exactly_one_of', ensuring that exactly one parameter gets selected. ```ruby params do optional :beer optional :wine exactly_one_of :beer, :wine end ``` Note that using `:default` with `mutually_exclusive` will cause multiple parameters to always have a default value and raise a `Grape::Exceptions::Validation` mutually exclusive exception. #### `at_least_one_of` Parameters can be defined as 'at_least_one_of', ensuring that at least one parameter gets selected. ```ruby params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end ``` #### `all_or_none_of` Parameters can be defined as 'all_or_none_of', ensuring that all or none of parameters gets selected. ```ruby params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice end ``` #### Nested `mutually_exclusive`, `exactly_one_of`, `at_least_one_of`, `all_or_none_of` All of these methods can be used at any nested level. ```ruby params do requires :food, type: Hash do optional :meat optional :fish optional :rice at_least_one_of :meat, :fish, :rice end group :drink, type: Hash do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end optional :dessert, type: Hash do optional :cake optional :icecream mutually_exclusive :cake, :icecream end optional :recipe, type: Hash do optional :oil optional :meat all_or_none_of :oil, :meat end end ``` ### Namespace Validation and Coercion Namespaces allow parameter definitions and apply to every method within the namespace. ```ruby namespace :statuses do params do requires :user_id, type: Integer, desc: 'A user ID.' end namespace ':user_id' do desc "Retrieve a user's status." params do requires :status_id, type: Integer, desc: 'A status ID.' end get ':status_id' do User.find(params[:user_id]).statuses.find(params[:status_id]) end end end ``` The `namespace` method has a number of aliases, including: `group`, `resource`, `resources`, and `segment`. Use whichever reads the best for your API. You can conveniently define a route parameter as a namespace using `route_param`. ```ruby namespace :statuses do route_param :id do desc 'Returns all replies for a status.' get 'replies' do Status.find(params[:id]).replies end desc 'Returns a status.' get do Status.find(params[:id]) end end end ``` You can also define a route parameter type by passing to `route_param`'s options. ```ruby namespace :arithmetic do route_param :n, type: Integer do desc 'Returns in power' get 'power' do params[:n] ** params[:n] end end end ``` ### Custom Validators ```ruby class AlphaNumeric < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name] =~ /\A[[:alnum:]]+\z/ raise Grape::Exceptions::Validation.new params: [@scope.full_name(attr_name)], message: 'must consist of alpha-numeric characters' end end end ``` ```ruby params do requires :text, alpha_numeric: true end ``` You can also create custom classes that take parameters. ```ruby class Length < Grape::Validations::Validators::Base def validate_param!(attr_name, params) unless params[attr_name].length <= @option raise Grape::Exceptions::Validation.new params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long" end end end ``` ```ruby params do requires :text, length: 140 end ``` You can also create custom validation that use request to validate the attribute. For example if you want to have parameters that are available to only admins, you can do the following. ```ruby class Admin < Grape::Validations::Validators::Base def validate(request) # return if the param we are checking was not in request # @attrs is a list containing the attribute we are currently validating # in our sample case this method once will get called with # @attrs being [:admin_field] and once with @attrs being [:admin_false_field] return unless request.params.key?(@attrs.first) # check if admin flag is set to true return unless @option # check if user is admin or not # as an example get a token from request and check if it's admin or not raise Grape::Exceptions::Validation.new params: @attrs, message: 'Can not set admin-only field.' unless request.headers['X-Access-Token'] == 'admin' end end ``` And use it in your endpoint definition as: ```ruby params do optional :admin_field, type: String, admin: true optional :non_admin_field, type: String optional :admin_false_field, type: String, admin: false end ``` Every validation will have its own instance of the validator, which means that the validator can have a state. ### Validation Errors Validation and coercion errors are collected and an exception of type `Grape::Exceptions::ValidationErrors` is raised. If the exception goes uncaught it will respond with a status of 400 and an error message. The validation errors are grouped by parameter name and can be accessed via `Grape::Exceptions::ValidationErrors#errors`. The default response from a `Grape::Exceptions::ValidationErrors` is a humanly readable string, such as "beer, wine are mutually exclusive", in the following example. ```ruby params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end ``` You can rescue a `Grape::Exceptions::ValidationErrors` and respond with a custom response or turn the response into well-formatted JSON for a JSON API that separates individual parameters and the corresponding error messages. The following `rescue_from` example produces `[{"params":["beer","wine"],"messages":["are mutually exclusive"]}]`. ```ruby format :json subject.rescue_from Grape::Exceptions::ValidationErrors do |e| error! e, 400 end ``` `Grape::Exceptions::ValidationErrors#full_messages` returns the validation messages as an array. `Grape::Exceptions::ValidationErrors#message` joins the messages to one string. For responding with an array of validation messages, you can use `Grape::Exceptions::ValidationErrors#full_messages`. ```ruby format :json subject.rescue_from Grape::Exceptions::ValidationErrors do |e| error!({ messages: e.full_messages }, 400) end ``` Grape returns all validation and coercion errors found by default. To skip all subsequent validation checks when a specific param is found invalid, use `fail_fast: true`. The following example will not check if `:wine` is present unless it finds `:beer`. ```ruby params do required :beer, fail_fast: true required :wine end ``` The result of empty params would be a single `Grape::Exceptions::ValidationErrors` error. Similarly, no regular expression test will be performed if `:blah` is blank in the following example. ```ruby params do required :blah, allow_blank: false, regexp: /blah/, fail_fast: true end ``` ### I18n Grape supports I18n for parameter-related error messages, but will fallback to English if translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys. In case your app enforces available locales only and :en is not included in your available locales, Grape cannot fall back to English and will return the translation key for the error message. To avoid this behaviour, either provide a translation for your default locale or add :en to your available locales. Custom validators that inherit from `Grape::Validations::Validators::Base` have access to a `translate` helper (see `Grape::Util::Translation`) and should use it instead of calling `I18n` directly. It applies the same `:en` fallback as built-in validators, defaults `scope` to `'grape.errors.messages'`, and handles interpolation without needing `format`: ```ruby # Good — scope defaults to 'grape.errors.messages', interpolation forwarded automatically translate(:special, min: 2, max: 10) # Bad — format is unnecessary and risks conflicting with I18n reserved keys format I18n.t(:special, scope: 'grape.errors.messages'), min: 2, max: 10 ``` Example custom validator: ```ruby class SpecialValidator < Grape::Validations::Validators::Base def validate_param!(attr_name, params) return if valid?(params[attr_name]) raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: translate(:special, min: 2, max: 10) ) end end ``` ### Custom Validation messages Grape supports custom validation messages for parameter-related and coerce-related error messages. #### `presence`, `allow_blank`, `values`, `regexp` ```ruby params do requires :name, values: { value: 1..10, message: 'not in range from 1 to 10' }, allow_blank: { value: false, message: 'cannot be blank' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' }, message: 'is required' end ``` #### `same_as` ```ruby params do requires :password requires :password_confirmation, same_as: { value: :password, message: 'not match' } end ``` #### `length` ```ruby params do requires :code, type: String, length: { is: 2, message: 'code is expected to be exactly 2 characters long' } requires :str, type: String, length: { min: 5, message: 'str is expected to be at least 5 characters long' } requires :list, type: [Integer], length: { min: 2, max: 3, message: 'list is expected to have between 2 and 3 elements' } end ``` #### `all_or_none_of` ```ruby params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice, message: "all params are required or none is required" end ``` #### `mutually_exclusive` ```ruby params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice, message: "are mutually exclusive cannot pass both params" end ``` #### `exactly_one_of` ```ruby params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice, message: { exactly_one: "are missing, exactly one parameter is required", mutual_exclusion: "are mutually exclusive, exactly one parameter is required" } end ``` #### `at_least_one_of` ```ruby params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice, message: "are missing, please specify at least one param" end ``` #### `Coerce` ```ruby params do requires :int, type: { value: Integer, message: "type cast is invalid" } end ``` #### `With Lambdas` ```ruby params do requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' } end ``` #### `Pass symbols for i18n translations` You can pass a symbol if you want i18n translations for your custom validation messages. ```ruby params do requires :name, message: :name_required end ``` ```ruby # en.yml en: grape: errors: format: ! '%{attributes} %{message}' messages: name_required: 'must be present' ``` #### Overriding Attribute Names You can also override attribute names. ```ruby # en.yml en: grape: errors: format: ! '%{attributes} %{message}' messages: name_required: 'must be present' attributes: name: 'Oops! Name' ``` Will produce 'Oops! Name must be present' #### With Default You cannot set a custom message option for Default as it requires interpolation `%{option1}: %{value1} is incompatible with %{option2}: %{value2}`. You can change the default error message for Default by changing the `incompatible_option_values` message key inside [en.yml](lib/grape/locale/en.yml) ```ruby params do requires :name, values: { value: -> { (1..10).to_a }, message: 'not in range from 1 to 10' }, default: 5 end ``` ### Using `dry-validation` or `dry-schema` As an alternative to the `params` DSL described above, you can use a schema or `dry-validation` contract to describe an endpoint's parameters. This can be especially useful if you use the above already in some other parts of your application. If not, you'll need to add `dry-validation` or `dry-schema` to your `Gemfile`. Then call `contract` with a contract or schema defined previously: ```rb CreateOrdersSchema = Dry::Schema.Params do required(:orders).array(:hash) do required(:name).filled(:string) optional(:volume).maybe(:integer, lt?: 9) end end # ... contract CreateOrdersSchema ``` or with a block, using the [schema definition syntax](https://dry-rb.org/gems/dry-schema/1.13/#quick-start): ```rb contract do required(:orders).array(:hash) do required(:name).filled(:string) optional(:volume).maybe(:integer, lt?: 9) end end ``` The latter will define a coercing schema (`Dry::Schema.Params`). When using the former approach, it's up to you to decide whether the input will need coercing. The `params` and `contract` declarations can also be used together in the same API, e.g. to describe different parts of a nested namespace for an endpoint. ## Headers ### Request Request headers are available through the `headers` helper or from `env` in their original form. ```ruby get do error!('Unauthorized', 401) unless headers['Secret-Password'] == 'swordfish' end ``` ```ruby get do error!('Unauthorized', 401) unless env['HTTP_SECRET_PASSWORD'] == 'swordfish' end ``` #### Header Case Handling The above example may have been requested as follows: ``` shell curl -H "secret_PassWord: swordfish" ... ``` The header name will have been normalized for you. - In the `header` helper names will be coerced into a downcased kebab case as `secret-password` if using Rack 3. - In the `header` helper names will be coerced into a capitalized kebab case as `Secret-PassWord` if using Rack < 3. - In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_' as `HTTP_SECRET_PASSWORD` The header name will have been normalized per HTTP standards defined in [RFC2616 Section 4.2](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) regardless of what is being sent by a client. ### Response You can set a response header with `header` inside an API. ```ruby header 'X-Robots-Tag', 'noindex' ``` When raising `error!`, pass additional headers as arguments. Additional headers will be merged with headers set before `error!` call. ```ruby error! 'Unauthorized', 401, 'X-Error-Detail' => 'Invalid token.' ``` ## Routes To define routes you can use the `route` method or the shorthands for the HTTP verbs. To define a route that accepts any route set to `:any`. Parts of the path that are denoted with a colon will be interpreted as route parameters. ```ruby route :get, 'status' do end # is the same as get 'status' do end # is the same as get :status do end # is NOT the same as get ':status' do # this makes params[:status] available end # This will make both params[:status_id] and params[:id] available get 'statuses/:status_id/reviews/:id' do end ``` To declare a namespace that prefixes all routes within, use the `namespace` method. `group`, `resource`, `resources` and `segment` are aliases to this method. Any endpoints within will share their parent context as well as any configuration done in the namespace context. The `route_param` method is a convenient method for defining a parameter route segment. If you define a type, it will add a validation for this parameter. ```ruby route_param :id, type: Integer do get 'status' do end end # is the same as namespace ':id' do params do requires :id, type: Integer end get 'status' do end end ``` Optionally, you can define requirements for your named route parameters using regular expressions on namespace or endpoint. The route will match only if all requirements are met. ```ruby get ':id', requirements: { id: /[0-9]*/ } do Status.find(params[:id]) end namespace :outer, requirements: { id: /[0-9]*/ } do get :id do end get ':id/edit' do end end ``` ## Helpers You can define helper methods that your endpoints can use with the `helpers` macro by either giving a block or an array of modules. ```ruby module StatusHelpers def user_info(user) "#{user} has statused #{user.statuses} status(s)" end end module HttpCodesHelpers def unauthorized 401 end end class API < Grape::API # define helpers with a block helpers do def current_user User.find(params[:user_id]) end end # or mix in an array of modules helpers StatusHelpers, HttpCodesHelpers before do error!('Access Denied', unauthorized) unless current_user end get 'info' do # helpers available in your endpoint and filters user_info(current_user) end end ``` You can define reusable `params` using `helpers`. ```ruby class API < Grape::API helpers do params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end desc 'Get collection' params do use :pagination # aliases: includes, use_scope end get do Collection.page(params[:page]).per(params[:per_page]) end end ``` You can also define reusable `params` using shared helpers. ```ruby module SharedParams extend Grape::API::Helpers params :period do optional :start_date optional :end_date end params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end class API < Grape::API helpers SharedParams desc 'Get collection.' params do use :period, :pagination end get do Collection .from(params[:start_date]) .to(params[:end_date]) .page(params[:page]) .per(params[:per_page]) end end ``` Helpers support blocks that can help set default values. The following API can return a collection sorted by `id` or `created_at` in `asc` or `desc` order. ```ruby module SharedParams extend Grape::API::Helpers params :order do |options| optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by] optional :order, type: Symbol, values: %i(asc desc), default: options[:default_order] end end class API < Grape::API helpers SharedParams desc 'Get a sorted collection.' params do use :order, order_by: %i(id created_at), default_order_by: :created_at, default_order: :asc end get do Collection.send(params[:order], params[:order_by]) end end ``` ## Path Helpers If you need methods for generating paths inside your endpoints, please see the [grape-route-helpers](https://github.com/reprah/grape-route-helpers) gem. ## Parameter Documentation You can attach additional documentation to `params` using a `documentation` hash. ```ruby params do optional :first_name, type: String, documentation: { example: 'Jim' } requires :last_name, type: String, documentation: { example: 'Smith' } end ``` If documentation isn't needed (for instance, it is an internal API), documentation can be disabled. ```ruby class API < Grape::API do_not_document! # endpoints... end ``` In this case, Grape won't create objects related to documentation which are retained in RAM forever. ## Cookies You can set, get and delete your cookies very simply using `cookies` method. ```ruby class API < Grape::API get 'status_count' do cookies[:status_count] ||= 0 cookies[:status_count] += 1 { status_count: cookies[:status_count] } end delete 'status_count' do { status_count: cookies.delete(:status_count) } end end ``` Use a hash-based syntax to set more than one value. ```ruby cookies[:status_count] = { value: 0, expires: Time.tomorrow, domain: '.twitter.com', path: '/' } cookies[:status_count][:value] +=1 ``` Delete a cookie with `delete`. ```ruby cookies.delete :status_count ``` Specify an optional path. ```ruby cookies.delete :status_count, path: '/' ``` ## HTTP Status Code By default Grape returns a 201 for `POST`-Requests, 204 for `DELETE`-Requests that don't return any content, and 200 status code for all other Requests. You can use `status` to query and set the actual HTTP Status Code ```ruby post do status 202 if status == 200 # do some thing end end ``` You can also use one of status codes symbols that are provided by [Rack utils](http://www.rubydoc.info/github/rack/rack/Rack/Utils#HTTP_STATUS_CODES-constant) ```ruby post do status :no_content end ``` ## Redirecting You can redirect to a new url temporarily (302) or permanently (301). ```ruby redirect '/statuses' ``` ```ruby redirect '/statuses', permanent: true ``` ## Recognizing Path You can recognize the endpoint matched with given path. This API returns an instance of `Grape::Endpoint`. ```ruby class API < Grape::API get '/statuses' do end end API.recognize_path '/statuses' ``` Since version `2.1.0`, the `recognize_path` method takes into account the parameters type to determine which endpoint should match with given path. ```ruby class Books < Grape::API resource :books do route_param :id, type: Integer do # GET /books/:id get do #... end end resource :share do # POST /books/share post do # .... end end end end API.recognize_path '/books/1' # => /books/:id API.recognize_path '/books/share' # => /books/share API.recognize_path '/books/other' # => nil ``` ## Allowed Methods When you add a `GET` route for a resource, a route for the `HEAD` method will also be added automatically. You can disable this behavior with `do_not_route_head!`. ``` ruby class API < Grape::API do_not_route_head! get '/example' do # only responds to GET end end ``` When you add a route for a resource, a route for the `OPTIONS` method will also be added. The response to an OPTIONS request will include an "Allow" header listing the supported methods. If the resource has `before` and `after` callbacks they will be executed, but no other callbacks will run. ```ruby class API < Grape::API get '/rt_count' do { rt_count: current_user.rt_count } end params do requires :value, type: Integer, desc: 'Value to add to the rt count.' end put '/rt_count' do current_user.rt_count += params[:value].to_i { rt_count: current_user.rt_count } end end ``` ``` shell curl -v -X OPTIONS http://localhost:3000/rt_count > OPTIONS /rt_count HTTP/1.1 > < HTTP/1.1 204 No Content < Allow: OPTIONS, GET, PUT ``` You can disable this behavior with `do_not_route_options!`. If a request for a resource is made with an unsupported HTTP method, an HTTP 405 (Method Not Allowed) response will be returned. If the resource has `before` callbacks they will be executed, but no other callbacks will run. ``` shell curl -X DELETE -v http://localhost:3000/rt_count/ > DELETE /rt_count/ HTTP/1.1 > Host: localhost:3000 > < HTTP/1.1 405 Method Not Allowed < Allow: OPTIONS, GET, PUT ``` ## Raising Exceptions You can abort the execution of an API method by raising errors with `error!`. ```ruby error! 'Access Denied', 401 ``` Anything that responds to `#to_s` can be given as a first argument to `error!`. ```ruby error! :not_found, 404 ``` You can also return JSON formatted objects by raising error! and passing a hash instead of a message. ```ruby error!({ error: 'unexpected error', detail: 'missing widget' }, 500) ``` You can set additional headers for the response. They will be merged with headers set before `error!` call. ```ruby error!('Something went wrong', 500, 'X-Error-Detail' => 'Invalid token.') ``` You can present documented errors with a Grape entity using the the [grape-entity](https://github.com/ruby-grape/grape-entity) gem. ```ruby module API class Error < Grape::Entity expose :code expose :message end end ``` The following example specifies the entity to use in the `http_codes` definition. ```ruby desc 'My Route' do failure [[408, 'Unauthorized', API::Error]] end error!({ message: 'Unauthorized' }, 408) ``` The following example specifies the presented entity explicitly in the error message. ```ruby desc 'My Route' do failure [[408, 'Unauthorized']] end error!({ message: 'Unauthorized', with: API::Error }, 408) ``` ### Default Error HTTP Status Code By default Grape returns a 500 status code from `error!`. You can change this with `default_error_status`. ``` ruby class API < Grape::API default_error_status 400 get '/example' do error! 'This should have http status code 400' end end ``` ### Handling 404 For Grape to handle all the 404s for your API, it can be useful to use a catch-all. In its simplest form, it can be like: ```ruby route :any, '*path' do error! # or something else end ``` It is very crucial to __define this endpoint at the very end of your API__, as it literally accepts every request. ## Exception Handling Grape can be told to rescue all `StandardError` exceptions and return them in the API format. ```ruby class Twitter::API < Grape::API rescue_from :all end ``` This mimics [default `rescue` behaviour](https://ruby-doc.org/core/StandardError.html) when an exception type is not provided. Any other exception should be rescued explicitly, see [below](#exceptions-that-should-be-rescued-explicitly). Grape can also rescue from all exceptions and still use the built-in exception handing. This will give the same behavior as `rescue_from :all` with the addition that Grape will use the exception handling defined by all Exception classes that inherit `Grape::Exceptions::Base`. The intent of this setting is to provide a simple way to cover the most common exceptions and return any unexpected exceptions in the API format. ```ruby class Twitter::API < Grape::API rescue_from :grape_exceptions end ``` If you want to customize the shape of grape exceptions returned to the user, to match your `:all` handler for example, you can pass a block to `rescue_from :grape_exceptions`. ```ruby rescue_from :grape_exceptions do |e| error!(e, e.status) end ``` You can also rescue specific exceptions. ```ruby class Twitter::API < Grape::API rescue_from ArgumentError, UserDefinedError end ``` In this case ```UserDefinedError``` must be inherited from ```StandardError```. Notice that you could combine these two approaches (rescuing custom errors takes precedence). For example, it's useful for handling all exceptions except Grape validation errors. ```ruby class Twitter::API < Grape::API rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e, 400) end rescue_from :all end ``` The error format will match the request format. See "Content-Types" below. Custom error formatters for existing and additional types can be defined with a proc. ```ruby class Twitter::API < Grape::API error_formatter :txt, ->(message, backtrace, options, env, original_exception) { "error: #{message} from #{backtrace}" } end ``` You can also use a module or class. ```ruby module CustomFormatter def self.call(message, backtrace, options, env, original_exception) { message: message, backtrace: backtrace } end end class Twitter::API < Grape::API error_formatter :custom, CustomFormatter end ``` You can rescue all exceptions with a code block. The `error!` wrapper automatically sets the default error code and content-type. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| error!("rescued from #{e.class.name}") end end ``` Optionally, you can set the format, status code and headers. ```ruby class Twitter::API < Grape::API format :json rescue_from :all do |e| error!({ error: 'Server error.' }, 500, { 'Content-Type' => 'text/error' }) end end ``` You can also rescue all exceptions with a code block and handle the Rack response at the lowest level. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }) end end ``` Or rescue specific exceptions. ```ruby class Twitter::API < Grape::API rescue_from ArgumentError do |e| error!("ArgumentError: #{e.message}") end rescue_from NoMethodError do |e| error!("NoMethodError: #{e.message}") end end ``` By default, `rescue_from` will rescue the exceptions listed and all their subclasses. Assume you have the following exception classes defined. ```ruby module APIErrors class ParentError < StandardError; end class ChildError < ParentError; end end ``` Then the following `rescue_from` clause will rescue exceptions of type `APIErrors::ParentError` and its subclasses (in this case `APIErrors::ChildError`). ```ruby rescue_from APIErrors::ParentError do |e| error!({ error: "#{e.class} error", message: e.message }, e.status) end ``` To only rescue the base exception class, set `rescue_subclasses: false`. The code below will rescue exceptions of type `RuntimeError` but _not_ its subclasses. ```ruby rescue_from RuntimeError, rescue_subclasses: false do |e| error!({ status: e.status, message: e.message, errors: e.errors }, e.status) end ``` Helpers are also available inside `rescue_from`. ```ruby class Twitter::API < Grape::API format :json helpers do def server_error! error!({ error: 'Server error.' }, 500, { 'Content-Type' => 'text/error' }) end end rescue_from :all do |e| server_error! end end ``` The `rescue_from` handler must return a `Rack::Response` object, call `error!`, or raise an exception (either the original exception or another custom one). The exception raised in `rescue_from` will be handled outside Grape. For example, if you mount Grape in Rails, the exception will be handle by [Rails Action Controller](https://guides.rubyonrails.org/action_controller_overview.html#rescue). Alternately, use the `with` option in `rescue_from` to specify a method or a `proc`. ```ruby class Twitter::API < Grape::API format :json helpers do def server_error! error!({ error: 'Server error.' }, 500, { 'Content-Type' => 'text/error' }) end end rescue_from :all, with: :server_error! rescue_from ArgumentError, with: -> { Rack::Response.new('rescued with a method', 400) } end ``` Inside the `rescue_from` block, the environment of the original controller method(`.self` receiver) is accessible through the `#context` method. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| user_id = context.params[:user_id] error!("error for #{user_id}") end end ``` #### Rescuing exceptions inside namespaces You could put `rescue_from` clauses inside a namespace and they will take precedence over ones defined in the root scope: ```ruby class Twitter::API < Grape::API rescue_from ArgumentError do |e| error!("outer") end namespace :statuses do rescue_from ArgumentError do |e| error!("inner") end get do raise ArgumentError.new end end end ``` Here `'inner'` will be result of handling occurred `ArgumentError`. #### Unrescuable Exceptions `Grape::Exceptions::InvalidVersionHeader`, which is raised when the version in the request header doesn't match the currently evaluated version for the endpoint, will _never_ be rescued from a `rescue_from` block (even a `rescue_from :all`) This is because Grape relies on Rack to catch that error and try the next versioned-route for cases where there exist identical Grape endpoints with different versions. #### Exceptions that should be rescued explicitly Any exception that is not subclass of `StandardError` should be rescued explicitly. Usually it is not a case for an application logic as such errors point to problems in Ruby runtime. This is following [standard recommendations for exceptions handling](https://ruby-doc.org/core/Exception.html). ## Logging `Grape::API` provides a `logger` method which by default will return an instance of the `Logger` class from Ruby's standard library. To log messages from within an endpoint, you need to define a helper to make the logger available in the endpoint context. ```ruby class API < Grape::API helpers do def logger API.logger end end post '/statuses' do logger.info "#{current_user} has statused" end end ``` To change the logger level. ```ruby class API < Grape::API self.logger.level = Logger::INFO end ``` You can also set your own logger. ```ruby class MyLogger def warning(message) puts "this is a warning: #{message}" end end class API < Grape::API logger MyLogger.new helpers do def logger API.logger end end get '/statuses' do logger.warning "#{current_user} has statused" end end ``` For similar to Rails request logging try the [grape_logging](https://github.com/aserafin/grape_logging) or [grape-middleware-logger](https://github.com/ridiculous/grape-middleware-logger) gems. ## API Formats Your API can declare which content-types to support by using `content_type`. If you do not specify any, Grape will support _XML_, _JSON_, _BINARY_, and _TXT_ content-types. The default format is `:txt`; you can change this with `default_format`. Essentially, the two APIs below are equivalent. ```ruby class Twitter::API < Grape::API # no content_type declarations, so Grape uses the defaults end class Twitter::API < Grape::API # the following declarations are equivalent to the defaults content_type :xml, 'application/xml' content_type :json, 'application/json' content_type :binary, 'application/octet-stream' content_type :txt, 'text/plain' default_format :txt end ``` If you declare any `content_type` whatsoever, the Grape defaults will be overridden. For example, the following API will only support the `:xml` and `:rss` content-types, but not `:txt`, `:json`, or `:binary`. Importantly, this means the `:txt` default format is not supported! So, make sure to set a new `default_format`. ```ruby class Twitter::API < Grape::API content_type :xml, 'application/xml' content_type :rss, 'application/xml+rss' default_format :xml end ``` Serialization takes place automatically. For example, you do not have to call `to_json` in each JSON API endpoint implementation. The response format (and thus the automatic serialization) is determined in the following order: * Use the file extension, if specified. If the file is .json, choose the JSON format. * Use the value of the `format` parameter in the query string, if specified. * Use the format set by the `format` option, if specified. * Attempt to find an acceptable format from the `Accept` header. * Use the default format, if specified by the `default_format` option. * Default to `:txt`. For example, consider the following API. ```ruby class MultipleFormatAPI < Grape::API content_type :xml, 'application/xml' content_type :json, 'application/json' default_format :json get :hello do { hello: 'world' } end end ``` * `GET /hello` (with an `Accept: */*` header) does not have an extension or a `format` parameter, so it will respond with JSON (the default format). * `GET /hello.xml` has a recognized extension, so it will respond with XML. * `GET /hello?format=xml` has a recognized `format` parameter, so it will respond with XML. * `GET /hello.xml?format=json` has a recognized extension (which takes precedence over the `format` parameter), so it will respond with XML. * `GET /hello.xls` (with an `Accept: */*` header) has an extension, but that extension is not recognized, so it will respond with JSON (the default format). * `GET /hello.xls` with an `Accept: application/xml` header has an unrecognized extension, but the `Accept` header corresponds to a recognized format, so it will respond with XML. * `GET /hello.xls` with an `Accept: text/plain` header has an unrecognized extension *and* an unrecognized `Accept` header, so it will respond with JSON (the default format). You can override this process explicitly by calling `api_format` in the API itself. For example, the following API will let you upload arbitrary files and return their contents as an attachment with the correct MIME type. ```ruby class Twitter::API < Grape::API post 'attachment' do filename = params[:file][:filename] content_type MIME::Types.type_for(filename)[0].to_s api_format :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end end ``` You can have your API only respond to a single format with `format`. If you use this, the API will **not** respond to file extensions other than specified in `format`. For example, consider the following API. ```ruby class SingleFormatAPI < Grape::API format :json get :hello do { hello: 'world' } end end ``` * `GET /hello` will respond with JSON. * `GET /hello.json` will respond with JSON. * `GET /hello.xml`, `GET /hello.foobar`, or *any* other extension will respond with an HTTP 404 error code. * `GET /hello?format=xml` will respond with an HTTP 406 error code, because the XML format specified by the request parameter is not supported. * `GET /hello` with an `Accept: application/xml` header will still respond with JSON, since it could not negotiate a recognized content-type from the headers and JSON is the effective default. The formats apply to parsing, too. The following API will only respond to the JSON content-type and will not parse any other input than `application/json`, `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related` and `multipart/mixed`. All other requests will fail with an HTTP 406 error code. ```ruby class Twitter::API < Grape::API format :json end ``` When the content-type is omitted, Grape will return a 406 error code unless `default_format` is specified. The following API will try to parse any data without a content-type using a JSON parser. ```ruby class Twitter::API < Grape::API format :json default_format :json end ``` If you combine `format` with `rescue_from :all`, errors will be rendered using the same format. If you do not want this behavior, set the default error formatter with `default_error_formatter`. ```ruby class Twitter::API < Grape::API format :json content_type :txt, 'text/plain' default_error_formatter :txt end ``` Custom formatters for existing and additional types can be defined with a proc. ```ruby class Twitter::API < Grape::API content_type :xls, 'application/vnd.ms-excel' formatter :xls, ->(object, env) { object.to_xls } end ``` You can also use a module or class. ```ruby module XlsFormatter def self.call(object, env) object.to_xls end end class Twitter::API < Grape::API content_type :xls, 'application/vnd.ms-excel' formatter :xls, XlsFormatter end ``` Built-in formatters are the following. * `:json`: use object's `to_json` when available, otherwise call `MultiJson.dump` * `:xml`: use object's `to_xml` when available, usually via `MultiXml` * `:txt`: use object's `to_txt` when available, otherwise `to_s` * `:serializable_hash`: use object's `serializable_hash` when available, otherwise fallback to `:json` * `:binary`: data will be returned "as is" If a body is present in a request to an API, with a Content-Type header value that is of an unsupported type a "415 Unsupported Media Type" error code will be returned by Grape. Response statuses that indicate no content as defined by [Rack](https://github.com/rack) [here](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) will bypass serialization and the body entity - though there should be none - will not be modified. ### JSONP Grape supports JSONP via [Rack::JSONP](https://github.com/rack/rack-contrib), part of the [rack-contrib](https://github.com/rack/rack-contrib) gem. Add `rack-contrib` to your `Gemfile`. ```ruby require 'rack/contrib' class API < Grape::API use Rack::JSONP format :json get '/' do 'Hello World' end end ``` ### CORS Grape supports CORS via [Rack::CORS](https://github.com/cyu/rack-cors), part of the [rack-cors](https://github.com/cyu/rack-cors) gem. Add `rack-cors` to your `Gemfile`, then use the middleware in your config.ru file. ```ruby require 'rack/cors' use Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: :get end end run Twitter::API ``` ## Content-type Content-type is set by the formatter. You can override the content-type of the response at runtime by setting the `Content-Type` header. ```ruby class API < Grape::API get '/home_timeline_js' do content_type 'application/javascript' "var statuses = ...;" end end ``` ## API Data Formats Grape accepts and parses input data sent with the POST and PUT methods as described in the Parameters section above. It also supports custom data formats. You must declare additional content-types via `content_type` and optionally supply a parser via `parser` unless a parser is already available within Grape to enable a custom format. Such a parser can be a function or a class. With a parser, parsed data is available "as-is" in `env['api.request.body']`. Without a parser, data is available "as-is" and in `env['api.request.input']`. The following example is a trivial parser that will assign any input with the "text/custom" content-type to `:value`. The parameter will be available via `params[:value]` inside the API call. ```ruby module CustomParser def self.call(object, env) { value: object.to_s } end end ``` ```ruby content_type :txt, 'text/plain' content_type :custom, 'text/custom' parser :custom, CustomParser put 'value' do params[:value] end ``` You can invoke the above API as follows. ``` curl -X PUT -d 'data' 'http://localhost:9292/value' -H Content-Type:text/custom -v ``` You can disable parsing for a content-type with `nil`. For example, `parser :json, nil` will disable JSON parsing altogether. The request data is then available as-is in `env['api.request.body']`. ## JSON and XML Processors Grape uses `JSON` and `ActiveSupport::XmlMini` for JSON and XML parsing by default. It also detects and supports [multi_json](https://github.com/intridea/multi_json) and [multi_xml](https://github.com/sferik/multi_xml). Adding those gems to your Gemfile and requiring them will enable them and allow you to swap the JSON and XML back-ends. ## RESTful Model Representations Grape supports a range of ways to present your data with some help from a generic `present` method, which accepts two arguments: the object to be presented and the options associated with it. The options hash may include `:with`, which defines the entity to expose. ### Grape Entities Add the [grape-entity](https://github.com/ruby-grape/grape-entity) gem to your Gemfile. Please refer to the [grape-entity documentation](https://github.com/ruby-grape/grape-entity/blob/master/README.md) for more details. The following example exposes statuses. ```ruby module API module Entities class Status < Grape::Entity expose :user_name expose :text, documentation: { type: 'string', desc: 'Status update text.' } expose :ip, if: { type: :full } expose :user_type, :user_id, if: ->(status, options) { status.user.public? } expose :digest do |status, options| Digest::MD5.hexdigest(status.txt) end expose :replies, using: API::Status, as: :replies end end class Statuses < Grape::API version 'v1' desc 'Statuses index' do params: API::Entities::Status.documentation end get '/statuses' do statuses = Status.all type = current_user.admin? ? :full : :default present statuses, with: API::Entities::Status, type: type end end end ``` You can use entity documentation directly in the params block with `using: Entity.documentation`. ```ruby module API class Statuses < Grape::API version 'v1' desc 'Create a status' params do requires :all, except: [:ip], using: API::Entities::Status.documentation.except(:id) end post '/status' do Status.create! params end end end ``` You can present with multiple entities using an optional Symbol argument. ```ruby get '/statuses' do statuses = Status.all.page(1).per(20) present :total_page, 10 present :per_page, 20 present :statuses, statuses, with: API::Entities::Status end ``` The response will be ``` { total_page: 10, per_page: 20, statuses: [] } ``` In addition to separately organizing entities, it may be useful to put them as namespaced classes underneath the model they represent. ```ruby class Status def entity Entity.new(self) end class Entity < Grape::Entity expose :text, :user_id end end ``` If you organize your entities this way, Grape will automatically detect the `Entity` class and use it to present your models. In this example, if you added `present Status.new` to your endpoint, Grape will automatically detect that there is a `Status::Entity` class and use that as the representative entity. This can still be overridden by using the `:with` option or an explicit `represents` call. You can present `hash` with `Grape::Presenters::Presenter` to keep things consistent. ```ruby get '/users' do present { id: 10, name: :dgz }, with: Grape::Presenters::Presenter end ```` The response will be ```ruby { id: 10, name: 'dgz' } ``` It has the same result with ```ruby get '/users' do present :id, 10 present :name, :dgz end ``` ### Hypermedia and Roar You can use [Roar](https://github.com/apotonick/roar) to render HAL or Collection+JSON with the help of [grape-roar](https://github.com/ruby-grape/grape-roar), which defines a custom JSON formatter and enables presenting entities with Grape's `present` keyword. ### Rabl You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the [grape-rabl](https://github.com/ruby-grape/grape-rabl) gem, which defines a custom Grape Rabl formatter. ### Active Model Serializers You can use [Active Model Serializers](https://github.com/rails-api/active_model_serializers) serializers with the help of the [grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers) gem, which defines a custom Grape AMS formatter. ## Sending Raw or No Data In general, use the binary format to send raw data. ```ruby class API < Grape::API get '/file' do content_type 'application/octet-stream' File.binread 'file.bin' end end ``` You can set the response body explicitly with `body`. ```ruby class API < Grape::API get '/' do content_type 'text/plain' body 'Hello World' # return value ignored end end ``` Use `body false` to return `204 No Content` without any data or content-type. If you want to empty the body with an HTTP status code other than `204 No Content`, you can override the status code after specifying `body false` as follows ```ruby class API < Grape::API get '/' do body false status 304 end end ``` You can also set the response to a file with `sendfile`. This works with the [Rack::Sendfile](https://www.rubydoc.info/gems/rack/Rack/Sendfile) middleware to optimally send the file through your web server software. ```ruby class API < Grape::API get '/' do sendfile '/path/to/file' end end ``` To stream a file in chunks use `stream` ```ruby class API < Grape::API get '/' do stream '/path/to/file' end end ``` If you want to stream non-file data use the `stream` method and a `Stream` object. This is an object that responds to `each` and yields for each chunk to send to the client. Each chunk will be sent as it is yielded instead of waiting for all of the content to be available. ```ruby class MyStream def each yield 'part 1' yield 'part 2' yield 'part 3' end end class API < Grape::API get '/' do stream MyStream.new end end ``` ## Authentication ### Basic Auth Grape has built-in Basic authentication (the given `block` is executed in the context of the current `Endpoint`). Authentication applies to the current namespace and any children, but not parents. ```ruby http_basic do |username, password| # verify user's password here # IMPORTANT: make sure you use a comparison method which isn't prone to a timing attack end ``` ### Register custom middleware for authentication Grape can use custom Middleware for authentication. How to implement these Middleware have a look at `Rack::Auth::Basic` or similar implementations. For registering a Middleware you need the following options: * `label` - the name for your authenticator to use it later * `MiddlewareClass` - the MiddlewareClass to use for authentication * `option_lookup_proc` - A Proc with one Argument to lookup the options at runtime (return value is an `Array` as Parameter for the Middleware). Example: ```ruby Grape::Middleware::Auth::Strategies.add(:my_auth, AuthMiddleware, ->(options) { [options[:realm]] } ) auth :my_auth, { realm: 'Test Api'} do |credentials| # lookup the user's password here { 'user1' => 'password1' }[username] end ``` Use [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) for OAuth2 support. You can access the controller params, headers, and helpers through the context with the `#context` method inside any auth middleware inherited from `Grape::Middleware::Auth::Base`. ## Describing and Inspecting an API Grape routes can be reflected at runtime. This can notably be useful for generating documentation. Grape exposes arrays of API versions and compiled routes. Each route contains a `prefix`, `version`, `namespace`, `method` and `params`. You can add custom route settings to the route metadata with `route_setting`. ```ruby class TwitterAPI < Grape::API version 'v1' desc 'Includes custom settings.' route_setting :custom, key: 'value' get do end end ``` Examine the routes at runtime. ```ruby TwitterAPI::versions # yields [ 'v1', 'v2' ] TwitterAPI::routes # yields an array of Grape::Route objects TwitterAPI::routes[0].version # => 'v1' TwitterAPI::routes[0].description # => 'Includes custom settings.' TwitterAPI::routes[0].settings[:custom] # => { key: 'value' } ``` Note that `Route#route_xyz` methods have been deprecated since 0.15.0 and removed since 2.0.1. Please use `Route#xyz` instead. Note that difference of `Route#options` and `Route#settings`. The `options` can be referred from your route, it should be set by specifying key and value on verb methods such as `get`, `post` and `put`. The `settings` can also be referred from your route, but it should be set by specifying key and value on `route_setting`. ## Current Route and Endpoint It's possible to retrieve the information about the current route from within an API call with `route`. ```ruby class MyAPI < Grape::API desc 'Returns a description of a parameter.' params do requires :id, type: Integer, desc: 'Identity.' end get 'params/:id' do route.params[params[:id]] # yields the parameter description end end ``` The current endpoint responding to the request is `self` within the API block or `env['api.endpoint']` elsewhere. The endpoint has some interesting properties, such as `source` which gives you access to the original code block of the API implementation. This can be particularly useful for building a logger middleware. ```ruby class ApiLogger < Grape::Middleware::Base def before file = env['api.endpoint'].source.source_location[0] line = env['api.endpoint'].source.source_location[1] logger.debug "[api] #{file}:#{line}" end end ``` ## Before, After and Finally Blocks can be executed before or after every API call, using `before`, `after`, `before_validation` and `after_validation`. If the API fails the `after` call will not be triggered, if you need code to execute for sure use the `finally`. Before and after callbacks execute in the following order: 1. `before` 2. `before_validation` 3. _validations_ 4. `after_validation` (upon successful validation) 5. _the API call_ (upon successful validation) 6. `after` (upon successful validation and API call) 7. `finally` (always) Steps 4, 5 and 6 only happen if validation succeeds. If a request for a resource is made with an unsupported HTTP method (returning HTTP 405) only `before` callbacks will be executed. The remaining callbacks will be bypassed. If a request for a resource is made that triggers the built-in `OPTIONS` handler, only `before` and `after` callbacks will be executed. The remaining callbacks will be bypassed. For example, using a simple `before` block to set a header. ```ruby before do header 'X-Robots-Tag', 'noindex' end ``` You can ensure a block of code runs after every request (including failures) with `finally`: ```ruby finally do # this code will run after every request (successful or failed) end ``` **Namespaces** Callbacks apply to each API call within and below the current namespace: ```ruby class MyAPI < Grape::API get '/' do "root - #{@blah}" end namespace :foo do before do @blah = 'blah' end get '/' do "root - foo - #{@blah}" end namespace :bar do get '/' do "root - foo - bar - #{@blah}" end end end end ``` The behavior is then: ```bash GET / # 'root - ' GET /foo # 'root - foo - blah' GET /foo/bar # 'root - foo - bar - blah' ``` Params on a `namespace` (or whichever alias you are using) will also be available when using `before_validation` or `after_validation`: ```ruby class MyAPI < Grape::API params do requires :blah, type: Integer end resource ':blah' do after_validation do # if we reach this point validations will have passed @blah = declared(params, include_missing: false)[:blah] end get '/' do @blah.class end end end ``` The behavior is then: ```bash GET /123 # 'Integer' GET /foo # 400 error - 'blah is invalid' ``` **Versioning** When a callback is defined within a version block, it's only called for the routes defined in that block. ```ruby class Test < Grape::API resource :foo do version 'v1', :using => :path do before do @output ||= 'v1-' end get '/' do @output += 'hello' end end version 'v2', :using => :path do before do @output ||= 'v2-' end get '/' do @output += 'hello' end end end end ``` The behavior is then: ```bash GET /foo/v1 # 'v1-hello' GET /foo/v2 # 'v2-hello' ``` **Altering Responses** Using `present` in any callback allows you to add data to a response: ```ruby class MyAPI < Grape::API format :json after_validation do present :name, params[:name] if params[:name] end get '/greeting' do present :greeting, 'Hello!' end end ``` The behavior is then: ```bash GET /greeting # {"greeting":"Hello!"} GET /greeting?name=Alan # {"name":"Alan","greeting":"Hello!"} ``` Instead of altering a response, you can also terminate and rewrite it from any callback using `error!`, including `after`. This will cause all subsequent steps in the process to not be called. **This includes the actual api call and any callbacks** ## Anchoring Grape by default anchors all request paths, which means that the request URL should match from start to end to match, otherwise a `404 Not Found` is returned. However, this is sometimes not what you want, because it is not always known upfront what can be expected from the call. This is because Rack-mount by default anchors requests to match from the start to the end, or not at all. Rails solves this problem by using a `anchor: false` option in your routes. In Grape this option can be used as well when a method is defined. For instance when your API needs to get part of an URL, for instance: ```ruby class TwitterAPI < Grape::API namespace :statuses do get '/(*:status)', anchor: false do end end end ``` This will match all paths starting with '/statuses/'. There is one caveat though: the `params[:status]` parameter only holds the first part of the request url. Luckily this can be circumvented by using the described above syntax for path specification and using the `PATH_INFO` Rack environment variable, using `env['PATH_INFO']`. This will hold everything that comes after the '/statuses/' part. ## Instance Variables You can use instance variables to pass information across the various stages of a request. An instance variable set within a `before` validator is accessible within the endpoint's code and can also be utilized within the `rescue_from` handler. ```ruby class TwitterAPI < Grape::API before do @var = 1 end get '/' do puts @var # => 1 raise end rescue_from :all do puts @var # => 1 end end ``` The values of instance variables cannot be shared among various endpoints within the same API. This limitation arises due to Grape generating a new instance for each request made. Consequently, instance variables set within an endpoint during one request differ from those set during a subsequent request, as they exist within separate instances. ```ruby class TwitterAPI < Grape::API get '/first' do @var = 1 puts @var # => 1 end get '/second' do puts @var # => nil end end ``` ## Using Custom Middleware ### Grape Middleware You can make a custom middleware by using `Grape::Middleware::Base`. It's inherited from some grape official middlewares in fact. For example, you can write a middleware to log application exception. ```ruby class LoggingError < Grape::Middleware::Base def after return unless @app_response && @app_response[0] == 500 env['rack.logger'].error("Raised error on #{env['PATH_INFO']}") end end ``` Your middleware can overwrite application response as follows, except error case. ```ruby class Overwriter < Grape::Middleware::Base def after [200, { 'Content-Type' => 'text/plain' }, ['Overwritten.']] end end ``` You can add your custom middleware with `use`, that push the middleware onto the stack, and you can also control where the middleware is inserted using `insert`, `insert_before` and `insert_after`. ```ruby class CustomOverwriter < Grape::Middleware::Base def after [200, { 'Content-Type' => 'text/plain' }, [@options[:message]]] end end class API < Grape::API use Overwriter insert_before Overwriter, CustomOverwriter, message: 'Overwritten again.' insert 0, CustomOverwriter, message: 'Overwrites all other middleware.' get '/' do end end ``` You can access the controller params, headers, and helpers through the context with the `#context` method inside any middleware inherited from `Grape::Middleware::Base`. ### Rails Middleware Note that when you're using Grape mounted on Rails you don't have to use Rails middleware because it's already included into your middleware stack. You only have to implement the helpers to access the specific `env` variable. If you are using a custom application that is inherited from `Rails::Application` and need to insert a new middleware among the ones initiated via Rails, you will need to register it manually in your custom application class. ```ruby class Company::Application < Rails::Application config.middleware.insert_before(Rack::Attack, Middleware::ApiLogger) end ``` ### Remote IP By default you can access remote IP with `request.ip`. This is the remote IP address implemented by Rack. Sometimes it is desirable to get the remote IP [Rails-style](http://stackoverflow.com/questions/10997005/whats-the-difference-between-request-remote-ip-and-request-ip-in-rails) with `ActionDispatch::RemoteIp`. Add `gem 'actionpack'` to your Gemfile and `require 'action_dispatch/middleware/remote_ip.rb'`. Use the middleware in your API and expose a `client_ip` helper. See [this documentation](http://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html) for additional options. ```ruby class API < Grape::API use ActionDispatch::RemoteIp helpers do def client_ip env['action_dispatch.remote_ip'].to_s end end get :remote_ip do { ip: client_ip } end end ``` ## Writing Tests ### Writing Tests with Rack Use `rack-test` and define your API as `app`. #### RSpec You can test a Grape API with RSpec by making HTTP requests and examining the response. ```ruby describe Twitter::API do include Rack::Test::Methods def app Twitter::API end context 'GET /api/statuses/public_timeline' do it 'returns an empty array of statuses' do get '/api/statuses/public_timeline' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq [] end end context 'GET /api/statuses/:id' do it 'returns a status by id' do status = Status.create! get "/api/statuses/#{status.id}" expect(last_response.body).to eq status.to_json end end end ``` There's no standard way of sending arrays of objects via an HTTP GET, so POST JSON data and specify the correct content-type. ```ruby describe Twitter::API do context 'POST /api/statuses' do it 'creates many statuses' do statuses = [{ text: '...' }, { text: '...'}] post '/api/statuses', statuses.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq 201 end end end ``` #### Airborne You can test with other RSpec-based frameworks, including [Airborne](https://github.com/brooklynDev/airborne), which uses `rack-test` to make requests. ```ruby require 'airborne' Airborne.configure do |config| config.rack_app = Twitter::API end describe Twitter::API do context 'GET /api/statuses/:id' do it 'returns a status by id' do status = Status.create! get "/api/statuses/#{status.id}" expect_json(status.as_json) end end end ``` #### MiniTest ```ruby require 'test_helper' class Twitter::APITest < MiniTest::Test include Rack::Test::Methods def app Twitter::API end def test_get_api_statuses_public_timeline_returns_an_empty_array_of_statuses get '/api/statuses/public_timeline' assert last_response.ok? assert_equal [], JSON.parse(last_response.body) end def test_get_api_statuses_id_returns_a_status_by_id status = Status.create! get "/api/statuses/#{status.id}" assert_equal status.to_json, last_response.body end end ``` ### Writing Tests with Rails #### RSpec ```ruby describe Twitter::API do context 'GET /api/statuses/public_timeline' do it 'returns an empty array of statuses' do get '/api/statuses/public_timeline' expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq [] end end context 'GET /api/statuses/:id' do it 'returns a status by id' do status = Status.create! get "/api/statuses/#{status.id}" expect(response.body).to eq status.to_json end end end ``` In Rails, HTTP request tests would go into the `spec/requests` group. You may want your API code to go into `app/api` - you can match that layout under `spec` by adding the following in `spec/rails_helper.rb`. ```ruby RSpec.configure do |config| config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/ end ``` #### MiniTest ```ruby class Twitter::APITest < ActiveSupport::TestCase include Rack::Test::Methods def app Rails.application end test 'GET /api/statuses/public_timeline returns an empty array of statuses' do get '/api/statuses/public_timeline' assert last_response.ok? assert_equal [], JSON.parse(last_response.body) end test 'GET /api/statuses/:id returns a status by id' do status = Status.create! get "/api/statuses/#{status.id}" assert_equal status.to_json, last_response.body end end ``` ### Stubbing Helpers Because helpers are mixed in based on the context when an endpoint is defined, it can be difficult to stub or mock them for testing. The `Grape::Endpoint.before_each` method can help by allowing you to define behavior on the endpoint that will run before every request. ```ruby describe 'an endpoint that needs helpers stubbed' do before do Grape::Endpoint.before_each do |endpoint| allow(endpoint).to receive(:helper_name).and_return('desired_value') end end after do Grape::Endpoint.before_each nil end it 'stubs the helper' do end end ``` ## Reloading API Changes in Development ### Reloading in Rack Applications Use [grape-reload](https://github.com/AlexYankee/grape-reload). ### Reloading in Rails Applications #### Rails 7+ (Zeitwerk) Rails 7+ uses [Zeitwerk](https://github.com/fxn/zeitwerk) as the default autoloader, which automatically handles reloading of code in development mode without any additional configuration. If your API files are in `app/api`, Zeitwerk will automatically autoload and reload them. No additional configuration is needed. If you encounter issues with reloading, ensure that: 1. Your API files follow Zeitwerk naming conventions (file names should match class names). 2. The `config.enable_reloading` is set to `true` in `config/environments/development.rb` (this is the default). For troubleshooting autoloading issues, have a look at the [Rails documentation](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#troubleshooting). See the [Rails Autoloading and Reloading Constants guide](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html) for more information. #### Rails 6 and Earlier For Rails versions before 7, you need to configure reloading manually. Add API paths to `config/application.rb`. ```ruby # Auto-load API and its subdirectories config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] ``` Create `config/initializers/reload_api.rb`. ```ruby if Rails.env.development? ActiveSupport::Dependencies.explicitly_unloadable_constants << 'Twitter::API' api_files = Dir[Rails.root.join('app', 'api', '**', '*.rb')] api_reloader = ActiveSupport::FileUpdateChecker.new(api_files) do Rails.application.reload_routes! end ActiveSupport::Reloader.to_prepare do api_reloader.execute_if_updated end end ``` See [StackOverflow #3282655](http://stackoverflow.com/questions/3282655/ruby-on-rails-3-reload-lib-directory-for-each-request/4368838#4368838) for more information. ## Performance Monitoring ### Active Support Instrumentation Grape has built-in support for [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) which provides simple hook points to instrument key parts of your application. #### Hook Points The following hook points are currently supported: ##### endpoint_run.grape The main execution of an endpoint, includes filters and rendering. * *endpoint* - The endpoint instance ##### endpoint_render.grape The execution of the main content block of the endpoint. * *endpoint* - The endpoint instance ##### endpoint_run_filters.grape * *endpoint* - The endpoint instance * *filters* - The filters being executed * *type* - The type of filters (before, before_validation, after_validation, after) ##### endpoint_run_validators.grape The execution of validators. * *endpoint* - The endpoint instance * *validators* - The validators being executed * *request* - The request being validated ##### format_response.grape Serialization or template rendering. * *env* - The request environment * *formatter* - The formatter object (e.g., `Grape::Formatter::Json`) See the [ActiveSupport::Notifications documentation](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) for information on how to subscribe to these events. #### Subscribe to Hooks Once subscribed to the instrumentation, you can intercept the events reported above. ```ruby ActiveSupport::Notifications.subscribe(//) do |name, start, finish, id, payload| # your code to intercept the notification end ``` The request data, the API’s internal data, and the response can be retrieved from the payload. You can use `payload.fetch(:endpoint)` or directly `payload[:endpoint]`. The `:endpoint` contains the data currently being processed, and access to attributes such as `body`, `request`, `params`, `headers`, `cookies` and `response_cookies` For example, `payload[:endpoint].body` provides the current state of the response. ```ruby ActiveSupport::Notifications.subscribe(/v1/) do |name, start, finish, id, payload| hook_record = { hook: name status: payload[:env]&.dig("api.endpoint")&.status format: payload[:env]&.dig("api.format") body: payload[:endpoint]&.body duration: (finish - start) * 1000 } # your code to save the notification end ``` ### Monitoring Products Grape integrates with following third-party tools: * **New Relic** - [built-in support](https://docs.newrelic.com/docs/agents/ruby-agent/frameworks/grape-instrumentation) from v3.10.0 of the official [newrelic_rpm](https://github.com/newrelic/rpm) gem, also [newrelic-grape](https://github.com/xinminlabs/newrelic-grape) gem * **Librato Metrics** - [grape-librato](https://github.com/seanmoon/grape-librato) gem * **Rails Performance** - [rails_performance](https://github.com/igorkasyanchuk/rails_performance) gem * **[Skylight](https://www.skylight.io/)** - [skylight](https://github.com/skylightio/skylight-ruby) gem, [documentation](https://docs.skylight.io/grape/) * **[AppSignal](https://www.appsignal.com)** - [appsignal-ruby](https://github.com/appsignal/appsignal-ruby) gem, [documentation](http://docs.appsignal.com/getting-started/supported-frameworks.html#grape) * **[ElasticAPM](https://www.elastic.co/products/apm)** - [elastic-apm](https://github.com/elastic/apm-agent-ruby) gem, [documentation](https://www.elastic.co/guide/en/apm/agent/ruby/3.x/getting-started-rack.html#getting-started-grape) * **[Datadog APM](https://docs.datadoghq.com/tracing/)** - [ddtrace](https://github.com/datadog/dd-trace-rb) gem, [documentation](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#grape) ## Contributing to Grape Grape is work of hundreds of contributors. You're encouraged to submit pull requests, propose features and discuss issues. See [CONTRIBUTING](CONTRIBUTING.md). ## Security See [SECURITY](SECURITY.md) for details. ## License MIT License. See [LICENSE](LICENSE) for details. ## Copyright Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. ================================================ FILE: RELEASING.md ================================================ Releasing Grape =============== There're no particular rules about when to release Grape. Release bug fixes frequently, features not so frequently and breaking API changes rarely. ### Release Run tests, check that all tests succeed locally. ``` bundle install rake ``` Double-check that the [last build succeeded](https://github.com/ruby-grape/grape/actions) for all supported platforms. Those with r/w permissions to the [master Grape repository](https://github.com/ruby-grape/grape) generally have large Grape-based projects. Point one to Grape HEAD and run all your API tests to catch any obvious regressions. ``` gem grape, github: 'ruby-grape/grape' ``` Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the documentation for a stable release. Remove references to the previous release of Grape. Keep the file open, you'll have to undo this change after the release. ``` ## Stable Release You're reading the documentation for the stable release of Grape, 0.6.0. ``` Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version. ``` #### 0.6.0 (2013/9/16) ``` Remove the line with "Your contribution here.", since there will be no more contributions to this release. Commit your changes. ``` git add README.md CHANGELOG.md git commit -m "Preparing for release, 0.6.0." git push origin master ``` Release. ``` $ rake release grape 0.6.0 built to pkg/grape-0.6.0.gem. Tagged v0.6.0. Pushed git commits and tags. Pushed grape 0.6.0 to rubygems.org. ``` ### Prepare for the Next Version Modify the "Stable Release" section in [README.md](README.md). Change the text to reflect that this is going to be the next release. ``` ## Stable Release You're reading the documentation for the next release of Grape, which should be 0.6.1. The current stable release is [0.6.0](https://github.com/ruby-grape/grape/blob/v0.6.0/README.md). ``` Add the next release to [CHANGELOG.md](CHANGELOG.md). ``` ### 0.6.1 (Next) #### Features * Your contribution here. #### Fixes * Your contribution here. ``` Bump the minor version in lib/grape/version.rb. ```ruby module Grape VERSION = '0.6.1'.freeze end ``` Commit your changes. ``` git add CHANGELOG.md README.md lib/grape/version.rb git commit -m "Preparing for next development iteration, 0.6.1." git push origin master ``` ### Make an Announcement Make an announcement on the [ruby-grape@googlegroups.com](mailto:ruby-grape@googlegroups.com) mailing list. The general format is as follows. ``` Grape 0.6.0 has been released. There were 8 contributors to this release, not counting documentation. Please note the breaking API change in ... [copy/paste CHANGELOG here] ``` ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require('rubygems') require('bundler') Bundler.setup(:default, :test, :development) Bundler::GemHelper.install_tasks require('rspec/core/rake_task') RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.exclude_pattern = 'spec/integration/**/*_spec.rb' end RSpec::Core::RakeTask.new(:rcov) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.rcov = true end task(:spec) require('rainbow/ext/string') unless String.respond_to?(:color) require('rubocop/rake_task') RuboCop::RakeTask.new task(default: %i[rubocop spec]) ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Version 2.2 or newer is currently supported. ## Reporting a Vulnerability 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. ================================================ FILE: UPGRADING.md ================================================ Upgrading Grape =============== ### Upgrading to >= 3.2 #### `with` now uses keyword arguments The `with` DSL method now uses `**opts` instead of a positional hash. Calls using bare keyword syntax are unaffected: ```ruby # still works with(type: String, documentation: { in: 'body' }) { ... } ``` However, passing an explicit hash literal will now raise an `ArgumentError`: ```ruby # raises ArgumentError with({ type: String }) { ... } ``` See [#2663](https://github.com/ruby-grape/grape/pull/2663) for more information. #### Custom validators: use `translate` instead of `I18n` directly `Grape::Util::Translation` is now included in `Grape::Validations::Validators::Base`. Custom validators that previously called `I18n.t` or `I18n.translate` directly should switch to the `translate`, which provides the same `:en` fallback logic used by all built-in validators. Key points: - `scope` defaults to `'grape.errors.messages'` — no need to specify it for standard error message keys. - Interpolation variables are passed directly to I18n. - `format` is no longer needed — `translate` returns the fully interpolated string. ```ruby # Before raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: format(I18n.t(:my_key, scope: 'grape.errors.messages'), min: 2, max: 10) ) # After raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: translate(:my_key, min: 2, max: 10) ) ``` See [#2662](https://github.com/ruby-grape/grape/pull/2662) for more information. ### Upgrading to >= 3.1 #### Explicit kwargs for `namespace` and `route_param` The `API#namespace` and `route_param` methods are now defined with `**options` instead of `options = {}`. In addtion, `requirements` in explicitly defined so it's not in `options` anymore. You can still call `requirements` like before but `options[:requirements]` will be empty. For `route_param`, `type` is also an explicit parameter so it's not in `options` anymore. See [#2647](https://github.com/ruby-grape/grape/pull/2647) for more information. #### ParamsBuilder Grape::Extensions Deprecated [ParamsBuilder's extensions](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#params-builder) have been removed. #### Enhanced API compile! Endpoints are now "compiled" instead of lazy loaded. Historically, when calling `YourAPI.compile!` in `config.ru` (or just receiving the first API call), only routing was compiled see [Grape::Router#compile!](https://github.com/ruby-grape/grape/blob/bf90e95c3b17c415c944363b1c07eb9727089ee7/lib/grape/router.rb#L41-L54) and endpoints were lazy loaded. Now, it's part of the API compilation. See [#2645](https://github.com/ruby-grape/grape/pull/2645) for more information. ### Upgrading to >= 3.0.0 #### Ruby 3+ Argument Delegation Modernization Grape has been modernized to use Ruby 3+'s preferred argument delegation patterns. This change replaces `args.extract_options!` with explicit `**kwargs` parameters throughout the codebase. - All DSL methods now use explicit keyword arguments (`**kwargs`) instead of extracting options from mixed argument lists - Method signatures are now more explicit and follow Ruby 3+ best practices - The `active_support/core_ext/array/extract_options` dependency has been removed This is a modernization effort that improves code quality while maintaining full backward compatibility. See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information. #### Configuration API Migration from ActiveSupport::Configurable to Dry::Configurable Grape has migrated from `ActiveSupport::Configurable` to `Dry::Configurable` for its configuration system since its [deprecated](https://github.com/rails/rails/blob/1cdd190a25e483b65f1f25bbd0f13a25d696b461/activesupport/lib/active_support/configurable.rb#L3-L7). See [#2617](https://github.com/ruby-grape/grape/pull/2617) for more information. #### Endpoint execution simplified and `return` deprecated Executing a endpoint's block has been simplified and calling `return` in it has been deprecated. Use `next` instead. See [#2577](https://github.com/ruby-grape/grape/pull/2577) for more information. #### Old Deprecations Clean Up - `rack_response` has been removed in favor of using `error!`. - `Grape::Exceptions::MissingGroupType` and `Grape::Exceptions::UnsupportedGroupType` aliases `MissingGroupTypeError and `UnsupportedGroupType` have been removed. - `Grape::Validations::Base` has been removed in favor of `Grape::Validations::Validators::Base`. See [2573](https://github.com/ruby-grape/grape/pull/2573) for more information. ### Upgrading to >= 2.4.0 #### Grape::Middleware::Auth::Base `type` is now validated at compile time and will raise a `Grape::Exceptions::UnknownAuthStrategy` if unknown. #### Grape::Middleware::Base - Second argument `options` is now a double splat (**) instead of single splat (*). If you're redefining `initialize` in your middleware and/or calling `super` in it, you might have to adapt the signature and the `super` call. Also, you might have to remove `{}` if you're pass `options` as a literal `Hash` or add `**` if you're using a variable. - `Grape::Middleware::Helpers` has been removed. The equivalent method `context` is now part of `Grape::Middleware::Base`. #### Grape::Http::Headers, Grape::Util::Lazy::Object Both have been removed. See [2554](https://github.com/ruby-grape/grape/pull/2554). Here are the notable changes: - Constants like `HTTP_ACCEPT` have been replaced by their literal value. - `SUPPORTED_METHODS` has been moved to `Grape` module. - `HTTP_HEADERS` has been moved to `Grape::Request` and renamed `KNOWN_HEADERS`. The last has been refreshed with new headers, and it's not lazy anymore. - `SUPPORTED_METHODS_WITHOUT_OPTIONS` and `find_supported_method` have been removed. #### Grape::Middleware::Base - Constant `TEXT_HTML` has been removed in favor of using literal string 'text/html'. - `rack_request` and `query_params` have been added. Feel free to call these in your middlewares. #### Params Builder - Passing a class to `build_with` or `Grape.config.param_builder` has been deprecated in favor of a symbolized short_name. See `SHORTNAME_LOOKUP` in [params_builder](lib/grape/params_builder.rb). - Including Grape's extensions like `Grape::Extensions::Hashie::Mash::ParamBuilder` has been deprecated in favor of using `build_with` at the route level. #### Accept Header Negotiation Harmonized [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept) header is now fully interpreted through `Rack::Utils.best_q_match` which is following [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1). Since [Grape 2.1.0](https://github.com/ruby-grape/grape/blob/master/CHANGELOG.md#210-20240615), the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header) was adhering to it, but `Grape::Middleware::Formatter` never did. Your API might act differently since it will strictly follow the [RFC2616 14.1](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) when interpreting the `Accept` header. Here are the differences: ##### Invalid or missing quality ranking The following used to yield `application/xml` and now will yield `application/json` as the preferred media type: - `application/json;q=invalid,application/xml;q=0.5` - `application/json,application/xml;q=1.0` For the invalid case, the value `invalid` was automatically `to_f` and `invalid.to_f` equals `0.0`. Now, since it doesn't match [Rack's regex](https://github.com/rack/rack/blob/3-1-stable/lib/rack/utils.rb#L138), its interpreted as non provided and its quality ranking equals 1.0. For the non provided case, 1.0 was automatically assigned and in a case of multiple best matches, the first was returned based on Ruby's sort_by `quality`. Now, 1.0 is still assigned and the last is returned in case of multiple best matches. See [Rack's implementation](https://github.com/rack/rack/blob/e8f47608668d507e0f231a932fa37c9ca551c0a5/lib/rack/utils.rb#L167) of the RFC. ##### Considering the closest generic when vendor tree Excluding the [header versioning strategy](https://github.com/ruby-grape/grape?tab=readme-ov-file#header), whenever a media type with the [vendor tree](https://datatracker.ietf.org/doc/html/rfc6838#section-3.2) leading facet `vnd.` like `application/vnd.api+json` was provided, Grape would also consider its closest generic when negotiating. In that case, `application/json` was added to the negotiation. Now, it will just consider the provided media types without considering any closest generics, and you'll need to [register](https://github.com/ruby-grape/grape?tab=readme-ov-file#api-formats) it. You can find the official vendor tree registrations on [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml) #### Custom Validators If you now receive an error of `'Grape::Validations.require_validator': unknown validator: your_custom_validation (Grape::Exceptions::UnknownValidator)` after upgrading to 2.4.0 then you will need to ensure that you require the `your_custom_validation` file before your Grape API code is loaded. See [2533](https://github.com/ruby-grape/grape/issues/2533) for more information. ### Upgrading to >= 2.3.0 ### `content_type` vs `api.format` inside API Before 2.3.0, `content_type` had priority over `env['api.format']` when set in an API, which was incorrect. The priority has been flipped and `env['api.format']` will be checked first. In addition, the function `api_format` has been added. Instead of setting `env['api.format']` directly, you can call `api_format`. See [#2506](https://github.com/ruby-grape/grape/pull/2506) for more information. #### Remove Deprecated Methods and Options - Deprecated `file` method has been removed. Use `send_file` or `stream`. See [#2500](https://github.com/ruby-grape/grape/pull/2500) for more information. - The `except` and `proc` options have been removed from the `values` validator. Use `except_values` validator or assign `proc` directly to `values`. See [#2501](https://github.com/ruby-grape/grape/pull/2501) for more information. - `Passing an options hash and a block to 'desc'` deprecation has been removed. Move all hash options to block instead. See [#2502](https://github.com/ruby-grape/grape/pull/2502) for more information. ### Upgrading to >= 2.2.0 ### `Length` validator After Grape 2.2.0, `length` validator will only take effect for parameters with types that support `#length` method, will not throw `ArgumentError` exception. See [#2464](https://github.com/ruby-grape/grape/pull/2464) for more information. ### Upgrading to >= 2.1.0 #### Optional Builder The `builder` gem dependency has been made optional as it's only used when generating XML. If your code does, add `builder` to your `Gemfile`. See [#2445](https://github.com/ruby-grape/grape/pull/2445) for more information. #### Deep Merging of Parameter Attributes Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level. With deep merge, attributes are now combined, allowing for more detailed and nuanced API specifications. For example: ```ruby with(documentation: { in: 'body' }) do optional :vault, documentation: { default: 33 } end ``` Before it was equivalent to: ```ruby optional :vault, documentation: { default: 33 } ``` After it is an equivalent of: ```ruby optional :vault, documentation: { in: 'body', default: 33 } ``` See [#2432](https://github.com/ruby-grape/grape/pull/2432) for more information. #### Zeitwerk Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk). If you MP (Monkey Patch) some files and you're not following the [file structure](https://github.com/fxn/zeitwerk?tab=readme-ov-file#file-structure), you might end up with a Zeitwerk error. See [#2363](https://github.com/ruby-grape/grape/pull/2363) for more information. #### Changes in rescue_from The `rack_response` method has been deprecated and the `error_response` method has been removed. Use `error!` instead. See [#2414](https://github.com/ruby-grape/grape/pull/2414) for more information. #### Change in parameters precedence When using together with `Grape::Extensions::Hash::ParamBuilder`, `route_param` takes higher precedence over a regular parameter defined with same name, which now matches the default param builder behavior. This was a regression introduced by [#2326](https://github.com/ruby-grape/grape/pull/2326) in Grape v1.8.0. ```ruby Grape.configure do |config| config.param_builder = Grape::Extensions::Hash::ParamBuilder end params do requires :foo, type: String end route_param :foo do get do { value: params[:foo] } end end ``` Request: ```bash curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}' ``` Response prior to v1.8.0: ```json { "value": "bar" } ``` v1.8.0..v2.0.0: ```json { "value": "baz" } ``` v2.1.0+: ```json { "value": "bar" } ``` See [#2378](https://github.com/ruby-grape/grape/pull/2378) for details. #### Grape::Router::Route.route_xxx methods have been removed - `route_method` is accessible through `request_method` - `route_path` is accessible through `path` - Any other `route_xyz` are accessible through `options[xyz]` #### Instance variables scope Due to the changes done in [#2377](https://github.com/ruby-grape/grape/pull/2377), the instance variables defined inside each of the endpoints (or inside a `before` validator) are now accessible inside the `rescue_from`. The behavior of the instance variables was undefined until `2.1.0`. If you were using the same variable name defined inside an endpoint or `before` validator inside a `rescue_from` handler, you need to take in mind that you can start getting different values or you can be overriding values. Before: ```ruby class TwitterAPI < Grape::API before do @var = 1 end get '/' do puts @var # => 1 raise end rescue_from :all do puts @var # => nil end end ``` After: ```ruby class TwitterAPI < Grape::API before do @var = 1 end get '/' do puts @var # => 1 raise end rescue_from :all do puts @var # => 1 end end ``` #### Recognizing Path Grape now considers the types of the configured `route_params` in order to determine the endpoint that matches with the performed request. So taking into account this `Grape::API` class ```ruby class Books < Grape::API resource :books do route_param :id, type: Integer do # GET /books/:id get do #... end end resource :share do # POST /books/share post do # .... end end end end ``` Before: ```ruby API.recognize_path '/books/1' # => /books/:id API.recognize_path '/books/share' # => /books/:id API.recognize_path '/books/other' # => /books/:id ``` After: ```ruby API.recognize_path '/books/1' # => /books/:id API.recognize_path '/books/share' # => /books/share API.recognize_path '/books/other' # => nil ``` This implies that before this changes, when you performed `/books/other` and it matched with the `/books/:id` endpoint, you get a `400 Bad Request` response because the type of the provided `:id` param was not an `Integer`. However, after upgrading to version `2.1.0` you will get a `404 Not Found` response, because there is not a defined endpoint that matches with `/books/other`. See [#2379](https://github.com/ruby-grape/grape/pull/2379) for more information. ### Upgrading to >= 2.0.0 #### Headers As per [rack/rack#1592](https://github.com/rack/rack/issues/1592) Rack 3 is following the HTTP/2+ semantics which require header names to be lower case. To avoid compatibility issues, starting with Grape 1.9.0, headers will be cased based on what version of Rack you are using. Given this request: ```shell curl -H "Content-Type: application/json" -H "Secret-Password: foo" ... ``` If you are using Rack 3 in your application then the headers will be set to: ```ruby { "content-type" => "application/json", "secret-password" => "foo"} ``` This means if you are checking for header values in your application, you would need to change your code to use downcased keys. ```ruby get do # This would use headers['Secret-Password'] in Rack < 3 error!('Unauthorized', 401) unless headers['secret-password'] == 'swordfish' end ``` See [#2355](https://github.com/ruby-grape/grape/pull/2355) for more information. #### Digest auth deprecation Digest auth has been removed along with the deprecation of `Rack::Auth::Digest` in Rack 3. See [#2294](https://github.com/ruby-grape/grape/issues/2294) for more information. ### Upgrading to >= 1.7.0 #### Exceptions renaming The following exceptions has been renamed for consistency through exceptions naming : * `MissingGroupTypeError` => `MissingGroupType` * `UnsupportedGroupTypeError` => `UnsupportedGroupType` See [#2227](https://github.com/ruby-grape/grape/pull/2227) for more information. #### Handling Multipart Limit Errors Rack supports a configurable limit on the number of files created from multipart parameters (`Rack::Utils.multipart_part_limit`) and raises an error if params are received that create too many files. If you were handling the Rack error directly, Grape now wraps that error in `Grape::Execeptions::TooManyMultipartFiles`. Additionally, Grape will return a 413 status code if the exception goes unhandled. ### Upgrading to >= 1.6.0 #### Parameter renaming with :as Prior to 1.6.0 the [parameter renaming](https://github.com/ruby-grape/grape#renaming) with `:as` was directly touching the request payload ([`#params`](https://github.com/ruby-grape/grape#parameters)) while duplicating the old and the new key to be both available in the hash. This allowed clients to bypass any validation in case they knew the internal name of the parameter. Unfortunately, in combination with [grape-swagger](https://github.com/ruby-grape/grape-swagger) the internal name (name set with `:as`) of the parameters were documented. This behavior was fixed. Parameter renaming is now done when using the [`#declared(params)`](https://github.com/ruby-grape/grape#declared) parameters helper. This stops confusing validation/coercion behavior. Here comes an illustration of the old and new behaviour as code: ```ruby # (1) Rename a to b, while client sends +a+ optional :a, type: Integer, as: :b params = { a: 1 } declared(params, include_missing: false) # expected => { b: 1 } # actual => { b: 1 } # (2) Rename a to b, while client sends +b+ optional :a, type: Integer, as: :b, values: [1, 2, 3] params = { b: '5' } declared(params, include_missing: false) # expected => { } (>= 1.6.0) # actual => { b: '5' } (uncasted, unvalidated, <= 1.5.3) ``` Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced a `Grape::Exceptions::UnknownParameter` because `:a` was replaced by `:b`: ```ruby params do optional :a, as: :b given :a do # (<= 1.5.3 you had to reference +:b+ here to make it work) requires :c end end ``` This code now works without any errors, as the renaming is just an internal behaviour of the `#declared(params)` parameter helper. See [#2189](https://github.com/ruby-grape/grape/pull/2189) for more information. ### Upgrading to >= 1.5.3 #### Nil value and coercion Prior to 1.2.5 version passing a `nil` value for a parameter with a custom coercer would invoke the coercer, and not passing a parameter would not invoke it. This behavior was not tested or documented. Version 1.3.0 quietly changed this behavior, in that `nil` values skipped the coercion. Version 1.5.3 fixes and documents this as follows: ```ruby class Api < Grape::API params do optional :value, type: Integer, coerce_with: ->(val) { val || 0 } end get 'example' do params[:my_param] end get '/example', params: { value: nil } # 1.5.2 = nil # 1.5.3 = 0 get '/example', params: {} # 1.5.2 = nil # 1.5.3 = nil end ``` See [#2164](https://github.com/ruby-grape/grape/pull/2164) for more information. ### Upgrading to >= 1.5.1 #### Dependent params If you use [dependent params](https://github.com/ruby-grape/grape#dependent-parameters) with `Grape::Extensions::Hash::ParamBuilder`, make sure a parameter to be dependent on is set as a Symbol. If a String is given, a parameter that other parameters depend on won't be found even if it is present. _Correct_: ```ruby given :matrix do # dependent params end ``` _Wrong_: ```ruby given 'matrix' do # dependent params end ``` ### Upgrading to >= 1.5.0 Prior to 1.3.3, the `declared` helper would always return the complete params structure if `include_missing=true` was set. In 1.3.3 a regression was introduced such that a missing Hash with or without nested parameters would always resolve to `{}`. In 1.5.0 this behavior is reverted, so the whole params structure will always be available via `declared`, regardless of whether any params are passed. The following rules now apply to the `declared` helper when params are missing and `include_missing=true`: * Hash params with children will resolve to a Hash with keys for each declared child. * Hash params with no children will resolve to `{}`. * Set params will resolve to `Set.new`. * Array params will resolve to `[]`. * All other params will resolve to `nil`. #### Example ```ruby class Api < Grape::API params do optional :outer, type: Hash do optional :inner, type: Hash do optional :value, type: String end end end get 'example' do declared(params, include_missing: true) end end ``` ``` get '/example' # 1.3.3 = {} # 1.5.0 = {outer: {inner: {value:null}}} ``` For more information see [#2103](https://github.com/ruby-grape/grape/pull/2103). ### Upgrading to >= 1.4.0 #### Reworking stream and file and un-deprecating stream like-objects Previously in 0.16 stream-like objects were deprecated. This release restores their functionality for use-cases other than file streaming. This release deprecated `file` in favor of `sendfile` to better document its purpose. To deliver a file via the Sendfile support in your web server and have the Rack::Sendfile middleware enabled. See [`Rack::Sendfile`](https://www.rubydoc.info/gems/rack/Rack/Sendfile). ```ruby class API < Grape::API get '/' do sendfile '/path/to/file' end end ``` Use `stream` to stream file content in chunks. ```ruby class API < Grape::API get '/' do stream '/path/to/file' end end ``` Or use `stream` to stream other kinds of content. In the following example a streamer class streams paginated data from a database. ```ruby class MyObject attr_accessor :result def initialize(query) @result = query end def each yield '[' # Do paginated DB fetches and return each page formatted first = false result.find_in_batches do |records| yield process_records(records, first) first = false end yield ']' end def process_records(records, first) buffer = +'' buffer << ',' unless first buffer << records.map(&:to_json).join(',') buffer end end class API < Grape::API get '/' do stream MyObject.new(Sprocket.all) end end ``` ### Upgrading to >= 1.3.3 #### Nil values for structures Nil values have always been a special case when dealing with types, especially with the following structures: - Array - Hash - Set The behavior for these structures has changed throughout the latest releases. For example: ```ruby class Api < Grape::API params do require :my_param, type: Array[Integer] end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } # 1.3.1 = [] # 1.3.2 = nil end ``` For now on, `nil` values stay `nil` values for all types, including arrays, sets and hashes. If you want to have the same behavior as 1.3.1, apply a `default` validator: ```ruby class Api < Grape::API params do require :my_param, type: Array[Integer], default: [] end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } # => [] end ``` #### Default validator Default validator is now applied for `nil` values. ```ruby class Api < Grape::API params do requires :my_param, type: Integer, default: 0 end get 'example' do params[:my_param] end get '/example', params: { my_param: nil } #=> before: nil, after: 0 end ``` ### Upgrading to >= 1.3.0 You will need to upgrade to this version if you depend on `rack >= 2.1.0`. #### Ruby After adding dry-types, Ruby 2.4 or newer is required. #### Coercion [Virtus](https://github.com/solnic/virtus) has been replaced by [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter coercion. If your project depends on Virtus outside of Grape, explicitly add it to your `Gemfile`. Here's an example of how to migrate a custom type from Virtus to dry-types: ```ruby # Legacy Grape parser class SecureUriType < Virtus::Attribute def coerce(input) URI.parse value end def value_coerced?(input) value.is_a? String end end params do requires :secure_uri, type: SecureUri end ``` To use dry-types, we need to: 1. Remove the inheritance of `Virtus::Attribute` 1. Rename `coerce` to `self.parse` 1. Rename `value_coerced?` to `self.parsed?` The custom type must have a class-level `parse` method to the model. A class-level `parsed?` is needed if the parsed type differs from the defined type. In the example below, since `SecureUri` is not the same as `URI::HTTPS`, `self.parsed?` is needed: ```ruby # New dry-types parser class SecureUri def self.parse(value) URI.parse value end def self.parsed?(value) value.is_a? URI::HTTPS end end params do requires :secure_uri, type: SecureUri end ``` #### Coercing to `FalseClass` or `TrueClass` no longer works Previous Grape versions allowed this, though it wasn't documented: ```ruby requires :true_value, type: TrueClass requires :bool_value, types: [FalseClass, TrueClass] ``` This is no longer supported, if you do this, your values will never be valid. Instead you should do this: ```ruby requires :true_value, type: Boolean # in your endpoint you should validate if this is actually `true` requires :bool_value, type: Boolean ``` #### Ensure that Array types have explicit coercions Unlike Virtus, dry-types does not perform any implict coercions. If you have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they use a `coerce_with` block. For example: ```ruby requires :values, type: Array[String] ``` It's quite common to pass a comma-separated list, such as `tag1,tag2` as `values`. Previously Virtus would implicitly coerce this to `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but with `dry-types` the values are no longer coerced for you. To fix this, you might do: ```ruby requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) } ``` Likewise, for `Array[Integer]`, you might do: ```ruby requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(',').map(&:strip).map(&:to_i) } ``` For more information see [#1920](https://github.com/ruby-grape/grape/pull/1920). ### Upgrading to >= 1.2.4 #### Headers in `error!` call Headers in `error!` will be merged with `headers` hash. If any header need to be cleared on `error!` call, make sure to move it to the `after` block. ```ruby class SampleApi < Grape::API before do header 'X-Before-Header', 'before_call' end get 'ping' do header 'X-App-Header', 'on_call' error! :pong, 400, 'X-Error-Details' => 'Invalid token' end end ``` **Former behaviour** ```ruby response.headers['X-Before-Header'] # => nil response.headers['X-App-Header'] # => nil response.headers['X-Error-Details'] # => Invalid token ``` **Current behaviour** ```ruby response.headers['X-Before-Header'] # => 'before_call' response.headers['X-App-Header'] # => 'on_call' response.headers['X-Error-Details'] # => Invalid token ``` ### Upgrading to >= 1.2.1 #### Obtaining the name of a mounted class In order to make obtaining the name of a mounted class simpler, we've delegated `.to_s` to `base.name` **Deprecated in 1.2.0** ```ruby payload[:endpoint].options[:for].name ``` **New** ```ruby payload[:endpoint].options[:for].to_s ``` ### Upgrading to >= 1.2.0 #### Changes in the Grape::API class ##### Patching the class In an effort to make APIs re-mountable, The class `Grape::API` no longer refers to an API instance, rather, what used to be `Grape::API` is now `Grape::API::Instance` and `Grape::API` was replaced with a class that can contain several instances of `Grape::API`. This changes were done in such a way that no code-changes should be required. However, if experiencing problems, or relying on private methods and internal behaviour too deeply, it is possible to restore the prior behaviour by replacing the references from `Grape::API` to `Grape::API::Instance`. Note, this is particularly relevant if you are opening the class `Grape::API` for modification. **Deprecated** ```ruby class Grape::API # your patched logic ... end ``` **New** ```ruby class Grape::API::Instance # your patched logic ... end ``` ##### `name` (and other caveats) of the mounted API After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class which inherit from `Grape::API::Instance`. What this means in practice, is: - Generally: you can access the named class from the instance calling the getter `base`. - In particular: If you need the `name`, you can use `base`.`name`. **Deprecated** ```ruby payload[:endpoint].options[:for].name ``` **New** ```ruby payload[:endpoint].options[:for].base.name ``` #### Changes in rescue_from returned object Grape will now check the object returned from `rescue_from` and ensure that it is a `Rack::Response`. That makes sure response is valid and avoids exposing service information. Change any code that invoked `Rack::Response.new(...).finish` in a custom `rescue_from` block to `Rack::Response.new(...)` to comply with the validation. ```ruby class Twitter::API < Grape::API rescue_from :all do |e| # version prior to 1.2.0 Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }).finish # 1.2.0 version Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }) end end ``` See [#1757](https://github.com/ruby-grape/grape/pull/1757) and [#1776](https://github.com/ruby-grape/grape/pull/1776) for more information. ### Upgrading to >= 1.1.0 #### Changes in HTTP Response Code for Unsupported Content Type For PUT, POST, PATCH, and DELETE requests where a non-empty body and a "Content-Type" header is supplied that is not supported by the Grape API, Grape will no longer return a 406 "Not Acceptable" HTTP status code and will instead return a 415 "Unsupported Media Type" so that the usage of HTTP status code falls more in line with the specification of [RFC 2616](https://www.ietf.org/rfc/rfc2616.txt). ### Upgrading to >= 1.0.0 #### Changes in XML and JSON Parsers Grape no longer uses `multi_json` or `multi_xml` by default and uses `JSON` and `ActiveSupport::XmlMini` instead. This has no visible impact on JSON processing, but the default behavior of the XML parser has changed. For example, an XML POST containing `Bobby T.` was parsed as `Bobby T.` with `multi_xml`, and as now parsed as `{"__content__"=>"Bobby T."}` with `XmlMini`. If you were using `MultiJson.load`, `MultiJson.dump` or `MultiXml.parse`, you can substitute those with `Grape::Json.load`, `Grape::Json.dump`, `::Grape::Xml.parse`, or directly with `JSON.load`, `JSON.dump`, `XmlMini.parse`, etc. To restore previous behavior, add `multi_json` or `multi_xml` to your `Gemfile` and `require` it. See [#1623](https://github.com/ruby-grape/grape/pull/1623) for more information. #### Changes in Parameter Class The default class for `params` has changed from `Hashie::Mash` to `ActiveSupport::HashWithIndifferentAccess` and the `hashie` dependency has been removed. This means that by default you can no longer access parameters by method name. ```ruby class API < Grape::API params do optional :color, type: String end get do params[:color] # use params[:color] instead of params.color end end ``` To restore the behavior of prior versions, add `hashie` to your `Gemfile` and `include Grape::Extensions::Hashie::Mash::ParamBuilder` in your API. ```ruby class API < Grape::API include Grape::Extensions::Hashie::Mash::ParamBuilder params do optional :color, type: String end get do # params.color works end end ``` This behavior can also be overridden on individual parameter blocks using `build_with`. ```ruby params do build_with Grape::Extensions::Hash::ParamBuilder optional :color, type: String end ``` If you're constructing your own `Grape::Request` in a middleware, you can pass different parameter handlers to create the desired `params` class with `build_params_with`. ```ruby def request Grape::Request.new(env, build_params_with: Grape::Extensions::Hashie::Mash::ParamBuilder) end ``` See [#1610](https://github.com/ruby-grape/grape/pull/1610) for more information. #### The `except`, `except_message`, and `proc` options of the `values` validator are deprecated. The new `except_values` validator should be used in place of the `except` and `except_message` options of the `values` validator. Arity one Procs may now be used directly as the `values` option to explicitly test param values. **Deprecated** ```ruby params do requires :a, values: { value: 0..99, except: [3] } requires :b, values: { value: 0..99, except: [3], except_message: 'not allowed' } requires :c, values: { except: ['admin'] } requires :d, values: { proc: -> (v) { v.even? } } end ``` **New** ```ruby params do requires :a, values: 0..99, except_values: [3] requires :b, values: 0..99, except_values: { value: [3], message: 'not allowed' } requires :c, except_values: ['admin'] requires :d, values: -> (v) { v.even? } end ``` See [#1616](https://github.com/ruby-grape/grape/pull/1616) for more information. ### Upgrading to >= 0.19.1 #### DELETE now defaults to status code 200 for responses with a body, or 204 otherwise Prior to this version, DELETE requests defaulted to a status code of 204 No Content, even when the response included content. This behavior confused some clients and prevented the formatter middleware from running properly. As of this version, DELETE requests will only default to a 204 No Content status code if no response body is provided, and will default to 200 OK otherwise. Specifically, DELETE behaviour has changed as follows: - In versions < 0.19.0, all DELETE requests defaulted to a 200 OK status code. - In version 0.19.0, all DELETE requests defaulted to a 204 No Content status code, even when content was included in the response. - As of version 0.19.1, DELETE requests default to a 204 No Content status code, unless content is supplied, in which case they default to a 200 OK status code. To achieve the old behavior, one can specify the status code explicitly: ```ruby delete :id do status 204 # or 200, for < 0.19.0 behavior 'foo successfully deleted' end ``` One can also use the new `return_no_content` helper to explicitly return a 204 status code and an empty body for any request type: ```ruby delete :id do return_no_content 'this will not be returned' end ``` See [#1550](https://github.com/ruby-grape/grape/pull/1550) for more information. ### Upgrading to >= 0.18.1 #### Changes in priority of :any routes Prior to this version, `:any` routes were searched after matching first route and 405 routes. This behavior has changed and `:any` routes are now searched before 405 processing. In the following example the `:any` route will match first when making a request with an unsupported verb. ```ruby post :example do 'example' end route :any, '*path' do error! :not_found, 404 end get '/example' #=> before: 405, after: 404 ``` #### Removed param processing from built-in OPTIONS handler When a request is made to the built-in `OPTIONS` handler, only the `before` and `after` callbacks associated with the resource will be run. The `before_validation` and `after_validation` callbacks and parameter validations will be skipped. See [#1505](https://github.com/ruby-grape/grape/pull/1505) for more information. #### Changed endpoint params validation Grape now correctly returns validation errors for all params when multiple params are passed to a requires. The following code will return `one is missing, two is missing` when calling the endpoint without parameters. ```ruby params do requires :one, :two end ``` Prior to this version the response would be `one is missing`. See [#1510](https://github.com/ruby-grape/grape/pull/1510) for more information. #### The default status code for DELETE is now 204 instead of 200. Breaking change: Sets the default response status code for a delete request to 204. A status of 204 makes the response more distinguishable and therefore easier to handle on the client side, particularly because a DELETE request typically returns an empty body as the resource was deleted or voided. To achieve the old behavior, one has to set it explicitly: ```ruby delete :id do status 200 'foo successfully deleted' end ``` For more information see: [#1532](https://github.com/ruby-grape/grape/pull/1532). ### Upgrading to >= 0.17.0 #### Removed official support for Ruby < 2.2.2 Grape is no longer automatically tested against versions of Ruby prior to 2.2.2. This is because of its dependency on activesupport which, with version 5.0.0, now requires at least Ruby 2.2.2. See [#1441](https://github.com/ruby-grape/grape/pull/1441) for nmore information. #### Changed priority of `rescue_from` clauses applying The `rescue_from` clauses declared inside a namespace would take a priority over ones declared in the root scope. This could possibly affect those users who use different `rescue_from` clauses in root scope and in namespaces. See [#1405](https://github.com/ruby-grape/grape/pull/1405) for more information. #### Helper methods injected inside `rescue_from` in middleware Helper methods are injected inside `rescue_from` may cause undesirable effects. For example, definining a helper method called `error!` will take precendence over the built-in `error!` method and should be renamed. See [#1451](https://github.com/ruby-grape/grape/issues/1451) for an example. ### Upgrading to >= 0.16.0 #### Replace rack-mount with new router The `Route#route_xyz` methods have been deprecated since 0.15.1. Please use `Route#xyz` instead. Note that the `Route#route_method` was replaced by `Route#request_method`. The following code would work correctly. ```ruby TwitterAPI::versions # yields [ 'v1', 'v2' ] TwitterAPI::routes # yields an array of Grape::Route objects TwitterAPI::routes[0].version # => 'v1' TwitterAPI::routes[0].description # => 'Includes custom settings.' TwitterAPI::routes[0].settings[:custom] # => { key: 'value' } TwitterAPI::routes[0].request_method # => 'GET' ``` #### `file` method accepts path to file Now to serve files via Grape just pass the path to the file. Functionality with FileStreamer-like objects is deprecated. Please, replace your FileStreamer-like objects with paths of served files. Old style: ```ruby class FileStreamer def initialize(file_path) @file_path = file_path end def each(&blk) File.open(@file_path, 'rb') do |file| file.each(10, &blk) end end end # ... class API < Grape::API get '/' do file FileStreamer.new('/path/to/file') end end ``` New style: ```ruby class API < Grape::API get '/' do file '/path/to/file' end end ``` ### Upgrading to >= 0.15.0 #### Changes to availability of `:with` option of `rescue_from` method The `:with` option of `rescue_from` does not accept value except Proc, String or Symbol now. If you have been depending the old behavior, you should use lambda block instead. ```ruby class API < Grape::API rescue_from :all, with: -> { Rack::Response.new('rescued with a method', 400) } end ``` #### Changes to behavior of `after` method of middleware on error The `after` method of the middleware is now also called on error. The following code would work correctly. ```ruby class ErrorMiddleware < Grape::Middleware::Base def after return unless @app_response && @app_response[0] == 500 env['rack.logger'].debug("Raised error on #{env['PATH_INFO']}") end end ``` See [#1147](https://github.com/ruby-grape/grape/issues/1147) and [#1240](https://github.com/ruby-grape/grape/issues/1240) for discussion of the issues. A warning will be logged if an exception is raised in an `after` callback, which points you to middleware that was not called in the previous version and is called now. ``` caught error of type NoMethodError in after callback inside Api::Middleware::SomeMiddleware : undefined method `headers' for nil:NilClass ``` See [#1285](https://github.com/ruby-grape/grape/pull/1285) for more information. #### Changes to Method Not Allowed routes A `405 Method Not Allowed` error now causes `Grape::Exceptions::MethodNotAllowed` to be raised, which will be rescued via `rescue_from :all`. Restore old behavior with the following error handler. ```ruby rescue_from Grape::Exceptions::MethodNotAllowed do |e| error! e.message, e.status, e.headers end ``` See [#1283](https://github.com/ruby-grape/grape/pull/1283) for more information. #### Changes to Grape::Exceptions::Validation parameters When raising `Grape::Exceptions::Validation` explicitly, replace `message_key` with `message`. For example, ```ruby fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message_key: :presence ``` becomes ```ruby fail Grape::Exceptions::Validation, params: [:oauth_token_secret], message: :presence ``` See [#1295](https://github.com/ruby-grape/grape/pull/1295) for more information. ### Upgrading to >= 0.14.0 #### Changes to availability of DSL methods in filters The `#declared` method of the route DSL is no longer available in the `before` filter. Using `declared` in a `before` filter will now raise `Grape::DSL::InsideRoute::MethodNotYetAvailable`. See [#1074](https://github.com/ruby-grape/grape/issues/1074) for discussion of the issue. #### Changes to header versioning and invalid header version handling Identical endpoints with different versions now work correctly. A regression introduced in Grape 0.11.0 caused all but the first-mounted version for such an endpoint to wrongly throw an `InvalidAcceptHeader`. As a side effect, requests with a correct vendor but invalid version can no longer be rescued from a `rescue_from` block. See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information. #### Bypasses formatters when status code indicates no content To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567) See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information. #### Redirects respond as plain text with message `#redirect` now uses `text/plain` regardless of whether that format has been enabled. This prevents formatters from attempting to serialize the message body and allows for a descriptive message body to be provided - and optionally overridden - that better fulfills the theme of the HTTP spec. See [#1194](https://github.com/ruby-grape/grape/pull/1194) for more information. ### Upgrading to >= 0.12.0 #### Changes in middleware The Rack response object is no longer converted to an array by the formatter, enabling streaming. If your custom middleware is accessing `@app_response`, update it to expect a `Rack::Response` instance instead of an array. For example, ```ruby class CacheBusterMiddleware < Grape::Middleware::Base def after @app_response[1]['Expires'] = Time.at(0).utc.to_s @app_response end end ``` becomes ```ruby class CacheBusterMiddleware < Grape::Middleware::Base def after @app_response.headers['Expires'] = Time.at(0).utc.to_s @app_response end end ``` See [#1029](https://github.com/ruby-grape/grape/pull/1029) for more information. There is a known issue because of this change. When Grape is used with an older than 1.2.4 version of [warden](https://github.com/hassox/warden) there may be raised the following exception having the [rack-mount](https://github.com/jm/rack-mount) gem's lines as last ones in the backtrace: ``` NoMethodError: undefined method `[]' for nil:NilClass ``` The issue can be solved by upgrading warden to 1.2.4 version. See [#1151](https://github.com/ruby-grape/grape/issues/1151) for more information. #### Changes in present Using `present` with objects that responded to `merge` would cause early evaluation of the represented object, with unexpected side-effects, such as missing parameters or environment within rendering code. Grape now only merges represented objects with a previously rendered body, usually when multiple `present` calls are made in the same route. See [grape-with-roar#5](https://github.com/dblock/grape-with-roar/issues/5) and [#1023](https://github.com/ruby-grape/grape/issues/1023). #### Changes to regexp validator Parameters with `nil` value will now pass `regexp` validation. To disallow `nil` value for an endpoint, add `allow_blank: false`. ```ruby params do requires :email, allow_blank: false, regexp: /.+@.+/ end ``` See [#957](https://github.com/ruby-grape/grape/pull/957) for more information. #### Replace error_response with error! in rescue_from blocks Note: `error_response` is being deprecated, not removed. ```ruby def error!(message, status = options[:default_status], headers = {}, backtrace = []) headers = { 'Content-Type' => content_type }.merge(headers) rack_response(format_message(message, backtrace), status, headers) end ``` For example, ``` error_response({ message: { message: 'No such page.', id: 'missing_page' }, status: 404, headers: { 'Content-Type' => 'api/error' }) ``` becomes ``` error!({ message: 'No such page.', id: 'missing_page' }, 404, { 'Content-Type' => 'api/error' }) ``` `error!` also supports just passing a message. `error!('Server error.')` and `format: :json` returns the following JSON response ``` { 'error': 'Server error.' } ``` with a status code of 500 and a Content Type of text/error. Optionally, also replace `Rack::Response.new` with `error!.` The following are equivalent: ``` Rack::Response.new([ e.message ], 500, { "Content-type" => "text/error" }).finish error!(e) ``` See [#889](https://github.com/ruby-grape/grape/issues/889) for more information. #### Changes to routes when using `format` Version 0.10.0 has introduced a change via [#809](https://github.com/ruby-grape/grape/pull/809) whereas routes no longer got file-type suffixes added if you declared a single API `format`. This has been reverted, it's now again possible to call API with proper suffix when single `format` is defined: ```ruby class API < Grape::API format :json get :hello do { hello: 'world' } end end ``` Will respond with JSON to `/hello` **and** `/hello.json`. Will respond with 404 to `/hello.xml`, `/hello.txt` etc. See the [#1001](https://github.com/ruby-grape/grape/pull/1001) and [#914](https://github.com/ruby-grape/grape/issues/914) for more info. ### Upgrading to >= 0.11.0 #### Added Rack 1.6.0 support Grape now supports, but doesn't require Rack 1.6.0. If you encounter an issue with parsing requests larger than 128KB, explictly require Rack 1.6.0 in your Gemfile. ```ruby gem 'rack', '~> 1.6.0' ``` See [#559](https://github.com/ruby-grape/grape/issues/559) for more information. #### Removed route_info Key route_info is excluded from params. See [#879](https://github.com/ruby-grape/grape/pull/879) for more information. #### Fix callbacks within a version block Callbacks defined in a version block are only called for the routes defined in that block. This was a regression introduced in Grape 0.10.0, and is fixed in this version. See [#901](https://github.com/ruby-grape/grape/pull/901) for more information. #### Make type of group of parameters required Groups of parameters now require their type to be set explicitly as Array or Hash. Not setting the type now results in MissingGroupTypeError, unsupported type will raise UnsupportedTypeError. See [#886](https://github.com/ruby-grape/grape/pull/886) for more information. ### Upgrading to >= 0.10.1 #### Changes to `declared(params, include_missing: false)` Attributes with `nil` values or with values that evaluate to `false` are no longer considered *missing* and will be returned when `include_missing` is set to `false`. See [#864](https://github.com/ruby-grape/grape/pull/864) for more information. ### Upgrading to >= 0.10.0 #### Changes to content-types The following content-types have been removed: * atom (application/atom+xml) * rss (application/rss+xml) * jsonapi (application/jsonapi) This is because they have never been properly supported. #### Changes to desc New block syntax: Former: ```ruby desc "some descs", detail: 'more details', entity: API::Entities::Entity, params: API::Entities::Status.documentation, named: 'a name', headers: [XAuthToken: { description: 'Valdates your identity', required: true } get nil, http_codes: [ [401, 'Unauthorized', API::Entities::BaseError], [404, 'not found', API::Entities::Error] ] do ``` Now: ```ruby desc "some descs" do detail 'more details' params API::Entities::Status.documentation success API::Entities::Entity failure [ [401, 'Unauthorized', API::Entities::BaseError], [404, 'not found', API::Entities::Error] ] named 'a name' headers [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ] end ``` #### Changes to Route Options and Descriptions A common hack to extend Grape with custom DSL methods was manipulating `@last_description`. ``` ruby module Grape module Extensions module SortExtension def sort(value) @last_description ||= {} @last_description[:sort] ||= {} @last_description[:sort].merge! value value end end Grape::API.extend self end end ``` You could access this value from within the API with `route.route_sort` or, more generally, via `env['api.endpoint'].options[:route_options][:sort]`. This will no longer work, use the documented and supported `route_setting`. ``` ruby module Grape module Extensions module SortExtension def sort(value) route_setting :sort, sort: value value end end Grape::API.extend self end end ``` To retrieve this value at runtime from within an API, use `env['api.endpoint'].route_setting(:sort)` and when introspecting a mounted API, use `route.route_settings[:sort]`. #### Accessing Class Variables from Helpers It used to be possible to fetch an API class variable from a helper function. For example: ```ruby @@static_variable = 42 helpers do def get_static_variable @@static_variable end end get do get_static_variable end ``` This will no longer work. Use a class method instead of a helper. ```ruby @@static_variable = 42 def self.get_static_variable @@static_variable end get do get_static_variable end ``` For more information see [#836](https://github.com/ruby-grape/grape/issues/836). #### Changes to Custom Validators To implement a custom validator, you need to inherit from `Grape::Validations::Base` instead of `Grape::Validations::Validator`. For more information see [Custom Validators](https://github.com/ruby-grape/grape#custom-validators) in the documentation. #### Changes to Raising Grape::Exceptions::Validation In previous versions raising `Grape::Exceptions::Validation` required a single `param`. ```ruby raise Grape::Exceptions::Validation, param: :id, message_key: :presence ``` The `param` argument has been deprecated and is now an array of `params`, accepting multiple values. ```ruby raise Grape::Exceptions::Validation, params: [:id], message_key: :presence ``` #### Changes to routes when using `format` Routes will no longer get file-type suffixes added if you declare a single API `format`. For example, ```ruby class API < Grape::API format :json get :hello do { hello: 'world' } end end ``` Pre-0.10.0, this would respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc. Now, this will only respond with JSON to `/hello`, but will be a 404 when trying to access `/hello.json`, `/hello.xml`, `/hello.txt`, etc. If you declare further `content_type`s, this behavior will be circumvented. For example, the following API will respond with JSON to `/hello`, `/hello.json`, `/hello.xml`, `/hello.txt`, etc. ```ruby class API < Grape::API format :json content_type :json, 'application/json' get :hello do { hello: 'world' } end end ``` See the [the updated API Formats documentation](https://github.com/ruby-grape/grape#api-formats) and [#809](https://github.com/ruby-grape/grape/pull/809) for more info. #### Changes to Evaluation of Permitted Parameter Values Permitted and default parameter values are now only evaluated lazily for each request when declared as a proc. The following code would raise an error at startup time. ```ruby params do optional :v, values: -> { [:x, :y] }, default: -> { :z } end ``` Remove the proc to get the previous behavior. ```ruby params do optional :v, values: [:x, :y], default: :z end ``` See [#801](https://github.com/ruby-grape/grape/issues/801) for more information. #### Changes to version If version is used with a block, the callbacks defined within that version block are not scoped to that individual block. In other words, the callback would be inherited by all versions blocks that follow the first one e.g ```ruby class API < Grape::API resource :foo do version 'v1', :using => :path do before do @output ||= 'hello1' end get '/' do @output += '-v1' end end version 'v2', :using => :path do before do @output ||= 'hello2' end get '/:id' do @output += '-v2' end end end end ``` when making a API call `GET /foo/v2/1`, the API would set instance variable `@output` to `hello1-v2` See [#898](https://github.com/ruby-grape/grape/issues/898) for more information. ### Upgrading to >= 0.9.0 #### Changes in Authentication The following middleware classes have been removed: * `Grape::Middleware::Auth::Basic` * `Grape::Middleware::Auth::Digest` * `Grape::Middleware::Auth::OAuth2` When you use theses classes directly like: ```ruby module API class Root < Grape::API class Protected < Grape::API use Grape::Middleware::Auth::OAuth2, token_class: 'AccessToken', parameter: %w(access_token api_key) ``` you have to replace these classes. As replacement can be used * `Grape::Middleware::Auth::Basic` => [`Rack::Auth::Basic`](https://github.com/rack/rack/blob/master/lib/rack/auth/basic.rb) * `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb) * `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) and host these files within your application See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information. ### Upgrading to >= 0.7.0 #### Changes in Exception Handling Assume you have the following exception classes defined. ```ruby class ParentError < StandardError; end class ChildError < ParentError; end ``` In Grape <= 0.6.1, the `rescue_from` keyword only handled the exact exception being raised. The following code would rescue `ParentError`, but not `ChildError`. ```ruby rescue_from ParentError do |e| # only rescue ParentError end ``` This made it impossible to rescue an exception hieararchy, which is a more sensible default. In Grape 0.7.0 or newer, both `ParentError` and `ChildError` are rescued. ```ruby rescue_from ParentError do |e| # rescue both ParentError and ChildError end ``` To only rescue the base exception class, set `rescue_subclasses: false`. ```ruby rescue_from ParentError, rescue_subclasses: false do |e| # only rescue ParentError end ``` See [#544](https://github.com/ruby-grape/grape/pull/544) for more information. #### Changes in the Default HTTP Status Code In Grape <= 0.6.1, the default status code returned from `error!` was 403. ```ruby error! "You may not reticulate this spline!" # yields HTTP error 403 ``` This was a bad default value, since 403 means "Forbidden". Change any call to `error!` that does not specify a status code to specify one. The new default value is a more sensible default of 500, which is "Internal Server Error". ```ruby error! "You may not reticulate this spline!", 403 # yields HTTP error 403 ``` You may also use `default_error_status` to change the global default. ```ruby default_error_status 400 ``` See [#525](https://github.com/ruby-grape/Grape/pull/525) for more information. #### Changes in Parameter Declaration and Validation In Grape <= 0.6.1, `group`, `optional` and `requires` keywords with a block accepted either an `Array` or a `Hash`. ```ruby params do requires :id, type: Integer group :name do requires :first_name requires :last_name end end ``` This caused the ambiguity and unexpected errors described in [#543](https://github.com/ruby-grape/Grape/issues/543). In Grape 0.7.0, the `group`, `optional` and `requires` keywords take an additional `type` attribute which defaults to `Array`. This means that without a `type` attribute, these nested parameters will no longer accept a single hash, only an array (of hashes). Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.7.0. ```json { "id": 1, "name": { "first_name": "John", "last_name" : "Doe" } } ``` The `params` block should now read as follows. ```ruby params do requires :id, type: Integer requires :name, type: Hash do requires :first_name requires :last_name end end ``` See [#545](https://github.com/ruby-grape/Grape/pull/545) for more information. ### Upgrading to 0.6.0 In Grape <= 0.5.0, only the first validation error was raised and processing aborted. Validation errors are now collected and a single `Grape::Exceptions::ValidationErrors` exception is raised. You can access the collection of validation errors as `.errors`. ```ruby rescue_from Grape::Exceptions::Validations do |e| Rack::Response.new({ status: 422, message: e.message, errors: e.errors }.to_json, 422) end ``` For more information see [#462](https://github.com/ruby-grape/grape/issues/462). ================================================ FILE: benchmark/compile_many_routes.rb ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/ips' class API < Grape::API prefix :api version 'v1', using: :path 2000.times do |index| get "/test#{index}/" do 'hello' end end end Benchmark.ips do |ips| ips.report('Compiling 2000 routes') do API.compile! end end ================================================ FILE: benchmark/issue_mounting.rb ================================================ # frozen_string_literal: true require 'bundler/inline' gemfile(true) do source 'https://rubygems.org' gem 'grape' gem 'rack' gem 'minitest' gem 'rack-test' end require 'minitest/autorun' require 'rack/test' require 'grape' class GrapeAPIBugTest < Minitest::Test include Rack::Test::Methods RootAPI = Class.new(Grape::API) do format :json delete :test do status 200 [] end end def test_v1_users_via_api env = Rack::MockRequest.env_for('/test', method: Rack::DELETE) response = Rack::MockResponse[*RootAPI.call(env)] assert_equal '[]', response.body assert_equal 200, response.status end end ================================================ FILE: benchmark/large_model.rb ================================================ # frozen_string_literal: true # gem 'grape', '=1.0.1' require 'grape' require 'ruby-prof' require 'hashie' class API < Grape::API # include Grape::Extensions::Hash::ParamBuilder # include Grape::Extensions::Hashie::Mash::ParamBuilder rescue_from do |e| warn "\n\n#{e.class} (#{e.message}):\n #{e.backtrace.join("\n ")}\n\n" end prefix :api version 'v1', using: :path content_type :json, 'application/json; charset=UTF-8' default_format :json def self.vrp_request_timewindow(this) this.optional(:id, types: String) this.optional(:start, types: [String, Float, Integer]) this.optional(:end, types: [String, Float, Integer]) this.optional(:day_index, type: Integer, values: 0..6) this.at_least_one_of :start, :end, :day_index end def self.vrp_request_indice_range(this) this.optional(:start, type: Integer) this.optional(:end, type: Integer) end def self.vrp_request_point(this) this.requires(:id, type: String, allow_blank: false) this.optional(:location, type: Hash, allow_blank: false) do requires(:lat, type: Float, allow_blank: false) requires(:lon, type: Float, allow_blank: false) end end def self.vrp_request_unit(this) this.requires(:id, type: String, allow_blank: false) this.optional(:label, type: String) this.optional(:counting, type: Boolean) end def self.vrp_request_activity(this) this.optional(:duration, types: [String, Float, Integer]) this.optional(:additional_value, type: Integer) this.optional(:setup_duration, types: [String, Float, Integer]) this.optional(:late_multiplier, type: Float) this.optional(:timewindow_start_day_shift_number, documentation: { hidden: true }, type: Integer) this.requires(:point_id, type: String, allow_blank: false) this.optional(:timewindows, type: Array) do API.vrp_request_timewindow(self) end end def self.vrp_request_quantity(this) this.optional(:id, type: String) this.requires(:unit_id, type: String, allow_blank: false) this.optional(:value, type: Float) end def self.vrp_request_capacity(this) this.optional(:id, type: String) this.requires(:unit_id, type: String, allow_blank: false) this.requires(:limit, type: Float, allow_blank: false) this.optional(:initial, type: Float) this.optional(:overload_multiplier, type: Float) end def self.vrp_request_vehicle(this) this.requires(:id, type: String, allow_blank: false) this.optional(:cost_fixed, type: Float) this.optional(:cost_distance_multiplier, type: Float) this.optional(:cost_time_multiplier, type: Float) this.optional :router_dimension, type: String, values: %w[time distance] this.optional(:skills, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(',').map(&:strip)] : val }) this.optional(:unavailable_work_day_indices, type: Array[Integer]) this.optional(:free_approach, type: Boolean) this.optional(:free_return, type: Boolean) this.optional(:start_point_id, type: String) this.optional(:end_point_id, type: String) this.optional(:capacities, type: Array) do API.vrp_request_capacity(self) end this.optional(:sequence_timewindows, type: Array) do API.vrp_request_timewindow(self) end end def self.vrp_request_service(this) this.requires(:id, type: String, allow_blank: false) this.optional(:priority, type: Integer, values: 0..8) this.optional(:exclusion_cost, type: Integer) this.optional(:visits_number, type: Integer, coerce_with: ->(val) { val.to_i.positive? && val.to_i }, default: 1, allow_blank: false) this.optional(:unavailable_visit_indices, type: Array[Integer]) this.optional(:unavailable_visit_day_indices, type: Array[Integer]) this.optional(:minimum_lapse, type: Float) this.optional(:maximum_lapse, type: Float) this.optional(:sticky_vehicle_ids, type: Array[String]) this.optional(:skills, type: Array[String]) this.optional(:type, type: Symbol) this.optional(:activity, type: Hash) do API.vrp_request_activity(self) end this.optional(:quantities, type: Array) do API.vrp_request_quantity(self) end end def self.vrp_request_configuration(this) this.optional(:preprocessing, type: Hash) do API.vrp_request_preprocessing(self) end this.optional(:resolution, type: Hash) do API.vrp_request_resolution(self) end this.optional(:restitution, type: Hash) do API.vrp_request_restitution(self) end this.optional(:schedule, type: Hash) do API.vrp_request_schedule(self) end end def self.vrp_request_partition(this) this.requires(:method, type: String, values: %w[hierarchical_tree balanced_kmeans]) this.optional(:metric, type: Symbol) this.optional(:entity, type: Symbol, values: %i[vehicle work_day], coerce_with: lambda(&:to_sym)) this.optional(:threshold, type: Integer) end def self.vrp_request_preprocessing(this) this.optional(:max_split_size, type: Integer) this.optional(:partition_method, type: String, documentation: { hidden: true }) this.optional(:partition_metric, type: Symbol, documentation: { hidden: true }) this.optional(:kmeans_centroids, type: Array[Integer]) this.optional(:cluster_threshold, type: Float) this.optional(:force_cluster, type: Boolean) this.optional(:prefer_short_segment, type: Boolean) this.optional(:neighbourhood_size, type: Integer) this.optional(:partitions, type: Array) do API.vrp_request_partition(self) end this.optional(:first_solution_strategy, type: Array[String]) end def self.vrp_request_resolution(this) this.optional(:duration, type: Integer, allow_blank: false) this.optional(:iterations, type: Integer, allow_blank: false) this.optional(:iterations_without_improvment, type: Integer, allow_blank: false) this.optional(:stable_iterations, type: Integer, allow_blank: false) this.optional(:stable_coefficient, type: Float, allow_blank: false) this.optional(:initial_time_out, type: Integer, allow_blank: false, documentation: { hidden: true }) this.optional(:minimum_duration, type: Integer, allow_blank: false) this.optional(:time_out_multiplier, type: Integer) this.optional(:vehicle_limit, type: Integer) this.optional(:solver_parameter, type: Integer, documentation: { hidden: true }) this.optional(:solver, type: Boolean, default: true) this.optional(:same_point_day, type: Boolean) this.optional(:allow_partial_assignment, type: Boolean, default: true) this.optional(:split_number, type: Integer) this.optional(:evaluate_only, type: Boolean) this.optional(:several_solutions, type: Integer, allow_blank: false, default: 1) this.optional(:batch_heuristic, type: Boolean, default: false) this.optional(:variation_ratio, type: Integer) this.optional(:repetition, type: Integer, documentation: { hidden: true }) this.at_least_one_of :duration, :iterations, :iterations_without_improvment, :stable_iterations, :stable_coefficient, :initial_time_out, :minimum_duration this.mutually_exclusive :initial_time_out, :minimum_duration end def self.vrp_request_restitution(this) this.optional(:geometry, type: Boolean) this.optional(:geometry_polyline, type: Boolean) this.optional(:intermediate_solutions, type: Boolean) this.optional(:csv, type: Boolean) this.optional(:allow_empty_result, type: Boolean) end def self.vrp_request_schedule(this) this.optional(:range_indices, type: Hash) do API.vrp_request_indice_range(self) end this.optional(:unavailable_indices, type: Array[Integer]) end params do optional(:vrp, type: Hash, documentation: { param_type: 'body' }) do optional(:name, type: String) optional(:points, type: Array) do API.vrp_request_point(self) end optional(:units, type: Array) do API.vrp_request_unit(self) end requires(:vehicles, type: Array) do API.vrp_request_vehicle(self) end optional(:services, type: Array, allow_blank: false) do API.vrp_request_service(self) end optional(:configuration, type: Hash) do API.vrp_request_configuration(self) end end end post '/' do { skills_v1: params[:vrp][:vehicles].first[:skills], skills_v2: params[:vrp][:vehicles].last[:skills] } end end puts Grape::VERSION options = { method: Rack::POST, params: JSON.parse(File.read('benchmark/resource/vrp_example.json')) } env = Rack::MockRequest.env_for('/api/v1', options) start = Time.now result = RubyProf.profile do response = API.call env puts response.last end puts Time.now - start printer = RubyProf::FlatPrinter.new(result) File.open('test_prof.out', 'w+') { |f| printer.print(f, {}) } ================================================ FILE: benchmark/nested_params.rb ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/ips' class API < Grape::API prefix :api version 'v1', using: :path params do requires :address, type: Hash do requires :street, type: String requires :postal_code, type: Integer optional :city, type: String end end post '/' do 'hello' end end options = { method: Rack::POST, params: { address: { street: 'Alexis Pl.', postal_code: '90210', city: 'Beverly Hills' } } } env = Rack::MockRequest.env_for('/api/v1', options) 10.times do |i| env["HTTP_HEADER#{i}"] = '123' end Benchmark.ips do |ips| ips.report('POST with nested params') do API.call env end end ================================================ FILE: benchmark/remounting.rb ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/memory' class VotingApi < Grape::API logger Logger.new($stdout) helpers do def logger VotingApi.logger end end namespace 'votes' do get do logger end end end class PostApi < Grape::API mount VotingApi end class CommentAPI < Grape::API mount VotingApi end env = Rack::MockRequest.env_for('/votes', method: Rack::GET) Benchmark.memory do |api| calls = 1000 api.report('using Array') do VotingApi.instance_variable_set(:@setup, []) calls.times { PostApi.call(env) } puts " setup size: #{VotingApi.instance_variable_get(:@setup).size}" end api.report('using Set') do VotingApi.instance_variable_set(:@setup, Set.new) calls.times { PostApi.call(env) } puts " setup size: #{VotingApi.instance_variable_get(:@setup).size}" end api.compare! end ================================================ FILE: benchmark/resource/vrp_example.json ================================================ {"vrp":{"points":[{"id":"1002100","location":{"lat":48.865,"lon":2.3054}},{"id":"1103548","location":{"lat":48.8711,"lon":2.3079}},{"id":"1142617","location":{"lat":48.8756,"lon":2.302}},{"id":"1147052","location":{"lat":48.8758,"lon":2.3074}},{"id":"1104396","location":{"lat":48.8776,"lon":2.3056}},{"id":"1139292","location":{"lat":48.8767,"lon":2.3032}},{"id":"1139149","location":{"lat":48.8767,"lon":2.3073}},{"id":"1118656","location":{"lat":48.8732,"lon":2.3049}},{"id":"1123712","location":{"lat":48.8755,"lon":2.3023}},{"id":"1120539","location":{"lat":48.8739,"lon":2.303}},{"id":"1109631","location":{"lat":48.8774,"lon":2.3047}},{"id":"1139151","location":{"lat":48.8767,"lon":2.3071}},{"id":"1005088","location":{"lat":48.8714,"lon":2.307}},{"id":"1054022","location":{"lat":48.8735,"lon":2.3095}},{"id":"1052132","location":{"lat":48.8733,"lon":2.3058}},{"id":"1080067","location":{"lat":48.8755,"lon":2.3024}},{"id":"1080537","location":{"lat":48.8732,"lon":2.3057}},{"id":"1001821","location":{"lat":48.8721,"lon":2.3043}},{"id":"1033652","location":{"lat":48.8758,"lon":2.3031}},{"id":"1127811","location":{"lat":48.8768,"lon":2.3091}},{"id":"1031446","location":{"lat":48.8723,"lon":2.3033}},{"id":"1004332","location":{"lat":48.8733,"lon":2.3056}},{"id":"1030348","location":{"lat":48.875,"lon":2.3051}},{"id":"1062118","location":{"lat":48.873,"lon":2.305}},{"id":"1035112","location":{"lat":48.8755,"lon":2.3023}},{"id":"1001140","location":{"lat":48.8776,"lon":2.3038}},{"id":"1144968","location":{"lat":48.8749,"lon":2.304}},{"id":"1136835","location":{"lat":48.8732,"lon":2.3051}},{"id":"1133790","location":{"lat":48.879,"lon":2.3043}},{"id":"1133878","location":{"lat":48.8785,"lon":2.3039}},{"id":"1007882","location":{"lat":48.8738,"lon":2.2965}},{"id":"1020596","location":{"lat":48.8664,"lon":2.31}},{"id":"1064282","location":{"lat":48.8731,"lon":2.3072}},{"id":"1134687","location":{"lat":48.8759,"lon":2.3077}},{"id":"1135600","location":{"lat":48.8768,"lon":2.3092}},{"id":"1133576","location":{"lat":48.8768,"lon":2.3091}},{"id":"1138821","location":{"lat":48.8749,"lon":2.3035}},{"id":"1066596","location":{"lat":48.8722,"lon":2.2967}},{"id":"1080091","location":{"lat":48.8787,"lon":2.3051}},{"id":"1094392","location":{"lat":48.8732,"lon":2.3131}},{"id":"1071805","location":{"lat":48.8755,"lon":2.3022}},{"id":"1064291","location":{"lat":48.8731,"lon":2.3072}},{"id":"1137046","location":{"lat":48.8732,"lon":2.3051}},{"id":"1131694","location":{"lat":48.8744,"lon":2.2984}},{"id":"1005035","location":{"lat":48.8786,"lon":2.3131}},{"id":"1004005","location":{"lat":48.8733,"lon":2.3062}},{"id":"1041519","location":{"lat":48.8755,"lon":2.3022}},{"id":"1148428","location":{"lat":0.0,"lon":0.0}},{"id":"1119178","location":{"lat":48.8726,"lon":2.304}},{"id":"1030515","location":{"lat":48.8789,"lon":2.303}},{"id":"1130633","location":{"lat":48.8755,"lon":2.3023}},{"id":"1132792","location":{"lat":48.8744,"lon":2.2984}},{"id":"1124356","location":{"lat":48.8753,"lon":2.3047}},{"id":"1121089","location":{"lat":48.8769,"lon":2.3074}},{"id":"1102925","location":{"lat":48.8732,"lon":2.3131}},{"id":"1102928","location":{"lat":48.8732,"lon":2.3131}},{"id":"1105871","location":{"lat":48.872,"lon":2.3039}},{"id":"1116088","location":{"lat":48.8768,"lon":2.3091}},{"id":"1109290","location":{"lat":48.8747,"lon":2.2982}},{"id":"1131649","location":{"lat":48.8775,"lon":2.2997}},{"id":"1136697","location":{"lat":48.8732,"lon":2.3051}},{"id":"1030517","location":{"lat":48.8751,"lon":2.3064}},{"id":"1132871","location":{"lat":48.8732,"lon":2.3051}},{"id":"1148306","location":{"lat":0.0,"lon":0.0}},{"id":"1126467","location":{"lat":48.8768,"lon":2.3091}},{"id":"1130723","location":{"lat":48.8768,"lon":2.3006}},{"id":"1099009","location":{"lat":48.874,"lon":2.2984}},{"id":"1095726","location":{"lat":48.8777,"lon":2.2994}},{"id":"1005056","location":{"lat":48.8776,"lon":2.3038}},{"id":"1122952","location":{"lat":48.8738,"lon":2.3005}},{"id":"1126324","location":{"lat":48.8768,"lon":2.3091}},{"id":"1124513","location":{"lat":48.8732,"lon":2.3051}},{"id":"1124103","location":{"lat":48.873,"lon":2.3047}},{"id":"1131394","location":{"lat":48.8747,"lon":2.3239}},{"id":"1133951","location":{"lat":48.8704,"lon":2.3211}},{"id":"1137715","location":{"lat":48.8698,"lon":2.3182}},{"id":"1132589","location":{"lat":48.8739,"lon":2.3214}},{"id":"1145751","location":{"lat":48.8715,"lon":2.3236}},{"id":"1070749","location":{"lat":48.8712,"lon":2.3194}},{"id":"1070735","location":{"lat":48.8703,"lon":2.3176}},{"id":"1002504","location":{"lat":48.8696,"lon":2.3188}},{"id":"1007287","location":{"lat":48.8707,"lon":2.3199}},{"id":"1005919","location":{"lat":48.8698,"lon":2.3178}},{"id":"1143914","location":{"lat":48.8693,"lon":2.3201}},{"id":"1144594","location":{"lat":48.8764,"lon":2.3083}},{"id":"1127546","location":{"lat":48.8692,"lon":2.3209}},{"id":"1123348","location":{"lat":48.8742,"lon":2.3171}},{"id":"1103574","location":{"lat":48.8711,"lon":2.3185}},{"id":"1087334","location":{"lat":48.8724,"lon":2.3183}},{"id":"1088315","location":{"lat":48.8762,"lon":2.3135}},{"id":"1054230","location":{"lat":48.8697,"lon":2.3198}},{"id":"1058540","location":{"lat":48.8701,"lon":2.3209}},{"id":"1106440","location":{"lat":48.87,"lon":2.3185}},{"id":"1120609","location":{"lat":48.8729,"lon":2.3228}},{"id":"1119750","location":{"lat":48.8693,"lon":2.3195}},{"id":"1107065","location":{"lat":48.8708,"lon":2.3202}},{"id":"1096970","location":{"lat":48.8733,"lon":2.3193}},{"id":"1124357","location":{"lat":48.8716,"lon":2.3216}},{"id":"1130453","location":{"lat":48.8763,"lon":2.3139}},{"id":"1121283","location":{"lat":48.8733,"lon":2.3213}},{"id":"1143992","location":{"lat":48.8713,"lon":2.3226}},{"id":"1020782","location":{"lat":48.8717,"lon":2.3198}},{"id":"1109136","location":{"lat":48.8732,"lon":2.3214}},{"id":"1107406","location":{"lat":48.87,"lon":2.3189}},{"id":"1001454","location":{"lat":48.8717,"lon":2.322}},{"id":"1031405","location":{"lat":48.8733,"lon":2.3181}},{"id":"1099019","location":{"lat":48.8712,"lon":2.3184}},{"id":"1040631","location":{"lat":48.8722,"lon":2.3231}},{"id":"1030463","location":{"lat":48.8725,"lon":2.3218}},{"id":"1033191","location":{"lat":48.8736,"lon":2.3213}},{"id":"1133959","location":{"lat":48.873,"lon":2.3163}},{"id":"1004770","location":{"lat":48.8788,"lon":2.3171}},{"id":"1129651","location":{"lat":48.8713,"lon":2.3226}},{"id":"1121101","location":{"lat":48.8701,"lon":2.3183}},{"id":"1119751","location":{"lat":48.8703,"lon":2.3212}},{"id":"1137030","location":{"lat":48.8729,"lon":2.3223}},{"id":"1134263","location":{"lat":48.8764,"lon":2.3142}},{"id":"1133530","location":{"lat":48.873,"lon":2.3176}},{"id":"1142237","location":{"lat":48.8713,"lon":2.3226}},{"id":"1030487","location":{"lat":48.8701,"lon":2.3191}},{"id":"1004647","location":{"lat":48.874,"lon":2.3186}},{"id":"1004716","location":{"lat":48.8737,"lon":2.3172}},{"id":"1144936","location":{"lat":48.8772,"lon":2.3165}},{"id":"1134666","location":{"lat":48.874,"lon":2.3184}},{"id":"1006725","location":{"lat":48.8736,"lon":2.3158}},{"id":"1092502","location":{"lat":48.8754,"lon":2.323}},{"id":"1008001","location":{"lat":48.8749,"lon":2.3158}},{"id":"1144493","location":{"lat":48.873,"lon":2.3124}},{"id":"1147114","location":{"lat":48.8738,"lon":2.3165}},{"id":"1147721","location":{"lat":0.0,"lon":0.0}},{"id":"1003152","location":{"lat":48.8763,"lon":2.3205}},{"id":"1110450","location":{"lat":48.8735,"lon":2.3142}},{"id":"1070260","location":{"lat":48.8742,"lon":2.3206}},{"id":"1132451","location":{"lat":48.8739,"lon":2.3193}},{"id":"1122595","location":{"lat":48.8743,"lon":2.3212}},{"id":"1134348","location":{"lat":48.8749,"lon":2.3211}},{"id":"1127201","location":{"lat":48.8732,"lon":2.3131}},{"id":"1138580","location":{"lat":48.8751,"lon":2.3211}},{"id":"1143039","location":{"lat":48.8731,"lon":2.3135}},{"id":"1132224","location":{"lat":48.8746,"lon":2.3226}},{"id":"1095177","location":{"lat":48.877,"lon":2.3175}},{"id":"1111407","location":{"lat":48.8745,"lon":2.3219}},{"id":"1117925","location":{"lat":48.8739,"lon":2.3178}},{"id":"1135294","location":{"lat":48.8737,"lon":2.3138}},{"id":"1031534","location":{"lat":48.8735,"lon":2.3143}},{"id":"1047944","location":{"lat":48.8739,"lon":2.3195}},{"id":"1050281","location":{"lat":48.873,"lon":2.3157}},{"id":"1054024","location":{"lat":48.8754,"lon":2.3236}},{"id":"1040973","location":{"lat":48.8765,"lon":2.3173}},{"id":"1063338","location":{"lat":48.8752,"lon":2.3171}},{"id":"1031918","location":{"lat":48.8739,"lon":2.3178}},{"id":"1145151","location":{"lat":48.8739,"lon":2.3193}},{"id":"1054036","location":{"lat":48.8748,"lon":2.3215}},{"id":"1004708","location":{"lat":48.875,"lon":2.3203}},{"id":"1002561","location":{"lat":48.8744,"lon":2.3174}},{"id":"1005880","location":{"lat":48.8738,"lon":2.3161}},{"id":"1144485","location":{"lat":48.8736,"lon":2.3139}},{"id":"1116199","location":{"lat":48.8737,"lon":2.3142}},{"id":"1123435","location":{"lat":48.8738,"lon":2.318}},{"id":"1124213","location":{"lat":48.8743,"lon":2.3182}},{"id":"startvehicule1","location":{"lat":48.78,"lon":2.43}},{"id":"startvehicule2","location":{"lat":48.78,"lon":2.43}},{"id":"endvehicule1","location":{"lat":48.78,"lon":2.43}},{"id":"endvehicule2","location":{"lat":48.78,"lon":2.43}}],"units":[{"id":"kg","label":"kg"},{"id":"l","label":"l"},{"id":"qte","label":"qte"}],"services":[{"id":"1002100_EMP_ 28_1FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":3,"minimum_lapse":120.0,"activity":{"point_id":"1002100","duration":120,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":30600,"end":45000,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1147052_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":9.08},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1147052","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1104396_SAV_ 84_4FA","quantities":[{"unit_id":"kg","value":5.5},{"unit_id":"l","value":5.0},{"unit_id":"qte","value":5.0}],"visits_number":1,"minimum_lapse":20.0,"activity":{"point_id":"1104396","duration":20,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1147052_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":9.08},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1147052","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1139292_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139149_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1139149","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1104396_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":5.5},{"unit_id":"l","value":5.0},{"unit_id":"qte","value":5.0}],"visits_number":1,"minimum_lapse":20.0,"activity":{"point_id":"1104396","duration":20,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1104396_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":5.5},{"unit_id":"l","value":5.0},{"unit_id":"qte","value":5.0}],"visits_number":1,"minimum_lapse":20.0,"activity":{"point_id":"1104396","duration":20,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1120539_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1120539","duration":8,"setup_duration":120,"timewindows":[{"start":34200,"end":64800,"day_index":0},{"start":34200,"end":64800,"day_index":1},{"start":34200,"end":64800,"day_index":2},{"start":34200,"end":64800,"day_index":3},{"start":34200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1120539_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1120539","duration":8,"setup_duration":120,"timewindows":[{"start":34200,"end":64800,"day_index":0},{"start":34200,"end":64800,"day_index":1},{"start":34200,"end":64800,"day_index":2},{"start":34200,"end":64800,"day_index":3},{"start":34200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1120539_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1120539","duration":8,"setup_duration":120,"timewindows":[{"start":34200,"end":64800,"day_index":0},{"start":34200,"end":64800,"day_index":1},{"start":34200,"end":64800,"day_index":2},{"start":34200,"end":64800,"day_index":3},{"start":34200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1109631_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1109631_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1109631_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1118656_PH _ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139151_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1139151_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1139151_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1005088_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1052132_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1052132_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1080537_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080537_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080537_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1001821_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1001821_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1033652_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":21.0},{"unit_id":"l","value":256.0},{"unit_id":"qte","value":20.0}],"visits_number":3,"minimum_lapse":160.0,"activity":{"point_id":"1033652","duration":160,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":30600,"end":45000,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1052132_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1031446_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":4.89},{"unit_id":"l","value":19.55},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1031446","duration":100,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005088_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005088_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1004332_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1033652_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":21.0},{"unit_id":"l","value":256.0},{"unit_id":"qte","value":20.0}],"visits_number":3,"minimum_lapse":160.0,"activity":{"point_id":"1033652","duration":160,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":30600,"end":45000,"day_index":4}]},"type":"service"},{"id":"1001821_ASC_ 84_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1139149_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1139149","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1062118_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1062118","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1062118_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1062118","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1062118_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1062118","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1062118_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1062118","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1035112_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1035112","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1035112_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1035112","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1001140_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":104.0,"activity":{"point_id":"1001140","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":68400,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":68400,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":68400,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":68400,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1035112_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1035112","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1144968_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.852},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1144968","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144968_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.852},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1144968","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144968_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.852},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1144968","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136835_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.44},{"unit_id":"l","value":8.4},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1136835","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133790_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133790","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133790_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133790","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133790_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133790","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133878_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133878","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133878_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133878","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133878_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":10.4},{"unit_id":"l","value":49.248},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1133878","duration":64,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1007882_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1020596_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.2},{"unit_id":"l","value":15.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1020596","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064282_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":52.0,"activity":{"point_id":"1064282","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1064282_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":52.0,"activity":{"point_id":"1064282","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1134687_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1134687","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1135600_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":49.35},{"unit_id":"l","value":601.6},{"unit_id":"qte","value":47.0}],"visits_number":3,"minimum_lapse":376.0,"activity":{"point_id":"1135600","duration":376,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1135600_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":49.35},{"unit_id":"l","value":601.6},{"unit_id":"qte","value":47.0}],"visits_number":3,"minimum_lapse":376.0,"activity":{"point_id":"1135600","duration":376,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133576_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":59.52},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":192.0}],"visits_number":3,"minimum_lapse":768.0,"activity":{"point_id":"1133576","duration":768,"setup_duration":120,"timewindows":[{"start":23400,"end":34200,"day_index":0},{"start":23400,"end":34200,"day_index":1},{"start":23400,"end":34200,"day_index":2},{"start":23400,"end":34200,"day_index":3},{"start":23400,"end":34200,"day_index":4}]},"type":"service"},{"id":"1133576_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":59.52},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":192.0}],"visits_number":3,"minimum_lapse":768.0,"activity":{"point_id":"1133576","duration":768,"setup_duration":120,"timewindows":[{"start":23400,"end":34200,"day_index":0},{"start":23400,"end":34200,"day_index":1},{"start":23400,"end":34200,"day_index":2},{"start":23400,"end":34200,"day_index":3},{"start":23400,"end":34200,"day_index":4}]},"type":"service"},{"id":"1133576_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":59.52},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":192.0}],"visits_number":3,"minimum_lapse":768.0,"activity":{"point_id":"1133576","duration":768,"setup_duration":120,"timewindows":[{"start":23400,"end":34200,"day_index":0},{"start":23400,"end":34200,"day_index":1},{"start":23400,"end":34200,"day_index":2},{"start":23400,"end":34200,"day_index":3},{"start":23400,"end":34200,"day_index":4}]},"type":"service"},{"id":"1138821_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":26.25},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":125.0}],"visits_number":3,"minimum_lapse":500.0,"activity":{"point_id":"1138821","duration":500,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138821_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":26.25},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":125.0}],"visits_number":3,"minimum_lapse":500.0,"activity":{"point_id":"1138821","duration":500,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138821_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":26.25},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":125.0}],"visits_number":3,"minimum_lapse":500.0,"activity":{"point_id":"1138821","duration":500,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134687_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1134687","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1139149_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1139149","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1134687_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1134687","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1066596_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064282_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":52.0,"activity":{"point_id":"1064282","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1080091_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":64.74},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":204.0}],"visits_number":3,"minimum_lapse":816.0,"activity":{"point_id":"1080091","duration":816,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080091_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":64.74},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":204.0}],"visits_number":3,"minimum_lapse":816.0,"activity":{"point_id":"1080091","duration":816,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1094392_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1094392","duration":90,"setup_duration":120,"timewindows":[{"start":25200,"end":45000,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":25200,"end":45000,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":25200,"end":45000,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":25200,"end":45000,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":25200,"end":45000,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1080091_CLI_ 84_4FA","quantities":[{"unit_id":"kg","value":64.74},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":204.0}],"visits_number":3,"minimum_lapse":816.0,"activity":{"point_id":"1080091","duration":816,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1071805_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":130.0,"activity":{"point_id":"1071805","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":70200,"day_index":0},{"start":28800,"end":70200,"day_index":1},{"start":28800,"end":70200,"day_index":2},{"start":28800,"end":70200,"day_index":3},{"start":28800,"end":70200,"day_index":4}]},"type":"service"},{"id":"1064291_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1134687_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1134687","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1136835_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":4.44},{"unit_id":"l","value":8.4},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1136835","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1001821_CLI_ 84_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064291_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1066596_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064291_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1066596_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064291_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1137046_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137046_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131694_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1137046_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1071805_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":130.0,"activity":{"point_id":"1071805","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":70200,"day_index":0},{"start":28800,"end":70200,"day_index":1},{"start":28800,"end":70200,"day_index":2},{"start":28800,"end":70200,"day_index":3},{"start":28800,"end":70200,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1066596_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005035_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005035_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004005_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004005_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004005_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004005_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1041519_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":2.59},{"unit_id":"l","value":2.625},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1041519","duration":28,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1064282_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":52.0,"activity":{"point_id":"1064282","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1131694_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1131694_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148428_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148428_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1139292_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1118656_PH _ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004005_TAP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119178_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1123712_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119178_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_DIF_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1118656_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1001821_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005035_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1127811_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1130633_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1130633_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130633_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130633_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132792_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":16.6},{"unit_id":"l","value":66.0},{"unit_id":"qte","value":5.0}],"visits_number":6,"minimum_lapse":500.0,"activity":{"point_id":"1132792","duration":500,"setup_duration":120,"timewindows":[{"start":21600,"end":61200,"day_index":0},{"start":21600,"end":61200,"day_index":1},{"start":21600,"end":61200,"day_index":2},{"start":21600,"end":61200,"day_index":3},{"start":21600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1130633_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124356_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1124356","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1121089_EMP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1102925_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102925","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1102928_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102928","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1105871_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1105871_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1105871_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1116088_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1116088_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1105871_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109290_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":50.6},{"unit_id":"l","value":142.6},{"unit_id":"qte","value":23.0}],"visits_number":6,"minimum_lapse":299.0,"activity":{"point_id":"1109290","duration":299,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131649_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":3.36},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":16.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1131649","duration":64,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1131649_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":3.36},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":16.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1131649","duration":64,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1136697_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136697_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030517_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1007882_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1007882_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1007882_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1020596_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.2},{"unit_id":"l","value":15.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1020596","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030515_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030517_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005035_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030517_CLI_ 84_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030517_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136697_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136697_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132871_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1132871","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1148306_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148306_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148306_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1001140_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":104.0,"activity":{"point_id":"1001140","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":68400,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":68400,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":68400,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":68400,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030517_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139292_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136835_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.44},{"unit_id":"l","value":8.4},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1136835","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1126467_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1126467","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":30600,"day_index":0},{"start":21600,"end":30600,"day_index":1},{"start":21600,"end":30600,"day_index":2},{"start":21600,"end":30600,"day_index":3},{"start":21600,"end":30600,"day_index":4}]},"type":"service"},{"id":"1130633_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1102928_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102928","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1130633_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130633_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131649_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":3.36},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":16.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1131649","duration":64,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1132792_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":16.6},{"unit_id":"l","value":66.0},{"unit_id":"qte","value":5.0}],"visits_number":6,"minimum_lapse":500.0,"activity":{"point_id":"1132792","duration":500,"setup_duration":120,"timewindows":[{"start":21600,"end":61200,"day_index":0},{"start":21600,"end":61200,"day_index":1},{"start":21600,"end":61200,"day_index":2},{"start":21600,"end":61200,"day_index":3},{"start":21600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1136697_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136697_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131649_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":3.36},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":16.0}],"visits_number":3,"minimum_lapse":64.0,"activity":{"point_id":"1131649","duration":64,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1130633_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130633_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130633_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1130633","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1116088_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1105871_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109290_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":50.6},{"unit_id":"l","value":142.6},{"unit_id":"qte","value":23.0}],"visits_number":6,"minimum_lapse":299.0,"activity":{"point_id":"1109290","duration":299,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_EMP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124356_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1124356","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1127811_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1136697_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1136697_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":312.0,"activity":{"point_id":"1136697","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132871_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":4.8},{"unit_id":"l","value":94.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1132871","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1066596_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1071805_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":130.0,"activity":{"point_id":"1071805","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":70200,"day_index":0},{"start":28800,"end":70200,"day_index":1},{"start":28800,"end":70200,"day_index":2},{"start":28800,"end":70200,"day_index":3},{"start":28800,"end":70200,"day_index":4}]},"type":"service"},{"id":"1066596_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1066596_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1066596","duration":100,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064291_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1064291_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1064291_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1064291","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1004005_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1064282_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":52.0,"activity":{"point_id":"1064282","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":70200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":70200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":70200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":70200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":70200,"day_index":4}]},"type":"service"},{"id":"1116088_CLI_ 84_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1030517_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1148306_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148306_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148306_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.62},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":22.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1148306","duration":88,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1001140_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":104.0,"activity":{"point_id":"1001140","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":68400,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":68400,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":68400,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":68400,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030517_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030517_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030517_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":1.095},{"unit_id":"l","value":1.53},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1030517","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1041519_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":2.59},{"unit_id":"l","value":2.625},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1041519","duration":28,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1004005_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1116088_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1105871_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1139292_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139151_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1139151_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1139151_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1139151","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1005088_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005088_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005088_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":6.2},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1005088","duration":16,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1139149_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1139149","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":72000,"day_index":0},{"start":32400,"end":72000,"day_index":1},{"start":32400,"end":72000,"day_index":2},{"start":32400,"end":72000,"day_index":3},{"start":32400,"end":72000,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1147052_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":9.08},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1147052","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1147052_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":9.08},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1147052","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1109631_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1109631_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1118656_PH _ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1104396_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":5.5},{"unit_id":"l","value":5.0},{"unit_id":"qte","value":5.0}],"visits_number":1,"minimum_lapse":20.0,"activity":{"point_id":"1104396","duration":20,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1104396_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":5.5},{"unit_id":"l","value":5.0},{"unit_id":"qte","value":5.0}],"visits_number":1,"minimum_lapse":20.0,"activity":{"point_id":"1104396","duration":20,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1004332_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1080537_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1001821_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1001821_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":15.0,"activity":{"point_id":"1001821","duration":15,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1102925_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102925","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1102925_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102925","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1102928_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1102928","duration":90,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":63000,"end":73800,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":63000,"end":73800,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":63000,"end":73800,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":63000,"end":73800,"day_index":3},{"start":21600,"end":32400,"day_index":4},{"start":63000,"end":73800,"day_index":4}]},"type":"service"},{"id":"1105871_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1105871_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080537_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1116088_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":20.29},{"unit_id":"l","value":37.8},{"unit_id":"qte","value":64.0}],"visits_number":3,"minimum_lapse":256.0,"activity":{"point_id":"1116088","duration":256,"setup_duration":120,"timewindows":[{"start":28800,"end":43200,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":28800,"end":43200,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":28800,"end":43200,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":28800,"end":43200,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":28800,"end":43200,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1080537_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":11.55},{"unit_id":"l","value":140.8},{"unit_id":"qte","value":11.0}],"visits_number":3,"minimum_lapse":88.0,"activity":{"point_id":"1080537","duration":88,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1052132_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004332_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1004332_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1031446_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":4.89},{"unit_id":"l","value":19.55},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1031446","duration":100,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1033652_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":21.0},{"unit_id":"l","value":256.0},{"unit_id":"qte","value":20.0}],"visits_number":3,"minimum_lapse":160.0,"activity":{"point_id":"1033652","duration":160,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":30600,"end":45000,"day_index":4}]},"type":"service"},{"id":"1033652_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":21.0},{"unit_id":"l","value":256.0},{"unit_id":"qte","value":20.0}],"visits_number":3,"minimum_lapse":160.0,"activity":{"point_id":"1033652","duration":160,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":30600,"end":45000,"day_index":4}]},"type":"service"},{"id":"1052132_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1052132_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":14.7},{"unit_id":"l","value":179.2},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":112.0,"activity":{"point_id":"1052132","duration":112,"setup_duration":120,"timewindows":[{"start":34200,"end":61200,"day_index":0},{"start":34200,"end":61200,"day_index":1},{"start":34200,"end":61200,"day_index":2},{"start":34200,"end":61200,"day_index":3},{"start":34200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1130723_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":115.2},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1130723","duration":72,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004005_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1007882_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1099009_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099009_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099009_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099009_CLI_168_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099009_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1105871_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":41.8},{"unit_id":"l","value":117.8},{"unit_id":"qte","value":19.0}],"visits_number":6,"minimum_lapse":247.0,"activity":{"point_id":"1105871","duration":247,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109290_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":50.6},{"unit_id":"l","value":142.6},{"unit_id":"qte","value":23.0}],"visits_number":6,"minimum_lapse":299.0,"activity":{"point_id":"1109290","duration":299,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099009_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":160.0,"activity":{"point_id":"1099009","duration":160,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1095726_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1095726","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1095726_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1095726","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1095726_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1095726","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005056_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1005056","duration":26,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1005056_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1005056","duration":26,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1033652_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":21.0},{"unit_id":"l","value":256.0},{"unit_id":"qte","value":20.0}],"visits_number":3,"minimum_lapse":160.0,"activity":{"point_id":"1033652","duration":160,"setup_duration":120,"timewindows":[{"start":30600,"end":45000,"day_index":0},{"start":30600,"end":45000,"day_index":1},{"start":30600,"end":45000,"day_index":2},{"start":30600,"end":45000,"day_index":3},{"start":30600,"end":45000,"day_index":4}]},"type":"service"},{"id":"1054022_SNC_ 7_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1054022","duration":90,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1080067_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1080067_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":33.0},{"unit_id":"l","value":93.0},{"unit_id":"qte","value":15.0}],"visits_number":12,"minimum_lapse":195.0,"activity":{"point_id":"1080067","duration":195,"setup_duration":120,"timewindows":[{"start":21600,"end":46800,"day_index":0},{"start":21600,"end":46800,"day_index":1},{"start":21600,"end":46800,"day_index":2},{"start":21600,"end":46800,"day_index":3},{"start":21600,"end":46800,"day_index":4}]},"type":"service"},{"id":"1095726_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1095726","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1095726_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1095726","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_EMP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121089_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":19.5},{"unit_id":"l","value":92.34},{"unit_id":"qte","value":15.0}],"visits_number":6,"minimum_lapse":120.0,"activity":{"point_id":"1121089","duration":120,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1122952_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":15.4},{"unit_id":"l","value":43.4},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":91.0,"activity":{"point_id":"1122952","duration":91,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1126324_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":45.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1126324","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":36000,"day_index":0},{"start":32400,"end":36000,"day_index":1},{"start":32400,"end":36000,"day_index":2},{"start":32400,"end":36000,"day_index":3},{"start":32400,"end":36000,"day_index":4}]},"type":"service"},{"id":"1126324_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":45.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1126324","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":36000,"day_index":0},{"start":32400,"end":36000,"day_index":1},{"start":32400,"end":36000,"day_index":2},{"start":32400,"end":36000,"day_index":3},{"start":32400,"end":36000,"day_index":4}]},"type":"service"},{"id":"1126324_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":45.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1126324","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":36000,"day_index":0},{"start":32400,"end":36000,"day_index":1},{"start":32400,"end":36000,"day_index":2},{"start":32400,"end":36000,"day_index":3},{"start":32400,"end":36000,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1127811_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1126467_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1126467","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":30600,"day_index":0},{"start":21600,"end":30600,"day_index":1},{"start":21600,"end":30600,"day_index":2},{"start":21600,"end":30600,"day_index":3},{"start":21600,"end":30600,"day_index":4}]},"type":"service"},{"id":"1126467_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1126467","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":30600,"day_index":0},{"start":21600,"end":30600,"day_index":1},{"start":21600,"end":30600,"day_index":2},{"start":21600,"end":30600,"day_index":3},{"start":21600,"end":30600,"day_index":4}]},"type":"service"},{"id":"1130723_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":115.2},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1130723","duration":72,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132792_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":16.6},{"unit_id":"l","value":66.0},{"unit_id":"qte","value":5.0}],"visits_number":6,"minimum_lapse":500.0,"activity":{"point_id":"1132792","duration":500,"setup_duration":120,"timewindows":[{"start":21600,"end":61200,"day_index":0},{"start":21600,"end":61200,"day_index":1},{"start":21600,"end":61200,"day_index":2},{"start":21600,"end":61200,"day_index":3},{"start":21600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1126324_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":9.45},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":45.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1126324","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":36000,"day_index":0},{"start":32400,"end":36000,"day_index":1},{"start":32400,"end":36000,"day_index":2},{"start":32400,"end":36000,"day_index":3},{"start":32400,"end":36000,"day_index":4}]},"type":"service"},{"id":"1030348_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1124513_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":2.94},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1124513","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1122952_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":15.4},{"unit_id":"l","value":43.4},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":91.0,"activity":{"point_id":"1122952","duration":91,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124103_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":3.9},{"unit_id":"l","value":18.468},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1124103","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124103_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":3.9},{"unit_id":"l","value":18.468},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1124103","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1122952_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":15.4},{"unit_id":"l","value":43.4},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":91.0,"activity":{"point_id":"1122952","duration":91,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124356_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1124356","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1124103_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":3.9},{"unit_id":"l","value":18.468},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1124103","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124513_BOB_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":2.94},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1124513","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1124513_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":2.94},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1124513","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1124513_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":2.94},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1124513","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1124513_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":2.94},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1124513","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":68400,"day_index":0},{"start":36000,"end":68400,"day_index":1},{"start":36000,"end":68400,"day_index":2},{"start":36000,"end":68400,"day_index":3},{"start":36000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1004005_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030348_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1031446_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":4.89},{"unit_id":"l","value":19.55},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1031446","duration":100,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1137046_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131694_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1131694_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1137046_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131694_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":11.7},{"unit_id":"l","value":55.404},{"unit_id":"qte","value":9.0}],"visits_number":3,"minimum_lapse":72.0,"activity":{"point_id":"1131694","duration":72,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":52200,"end":61200,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":52200,"end":61200,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":52200,"end":61200,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":52200,"end":61200,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":52200,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127811_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1123712_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1127811_PH _ 7_4FA","quantities":[{"unit_id":"kg","value":29.76},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":96.0}],"visits_number":12,"minimum_lapse":384.0,"activity":{"point_id":"1127811","duration":384,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1137046_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1137046","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005035_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005035_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1007882_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1007882_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":26.4},{"unit_id":"l","value":74.4},{"unit_id":"qte","value":12.0}],"visits_number":6,"minimum_lapse":117.0,"activity":{"point_id":"1007882","duration":117,"setup_duration":120,"timewindows":[{"start":21600,"end":43200,"day_index":0},{"start":21600,"end":43200,"day_index":1},{"start":21600,"end":43200,"day_index":2},{"start":21600,"end":43200,"day_index":3},{"start":21600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1020596_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.2},{"unit_id":"l","value":15.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1020596","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1030515_SNC_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1030515_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1030515","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005035_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":7.64},{"unit_id":"l","value":16.8},{"unit_id":"qte","value":24.0}],"visits_number":3,"minimum_lapse":132.0,"activity":{"point_id":"1005035","duration":132,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004005_TAP_ 28_4FA","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":12.4},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":26.0,"activity":{"point_id":"1004005","duration":26,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123712_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1123712","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119178_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1142617_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_PCP_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1142617_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1139292_SNC_ 14_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139292_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139292_CLI_ 84_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1139292_DIF_ 84_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148428_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031446_ASC_ 84_4FA","quantities":[{"unit_id":"kg","value":4.89},{"unit_id":"l","value":19.55},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1031446","duration":100,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1139292_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004332_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1142617_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1148428_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119178_LPL_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_DIF_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119178_CLI_ 28_4FA","quantities":[{"unit_id":"kg","value":12.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":250.0}],"visits_number":3,"minimum_lapse":3250.0,"activity":{"point_id":"1119178","duration":3250,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1118656_PCP_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1118656_PH _ 14_4FA","quantities":[{"unit_id":"kg","value":14.88},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":48.0}],"visits_number":6,"minimum_lapse":192.0,"activity":{"point_id":"1118656","duration":192,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103548_TAP_ 7_4FA","quantities":[{"unit_id":"kg","value":9.7},{"unit_id":"l","value":37.02},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1103548","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148428_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1142617_SAV_ 14_4FA","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":24.0,"activity":{"point_id":"1142617","duration":24,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1139292_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":1.9},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":180.0,"activity":{"point_id":"1139292","duration":180,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1148428_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":22.1},{"unit_id":"l","value":104.652},{"unit_id":"qte","value":17.0}],"visits_number":3,"minimum_lapse":136.0,"activity":{"point_id":"1148428","duration":136,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031446_TAP_ 14_4FA","quantities":[{"unit_id":"kg","value":4.89},{"unit_id":"l","value":19.55},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":100.0,"activity":{"point_id":"1031446","duration":100,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1131394_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1131394_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1133951_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137715_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137715_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1131394_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1137715_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070735_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1007287_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":15.5},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":50.0}],"visits_number":3,"minimum_lapse":200.0,"activity":{"point_id":"1007287","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1143914_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":8.94},{"unit_id":"l","value":33.9},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1143914","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1127546_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127546_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127546_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1123348_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103574_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.305},{"unit_id":"l","value":14.7},{"unit_id":"qte","value":23.0}],"visits_number":3,"minimum_lapse":92.0,"activity":{"point_id":"1103574","duration":92,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1087334_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1088315_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.8},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1088315","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1088315_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.8},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1088315","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1058540_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1058540","duration":180,"setup_duration":120,"timewindows":[{"start":30600,"end":72000,"day_index":0},{"start":30600,"end":72000,"day_index":1},{"start":30600,"end":72000,"day_index":2},{"start":30600,"end":72000,"day_index":3},{"start":30600,"end":72000,"day_index":4}]},"type":"service"},{"id":"1054230_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1103574_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.305},{"unit_id":"l","value":14.7},{"unit_id":"qte","value":23.0}],"visits_number":3,"minimum_lapse":92.0,"activity":{"point_id":"1103574","duration":92,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1120609_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1123348_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123348_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123348_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119750_EMP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1107065_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1119750_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119750_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1120609_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1120609_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1070749_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1096970_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1124357_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1124357","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1130453_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1121283_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1143992_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1143992_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1020782_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1143992_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1121283_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1109136_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109136_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109136_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1020782_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1001454_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1001454_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1070735_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070735_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031405_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.375},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1031405","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1031405_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.375},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1031405","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1107065_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1096970_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1070749_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1096970_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1040631_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1001454_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1030463_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1033191_ASC_ 84_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":400.0,"activity":{"point_id":"1033191","duration":400,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1040631_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1054230_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1133951_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1133959_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004770_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004770_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002504_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_ASC_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1143914_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":8.94},{"unit_id":"l","value":33.9},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1143914","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1129651_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1129651","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1129651_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1129651","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1121101_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1121101_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1109136_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1109136_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_DIF_ 42_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004770_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1119751_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119750_EMP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1107065_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121101_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1121101_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1119750_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119750_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119751_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119751_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1054230_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1005919_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1137030_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1134263_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1134263_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1137030_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1137030_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1134263_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1134263_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1133530_PCP_ 84_4FF","quantities":[{"unit_id":"kg","value":7.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":36.0}],"visits_number":1,"minimum_lapse":144.0,"activity":{"point_id":"1133530","duration":144,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137030_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1137030_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1142237_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1142237_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1107406_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1121283_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124357_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1124357","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1130453_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1133530_PH _ 84_4FF","quantities":[{"unit_id":"kg","value":7.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":36.0}],"visits_number":1,"minimum_lapse":144.0,"activity":{"point_id":"1133530","duration":144,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133530_SAV_ 84_4FF","quantities":[{"unit_id":"kg","value":7.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":36.0}],"visits_number":1,"minimum_lapse":144.0,"activity":{"point_id":"1133530","duration":144,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1132589_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1107065_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1096970_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1005919_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1107065_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1005919_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1040631_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1137030_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1142237_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1142237_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1020782_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_CLI_ 42_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_INI_ 42_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1040631_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1030487_SNC_ 42_4FF","quantities":[{"unit_id":"kg","value":2.8},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":2,"minimum_lapse":180.0,"activity":{"point_id":"1030487","duration":180,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1007287_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":15.5},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":50.0}],"visits_number":3,"minimum_lapse":200.0,"activity":{"point_id":"1007287","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1005919_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1144594_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1143914_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":8.94},{"unit_id":"l","value":33.9},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1143914","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070735_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070749_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":7.35},{"unit_id":"l","value":89.6},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1070749","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1096970_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1096970_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1096970_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1031405_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.375},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1031405","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1031405_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.375},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1031405","duration":4,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1070735_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145751_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1145751","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1131394_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1131394_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1123348_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123348_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119750_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119750_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1120609_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1120609_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1120609_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":6.6},{"unit_id":"l","value":6.0},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1120609","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1119750_EMP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1107065_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1123348_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070735_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.7},{"unit_id":"l","value":7.0},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":28.0,"activity":{"point_id":"1070735","duration":28,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1123348_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123348","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1127546_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1133951_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137715_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137715_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1137715_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":130.0,"activity":{"point_id":"1137715","duration":130,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1131394_CLI_ 84_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1131394_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1131394_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.475},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1131394","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1127546_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127546_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.8},{"unit_id":"l","value":0.75},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1127546","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107065_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107065_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1109136_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1109136_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1109136_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1109136","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124357_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1124357","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1130453_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1121283_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107406_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004332_BOB_ 14_4FA","quantities":[{"unit_id":"kg","value":1.11},{"unit_id":"l","value":1.125},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004332","duration":12,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1030348_PH _ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1030348_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1030348_BOB_ 7_4FA","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":12,"minimum_lapse":156.0,"activity":{"point_id":"1030348","duration":156,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1107406_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1132589_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1143992_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1143992_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1040631_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_CLI_ 84_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1030463_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":9.66},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":46.0}],"visits_number":3,"minimum_lapse":184.0,"activity":{"point_id":"1030463","duration":184,"setup_duration":120,"timewindows":[{"start":30000,"end":68400,"day_index":0},{"start":30000,"end":68400,"day_index":1},{"start":30000,"end":68400,"day_index":2},{"start":30000,"end":68400,"day_index":3},{"start":30000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1107065_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1040631_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1001454_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1143992_SAV_ 84_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1143992_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1143992","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":62100,"day_index":0},{"start":32400,"end":62100,"day_index":1},{"start":32400,"end":62100,"day_index":2},{"start":32400,"end":62100,"day_index":3},{"start":32400,"end":62100,"day_index":4}]},"type":"service"},{"id":"1020782_CLI_ 42_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_INI_ 42_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1001454_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1001454_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":9.0,"activity":{"point_id":"1001454","duration":9,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1103574_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":7.305},{"unit_id":"l","value":14.7},{"unit_id":"qte","value":23.0}],"visits_number":3,"minimum_lapse":92.0,"activity":{"point_id":"1103574","duration":92,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1103574_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":7.305},{"unit_id":"l","value":14.7},{"unit_id":"qte","value":23.0}],"visits_number":3,"minimum_lapse":92.0,"activity":{"point_id":"1103574","duration":92,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004770_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004770_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1143914_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":8.94},{"unit_id":"l","value":33.9},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1143914","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004770_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.93},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1004770","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":48600,"end":64800,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":48600,"end":64800,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":48600,"end":64800,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":48600,"end":64800,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":48600,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_INI_ 84_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_CLI_ 84_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002504_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1096970_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1005919_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1005919_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_ASC_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1002504_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1144594_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144594_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":6.64},{"unit_id":"l","value":26.4},{"unit_id":"qte","value":2.0}],"visits_number":6,"minimum_lapse":200.0,"activity":{"point_id":"1144594","duration":200,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1107065_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1121101_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1121101_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1119750_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119750_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1119751_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119751_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119751_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1119751","duration":12,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1119750_EMP_ 14_4FF","quantities":[{"unit_id":"kg","value":39.0},{"unit_id":"l","value":184.68},{"unit_id":"qte","value":30.0}],"visits_number":6,"minimum_lapse":240.0,"activity":{"point_id":"1119750","duration":240,"setup_duration":120,"timewindows":[{"start":30600,"end":39600,"day_index":0},{"start":51300,"end":56700,"day_index":0},{"start":30600,"end":39600,"day_index":1},{"start":51300,"end":56700,"day_index":1},{"start":30600,"end":39600,"day_index":2},{"start":51300,"end":56700,"day_index":2},{"start":30600,"end":39600,"day_index":3},{"start":51300,"end":56700,"day_index":3},{"start":30600,"end":39600,"day_index":4},{"start":51300,"end":56700,"day_index":4}]},"type":"service"},{"id":"1096970_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":11.0},{"unit_id":"l","value":10.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1096970","duration":40,"setup_duration":120,"timewindows":[{"start":28800,"end":57600,"day_index":0},{"start":28800,"end":57600,"day_index":1},{"start":28800,"end":57600,"day_index":2},{"start":28800,"end":57600,"day_index":3},{"start":28800,"end":57600,"day_index":4}]},"type":"service"},{"id":"1121101_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1129651_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1129651","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1133951_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_DIF_ 84_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133951_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":136.0},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":360.0,"activity":{"point_id":"1133951","duration":360,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1133959_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1133959","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1129651_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1129651","duration":8,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1121101_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":3.3},{"unit_id":"l","value":3.0},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":12.0,"activity":{"point_id":"1121101","duration":12,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1099019_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1099019_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002504_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":77.0},{"unit_id":"l","value":217.0},{"unit_id":"qte","value":35.0}],"visits_number":12,"minimum_lapse":455.0,"activity":{"point_id":"1002504","duration":455,"setup_duration":120,"timewindows":[{"start":21600,"end":32400,"day_index":0},{"start":21600,"end":32400,"day_index":1},{"start":21600,"end":32400,"day_index":2},{"start":21600,"end":32400,"day_index":3},{"start":21600,"end":32400,"day_index":4}]},"type":"service"},{"id":"1107406_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1121283_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1121283","duration":8,"setup_duration":120,"timewindows":[{"start":36000,"end":64800,"day_index":0},{"start":36000,"end":64800,"day_index":1},{"start":36000,"end":64800,"day_index":2},{"start":36000,"end":64800,"day_index":3},{"start":36000,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124357_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":90.0,"activity":{"point_id":"1124357","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1130453_BOB_ 14_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_CLI_ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1130453_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":52.8},{"unit_id":"l","value":148.8},{"unit_id":"qte","value":24.0}],"visits_number":6,"minimum_lapse":312.0,"activity":{"point_id":"1130453","duration":312,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132589_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1005919_BOB_ 7_4FF","quantities":[{"unit_id":"kg","value":89.46},{"unit_id":"l","value":276.0},{"unit_id":"qte","value":60.0}],"visits_number":12,"minimum_lapse":274.0,"activity":{"point_id":"1005919","duration":274,"setup_duration":120,"timewindows":[{"start":24300,"end":43200,"day_index":0},{"start":24300,"end":43200,"day_index":1},{"start":24300,"end":43200,"day_index":2},{"start":24300,"end":43200,"day_index":3},{"start":24300,"end":43200,"day_index":4}]},"type":"service"},{"id":"1142237_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1088315_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":2.8},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1088315","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1088315_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":2.8},{"unit_id":"l","value":30.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1088315","duration":180,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_CLI_ 84_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_DIF_ 42_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1087334_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":60.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":360.0,"activity":{"point_id":"1087334","duration":360,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054230_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":0.0},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":1,"minimum_lapse":80.0,"activity":{"point_id":"1054230","duration":80,"setup_duration":120,"timewindows":[{"start":36000,"end":61200,"day_index":0},{"start":36000,"end":61200,"day_index":1},{"start":36000,"end":61200,"day_index":2},{"start":36000,"end":61200,"day_index":3},{"start":36000,"end":61200,"day_index":4}]},"type":"service"},{"id":"1058540_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":180.0,"activity":{"point_id":"1058540","duration":180,"setup_duration":120,"timewindows":[{"start":30600,"end":72000,"day_index":0},{"start":30600,"end":72000,"day_index":1},{"start":30600,"end":72000,"day_index":2},{"start":30600,"end":72000,"day_index":3},{"start":30600,"end":72000,"day_index":4}]},"type":"service"},{"id":"1109631_PCP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1109631","duration":40,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":32400,"end":43200,"day_index":4}]},"type":"service"},{"id":"1142237_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1137030_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1020782_SAV_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1020782_DIF_ 42_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1040631_TAP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1040631_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":23.1},{"unit_id":"l","value":21.0},{"unit_id":"qte","value":21.0}],"visits_number":3,"minimum_lapse":35.0,"activity":{"point_id":"1040631","duration":35,"setup_duration":120,"timewindows":[{"start":27000,"end":68400,"day_index":0},{"start":27000,"end":68400,"day_index":1},{"start":27000,"end":68400,"day_index":2},{"start":27000,"end":68400,"day_index":3},{"start":27000,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_PH _ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1107065_PCP_ 14_4FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":4.0,"activity":{"point_id":"1107065","duration":4,"setup_duration":120,"timewindows":[{"start":39600,"end":68400,"day_index":0},{"start":39600,"end":68400,"day_index":1},{"start":39600,"end":68400,"day_index":2},{"start":39600,"end":68400,"day_index":3},{"start":39600,"end":68400,"day_index":4}]},"type":"service"},{"id":"1099019_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":43.364},{"unit_id":"l","value":123.8},{"unit_id":"qte","value":21.0}],"visits_number":6,"minimum_lapse":273.0,"activity":{"point_id":"1099019","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1020782_SNC_ 14_4FF","quantities":[{"unit_id":"kg","value":68.77},{"unit_id":"l","value":126.0},{"unit_id":"qte","value":217.0}],"visits_number":6,"minimum_lapse":556.0,"activity":{"point_id":"1020782","duration":556,"setup_duration":120,"timewindows":[{"start":27000,"end":57600,"day_index":0},{"start":27000,"end":57600,"day_index":1},{"start":27000,"end":57600,"day_index":2},{"start":27000,"end":57600,"day_index":3},{"start":27000,"end":57600,"day_index":4}]},"type":"service"},{"id":"1137030_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1106440_TAP_ 7_4FF","quantities":[{"unit_id":"kg","value":3.32},{"unit_id":"l","value":13.2},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":200.0,"activity":{"point_id":"1106440","duration":200,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1142237_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1137030_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1134263_BOB_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1134263_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1134263_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1134263_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":12.06},{"unit_id":"l","value":75.6},{"unit_id":"qte","value":36.0}],"visits_number":3,"minimum_lapse":144.0,"activity":{"point_id":"1134263","duration":144,"setup_duration":120,"timewindows":[{"start":30600,"end":66600,"day_index":0},{"start":30600,"end":66600,"day_index":1},{"start":30600,"end":66600,"day_index":2},{"start":30600,"end":66600,"day_index":3},{"start":30600,"end":66600,"day_index":4}]},"type":"service"},{"id":"1137030_EMP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1137030_PCP_ 28_4FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1137030","duration":40,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":63000,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":63000,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":63000,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":63000,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1132589_PH _ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1132589_SAV_ 28_4FF","quantities":[{"unit_id":"kg","value":48.4},{"unit_id":"l","value":136.4},{"unit_id":"qte","value":22.0}],"visits_number":12,"minimum_lapse":286.0,"activity":{"point_id":"1132589","duration":286,"setup_duration":120,"timewindows":[{"start":23400,"end":30600,"day_index":0},{"start":23400,"end":30600,"day_index":1},{"start":23400,"end":30600,"day_index":2},{"start":23400,"end":30600,"day_index":3},{"start":23400,"end":30600,"day_index":4}]},"type":"service"},{"id":"1142237_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":9.1},{"unit_id":"l","value":43.092},{"unit_id":"qte","value":7.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1142237","duration":56,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1107406_SNC_ 28_4FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1107406","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":61200,"day_index":0},{"start":28800,"end":61200,"day_index":1},{"start":28800,"end":61200,"day_index":2},{"start":28800,"end":61200,"day_index":3},{"start":28800,"end":61200,"day_index":4}]},"type":"service"},{"id":"1120539_EMP_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1120539","duration":8,"setup_duration":120,"timewindows":[{"start":34200,"end":64800,"day_index":0},{"start":34200,"end":64800,"day_index":1},{"start":34200,"end":64800,"day_index":2},{"start":34200,"end":64800,"day_index":3},{"start":34200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1120539_SAV_ 28_4FA","quantities":[{"unit_id":"kg","value":2.2},{"unit_id":"l","value":2.0},{"unit_id":"qte","value":2.0}],"visits_number":3,"minimum_lapse":8.0,"activity":{"point_id":"1120539","duration":8,"setup_duration":120,"timewindows":[{"start":34200,"end":64800,"day_index":0},{"start":34200,"end":64800,"day_index":1},{"start":34200,"end":64800,"day_index":2},{"start":34200,"end":64800,"day_index":3},{"start":34200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_PH _ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_DIF_ 42_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_SNC_ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_PCP_ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004716_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":6,"minimum_lapse":104.0,"activity":{"point_id":"1004716","duration":104,"setup_duration":120,"timewindows":[{"start":30600,"end":43200,"day_index":0},{"start":30600,"end":43200,"day_index":1},{"start":30600,"end":43200,"day_index":2},{"start":30600,"end":43200,"day_index":3},{"start":30600,"end":43200,"day_index":4}]},"type":"service"},{"id":"1144936_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":10.85},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":35.0}],"visits_number":3,"minimum_lapse":140.0,"activity":{"point_id":"1144936","duration":140,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144936_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":10.85},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":35.0}],"visits_number":3,"minimum_lapse":140.0,"activity":{"point_id":"1144936","duration":140,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144936_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":10.85},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":35.0}],"visits_number":3,"minimum_lapse":140.0,"activity":{"point_id":"1144936","duration":140,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134666_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134666","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1006725_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":3.15},{"unit_id":"l","value":38.4},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1006725","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":65700,"day_index":0},{"start":32400,"end":65700,"day_index":1},{"start":32400,"end":65700,"day_index":2},{"start":32400,"end":65700,"day_index":3},{"start":32400,"end":65700,"day_index":4}]},"type":"service"},{"id":"1006725_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":3.15},{"unit_id":"l","value":38.4},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1006725","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":65700,"day_index":0},{"start":32400,"end":65700,"day_index":1},{"start":32400,"end":65700,"day_index":2},{"start":32400,"end":65700,"day_index":3},{"start":32400,"end":65700,"day_index":4}]},"type":"service"},{"id":"1092502_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":2.01},{"unit_id":"l","value":12.6},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1092502","duration":24,"setup_duration":120,"timewindows":[{"start":30600,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":30600,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":30600,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":30600,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":30600,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1092502_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":2.01},{"unit_id":"l","value":12.6},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1092502","duration":24,"setup_duration":120,"timewindows":[{"start":30600,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":30600,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":30600,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":30600,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":30600,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1092502_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":2.01},{"unit_id":"l","value":12.6},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1092502","duration":24,"setup_duration":120,"timewindows":[{"start":30600,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":30600,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":30600,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":30600,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":30600,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1092502_CLI_ 84_3FF","quantities":[{"unit_id":"kg","value":2.01},{"unit_id":"l","value":12.6},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1092502","duration":24,"setup_duration":120,"timewindows":[{"start":30600,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":30600,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":30600,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":30600,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":30600,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1008001_TAP_ 28_3FF","quantities":[{"unit_id":"kg","value":7.5},{"unit_id":"l","value":29.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":115.0,"activity":{"point_id":"1008001","duration":115,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1006725_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":3.15},{"unit_id":"l","value":38.4},{"unit_id":"qte","value":3.0}],"visits_number":3,"minimum_lapse":24.0,"activity":{"point_id":"1006725","duration":24,"setup_duration":120,"timewindows":[{"start":32400,"end":65700,"day_index":0},{"start":32400,"end":65700,"day_index":1},{"start":32400,"end":65700,"day_index":2},{"start":32400,"end":65700,"day_index":3},{"start":32400,"end":65700,"day_index":4}]},"type":"service"},{"id":"1008001_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":7.5},{"unit_id":"l","value":29.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":115.0,"activity":{"point_id":"1008001","duration":115,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1008001_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":7.5},{"unit_id":"l","value":29.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":115.0,"activity":{"point_id":"1008001","duration":115,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1008001_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":7.5},{"unit_id":"l","value":29.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":115.0,"activity":{"point_id":"1008001","duration":115,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1144936_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":10.85},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":35.0}],"visits_number":3,"minimum_lapse":140.0,"activity":{"point_id":"1144936","duration":140,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144493_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1144493","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144493_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1144493","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144493_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":76.8},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1144493","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1147114_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":2.94},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1147114","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1147114_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":2.94},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":14.0}],"visits_number":3,"minimum_lapse":56.0,"activity":{"point_id":"1147114","duration":56,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1147721_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1147721_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1147721_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1147721_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1147721_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1147721_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":17.6},{"unit_id":"l","value":49.6},{"unit_id":"qte","value":8.0}],"visits_number":3,"minimum_lapse":104.0,"activity":{"point_id":"1147721","duration":104,"setup_duration":120,"timewindows":[{"start":32400,"end":46800,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":46800,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":46800,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":46800,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":46800,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1003152_SNC_ 42_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1110450_BOB_ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070260_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1070260","duration":40,"setup_duration":120,"timewindows":[{"start":25200,"end":64800,"day_index":0},{"start":25200,"end":64800,"day_index":1},{"start":25200,"end":64800,"day_index":2},{"start":25200,"end":64800,"day_index":3},{"start":25200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1110450_PH _ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1110450_TAP_ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134666_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134666","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134666_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134666","duration":16,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132451_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1132451","duration":90,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132451_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1132451","duration":90,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132451_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1132451","duration":90,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1132451_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1132451","duration":90,"setup_duration":120,"timewindows":[{"start":34200,"end":45000,"day_index":0},{"start":50400,"end":64800,"day_index":0},{"start":34200,"end":45000,"day_index":1},{"start":50400,"end":64800,"day_index":1},{"start":34200,"end":45000,"day_index":2},{"start":50400,"end":64800,"day_index":2},{"start":34200,"end":45000,"day_index":3},{"start":50400,"end":64800,"day_index":3},{"start":34200,"end":45000,"day_index":4},{"start":50400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1122595_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":28.6},{"unit_id":"l","value":80.6},{"unit_id":"qte","value":13.0}],"visits_number":3,"minimum_lapse":169.0,"activity":{"point_id":"1122595","duration":169,"setup_duration":120,"timewindows":[{"start":28800,"end":39600,"day_index":0},{"start":28800,"end":39600,"day_index":1},{"start":28800,"end":39600,"day_index":2},{"start":28800,"end":39600,"day_index":3},{"start":28800,"end":39600,"day_index":4}]},"type":"service"},{"id":"1122595_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":28.6},{"unit_id":"l","value":80.6},{"unit_id":"qte","value":13.0}],"visits_number":3,"minimum_lapse":169.0,"activity":{"point_id":"1122595","duration":169,"setup_duration":120,"timewindows":[{"start":28800,"end":39600,"day_index":0},{"start":28800,"end":39600,"day_index":1},{"start":28800,"end":39600,"day_index":2},{"start":28800,"end":39600,"day_index":3},{"start":28800,"end":39600,"day_index":4}]},"type":"service"},{"id":"1122595_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":28.6},{"unit_id":"l","value":80.6},{"unit_id":"qte","value":13.0}],"visits_number":3,"minimum_lapse":169.0,"activity":{"point_id":"1122595","duration":169,"setup_duration":120,"timewindows":[{"start":28800,"end":39600,"day_index":0},{"start":28800,"end":39600,"day_index":1},{"start":28800,"end":39600,"day_index":2},{"start":28800,"end":39600,"day_index":3},{"start":28800,"end":39600,"day_index":4}]},"type":"service"},{"id":"1110450_SAV_ 14_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1003152_TAP_ 7_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070260_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1070260","duration":40,"setup_duration":120,"timewindows":[{"start":25200,"end":64800,"day_index":0},{"start":25200,"end":64800,"day_index":1},{"start":25200,"end":64800,"day_index":2},{"start":25200,"end":64800,"day_index":3},{"start":25200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1070260_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1070260","duration":40,"setup_duration":120,"timewindows":[{"start":25200,"end":64800,"day_index":0},{"start":25200,"end":64800,"day_index":1},{"start":25200,"end":64800,"day_index":2},{"start":25200,"end":64800,"day_index":3},{"start":25200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134348_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134348","duration":16,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1134348_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134348","duration":16,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127201_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":14.28},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1127201","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134348_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134348","duration":16,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1127201_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":14.28},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":68.0}],"visits_number":3,"minimum_lapse":272.0,"activity":{"point_id":"1127201","duration":272,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138580_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":5.2},{"unit_id":"l","value":24.624},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1138580","duration":32,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138580_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":5.2},{"unit_id":"l","value":24.624},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1138580","duration":32,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138580_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":5.2},{"unit_id":"l","value":24.624},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1138580","duration":32,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1143039_TAP_ 14_3FF","quantities":[{"unit_id":"kg","value":26.56},{"unit_id":"l","value":105.6},{"unit_id":"qte","value":8.0}],"visits_number":6,"minimum_lapse":800.0,"activity":{"point_id":"1143039","duration":800,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1134348_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":4.4},{"unit_id":"l","value":4.0},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":16.0,"activity":{"point_id":"1134348","duration":16,"setup_duration":120,"timewindows":[{"start":30600,"end":61200,"day_index":0},{"start":30600,"end":61200,"day_index":1},{"start":30600,"end":61200,"day_index":2},{"start":30600,"end":61200,"day_index":3},{"start":30600,"end":61200,"day_index":4}]},"type":"service"},{"id":"1132224_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1132224","duration":4,"setup_duration":120,"timewindows":[{"start":34200,"end":70200,"day_index":0},{"start":34200,"end":70200,"day_index":1},{"start":34200,"end":70200,"day_index":2},{"start":34200,"end":70200,"day_index":3},{"start":34200,"end":70200,"day_index":4}]},"type":"service"},{"id":"1132224_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1132224","duration":4,"setup_duration":120,"timewindows":[{"start":34200,"end":70200,"day_index":0},{"start":34200,"end":70200,"day_index":1},{"start":34200,"end":70200,"day_index":2},{"start":34200,"end":70200,"day_index":3},{"start":34200,"end":70200,"day_index":4}]},"type":"service"},{"id":"1110450_PH _ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1110450_TAP_ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1095177_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":51.2},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1095177","duration":32,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1111407_TAP_ 14_3FF","quantities":[{"unit_id":"kg","value":37.5},{"unit_id":"l","value":145.0},{"unit_id":"qte","value":5.0}],"visits_number":6,"minimum_lapse":500.0,"activity":{"point_id":"1111407","duration":500,"setup_duration":120,"timewindows":[{"start":25200,"end":50400,"day_index":0},{"start":25200,"end":50400,"day_index":1},{"start":25200,"end":50400,"day_index":2},{"start":25200,"end":50400,"day_index":3},{"start":25200,"end":50400,"day_index":4}]},"type":"service"},{"id":"1117925_EMP_ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1117925","duration":48,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1117925_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1117925","duration":48,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1117925_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1117925","duration":48,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1117925_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":48.0,"activity":{"point_id":"1117925","duration":48,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1132224_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":1.1},{"unit_id":"l","value":1.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1132224","duration":4,"setup_duration":120,"timewindows":[{"start":34200,"end":70200,"day_index":0},{"start":34200,"end":70200,"day_index":1},{"start":34200,"end":70200,"day_index":2},{"start":34200,"end":70200,"day_index":3},{"start":34200,"end":70200,"day_index":4}]},"type":"service"},{"id":"1138580_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":5.2},{"unit_id":"l","value":24.624},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1138580","duration":32,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1135294_CLI_ 28_3FF","quantities":[{"unit_id":"kg","value":0.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1135294","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1135294_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":0.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1135294","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1135294_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":0.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1135294","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031534_PH _ 84_3FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":16.0,"activity":{"point_id":"1031534","duration":16,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1031534_SAV_ 84_3FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":16.0,"activity":{"point_id":"1031534","duration":16,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1047944_PH _ 14_3FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":31.0,"activity":{"point_id":"1047944","duration":31,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1047944_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":1.05},{"unit_id":"l","value":12.8},{"unit_id":"qte","value":1.0}],"visits_number":6,"minimum_lapse":31.0,"activity":{"point_id":"1047944","duration":31,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1050281_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":52.0,"activity":{"point_id":"1050281","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1054024_SNC_ 7_3FF","quantities":[{"unit_id":"kg","value":6.3},{"unit_id":"l","value":102.0},{"unit_id":"qte","value":3.0}],"visits_number":12,"minimum_lapse":270.0,"activity":{"point_id":"1054024","duration":270,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1050281_PCP_ 14_3FF","quantities":[{"unit_id":"kg","value":8.8},{"unit_id":"l","value":24.8},{"unit_id":"qte","value":4.0}],"visits_number":6,"minimum_lapse":52.0,"activity":{"point_id":"1050281","duration":52,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1040973_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":28.6},{"unit_id":"l","value":80.6},{"unit_id":"qte","value":13.0}],"visits_number":6,"minimum_lapse":186.0,"activity":{"point_id":"1040973","duration":186,"setup_duration":120,"timewindows":[{"start":21600,"end":75600,"day_index":0},{"start":21600,"end":75600,"day_index":1},{"start":21600,"end":75600,"day_index":2},{"start":21600,"end":75600,"day_index":3},{"start":21600,"end":75600,"day_index":4}]},"type":"service"},{"id":"1063338_SNC_ 7_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":34.0},{"unit_id":"qte","value":1.0}],"visits_number":12,"minimum_lapse":90.0,"activity":{"point_id":"1063338","duration":90,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1031534_CLI_ 84_3FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":16.0,"activity":{"point_id":"1031534","duration":16,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1070260_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":5.25},{"unit_id":"l","value":64.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":40.0,"activity":{"point_id":"1070260","duration":40,"setup_duration":120,"timewindows":[{"start":25200,"end":64800,"day_index":0},{"start":25200,"end":64800,"day_index":1},{"start":25200,"end":64800,"day_index":2},{"start":25200,"end":64800,"day_index":3},{"start":25200,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031534_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":16.0,"activity":{"point_id":"1031534","duration":16,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1031918_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":277.0,"activity":{"point_id":"1031918","duration":277,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1135294_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":0.1},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":4.0,"activity":{"point_id":"1135294","duration":4,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1138580_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":5.2},{"unit_id":"l","value":24.624},{"unit_id":"qte","value":4.0}],"visits_number":3,"minimum_lapse":32.0,"activity":{"point_id":"1138580","duration":32,"setup_duration":120,"timewindows":[{"start":32400,"end":64800,"day_index":0},{"start":32400,"end":64800,"day_index":1},{"start":32400,"end":64800,"day_index":2},{"start":32400,"end":64800,"day_index":3},{"start":32400,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145151_EMP_ 14_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145151_PH _ 14_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1054036_SNC_ 7_3FF","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":180.0,"activity":{"point_id":"1054036","duration":180,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1003152_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1003152_ASC_ 28_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1003152_TAP_ 7_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1003152_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1031534_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":2.1},{"unit_id":"l","value":25.6},{"unit_id":"qte","value":2.0}],"visits_number":1,"minimum_lapse":16.0,"activity":{"point_id":"1031534","duration":16,"setup_duration":120,"timewindows":[{"start":32400,"end":68400,"day_index":0},{"start":32400,"end":68400,"day_index":1},{"start":32400,"end":68400,"day_index":2},{"start":32400,"end":68400,"day_index":3},{"start":32400,"end":68400,"day_index":4}]},"type":"service"},{"id":"1110450_BOB_ 7_3FF","quantities":[{"unit_id":"kg","value":46.2},{"unit_id":"l","value":130.2},{"unit_id":"qte","value":21.0}],"visits_number":12,"minimum_lapse":273.0,"activity":{"point_id":"1110450","duration":273,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004708_LPL_ 28_3FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":182.0,"activity":{"point_id":"1004708","duration":182,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1004708_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":0.4},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":5.0}],"visits_number":3,"minimum_lapse":182.0,"activity":{"point_id":"1004708","duration":182,"setup_duration":120,"timewindows":[{"start":32400,"end":43200,"day_index":0},{"start":50400,"end":61200,"day_index":0},{"start":32400,"end":43200,"day_index":1},{"start":50400,"end":61200,"day_index":1},{"start":32400,"end":43200,"day_index":2},{"start":50400,"end":61200,"day_index":2},{"start":32400,"end":43200,"day_index":3},{"start":50400,"end":61200,"day_index":3},{"start":32400,"end":43200,"day_index":4},{"start":50400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1145151_EMP_ 14_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1054036_SNC_ 7_3FF","quantities":[{"unit_id":"kg","value":4.2},{"unit_id":"l","value":68.0},{"unit_id":"qte","value":2.0}],"visits_number":12,"minimum_lapse":180.0,"activity":{"point_id":"1054036","duration":180,"setup_duration":120,"timewindows":[{"start":27000,"end":72000,"day_index":0},{"start":27000,"end":72000,"day_index":1},{"start":27000,"end":72000,"day_index":2},{"start":27000,"end":72000,"day_index":3},{"start":27000,"end":72000,"day_index":4}]},"type":"service"},{"id":"1003152_TAP_ 7_3FF","quantities":[{"unit_id":"kg","value":7.2},{"unit_id":"l","value":141.0},{"unit_id":"qte","value":3.0}],"visits_number":2,"minimum_lapse":270.0,"activity":{"point_id":"1003152","duration":270,"setup_duration":120,"timewindows":[{"start":27900,"end":64800,"day_index":0},{"start":27900,"end":64800,"day_index":1},{"start":27900,"end":64800,"day_index":2},{"start":27900,"end":64800,"day_index":3},{"start":27900,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145151_PH _ 14_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002561_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":46.0,"activity":{"point_id":"1002561","duration":46,"setup_duration":120,"timewindows":[{"start":29700,"end":64800,"day_index":0},{"start":29700,"end":64800,"day_index":1},{"start":29700,"end":64800,"day_index":2},{"start":29700,"end":64800,"day_index":3},{"start":29700,"end":64800,"day_index":4}]},"type":"service"},{"id":"1002561_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":46.0,"activity":{"point_id":"1002561","duration":46,"setup_duration":120,"timewindows":[{"start":29700,"end":64800,"day_index":0},{"start":29700,"end":64800,"day_index":1},{"start":29700,"end":64800,"day_index":2},{"start":29700,"end":64800,"day_index":3},{"start":29700,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005880_EMP_ 42_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":41.72},{"unit_id":"qte","value":7.0}],"visits_number":2,"minimum_lapse":61.0,"activity":{"point_id":"1005880","duration":61,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005880_SAV_ 42_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":41.72},{"unit_id":"qte","value":7.0}],"visits_number":2,"minimum_lapse":61.0,"activity":{"point_id":"1005880","duration":61,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1002561_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":9.6},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":3,"minimum_lapse":46.0,"activity":{"point_id":"1002561","duration":46,"setup_duration":120,"timewindows":[{"start":29700,"end":64800,"day_index":0},{"start":29700,"end":64800,"day_index":1},{"start":29700,"end":64800,"day_index":2},{"start":29700,"end":64800,"day_index":3},{"start":29700,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145151_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1145151_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":7.8},{"unit_id":"l","value":36.936},{"unit_id":"qte","value":6.0}],"visits_number":6,"minimum_lapse":48.0,"activity":{"point_id":"1145151","duration":48,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144485_LPL_ 28_3FF","quantities":[{"unit_id":"kg","value":2.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":32.0}],"visits_number":3,"minimum_lapse":416.0,"activity":{"point_id":"1144485","duration":416,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1116199_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":18.46},{"unit_id":"l","value":42.0},{"unit_id":"qte","value":76.0}],"visits_number":3,"minimum_lapse":304.0,"activity":{"point_id":"1116199","duration":304,"setup_duration":120,"timewindows":[{"start":33300,"end":61200,"day_index":0},{"start":33300,"end":61200,"day_index":1},{"start":33300,"end":61200,"day_index":2},{"start":33300,"end":61200,"day_index":3},{"start":33300,"end":61200,"day_index":4}]},"type":"service"},{"id":"1123435_SNC_ 28_3FF","quantities":[{"unit_id":"kg","value":2.4},{"unit_id":"l","value":47.0},{"unit_id":"qte","value":1.0}],"visits_number":3,"minimum_lapse":90.0,"activity":{"point_id":"1123435","duration":90,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124213_BOB_ 28_3FF","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":78.0,"activity":{"point_id":"1124213","duration":78,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124213_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":78.0,"activity":{"point_id":"1124213","duration":78,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1124213_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":13.2},{"unit_id":"l","value":37.2},{"unit_id":"qte","value":6.0}],"visits_number":3,"minimum_lapse":78.0,"activity":{"point_id":"1124213","duration":78,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144485_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":2.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":32.0}],"visits_number":3,"minimum_lapse":416.0,"activity":{"point_id":"1144485","duration":416,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144485_CLI_ 28_3FF","quantities":[{"unit_id":"kg","value":2.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":32.0}],"visits_number":3,"minimum_lapse":416.0,"activity":{"point_id":"1144485","duration":416,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1144485_PCP_ 28_3FF","quantities":[{"unit_id":"kg","value":2.56},{"unit_id":"l","value":0.0},{"unit_id":"qte","value":32.0}],"visits_number":3,"minimum_lapse":416.0,"activity":{"point_id":"1144485","duration":416,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1143039_TAP_ 14_3FF","quantities":[{"unit_id":"kg","value":26.56},{"unit_id":"l","value":105.6},{"unit_id":"qte","value":8.0}],"visits_number":6,"minimum_lapse":800.0,"activity":{"point_id":"1143039","duration":800,"setup_duration":120,"timewindows":[{"start":28800,"end":64800,"day_index":0},{"start":28800,"end":64800,"day_index":1},{"start":28800,"end":64800,"day_index":2},{"start":28800,"end":64800,"day_index":3},{"start":28800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1005880_PH _ 42_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":41.72},{"unit_id":"qte","value":7.0}],"visits_number":2,"minimum_lapse":61.0,"activity":{"point_id":"1005880","duration":61,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1005880_SNC_ 42_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":41.72},{"unit_id":"qte","value":7.0}],"visits_number":2,"minimum_lapse":61.0,"activity":{"point_id":"1005880","duration":61,"setup_duration":120,"timewindows":[{"start":32400,"end":61200,"day_index":0},{"start":32400,"end":61200,"day_index":1},{"start":32400,"end":61200,"day_index":2},{"start":32400,"end":61200,"day_index":3},{"start":32400,"end":61200,"day_index":4}]},"type":"service"},{"id":"1031918_BOB_ 14_3FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":277.0,"activity":{"point_id":"1031918","duration":277,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1031918_PH _ 28_3FF","quantities":[{"unit_id":"kg","value":22.0},{"unit_id":"l","value":62.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":277.0,"activity":{"point_id":"1031918","duration":277,"setup_duration":120,"timewindows":[{"start":32400,"end":63000,"day_index":0},{"start":32400,"end":63000,"day_index":1},{"start":32400,"end":63000,"day_index":2},{"start":32400,"end":63000,"day_index":3},{"start":32400,"end":63000,"day_index":4}]},"type":"service"},{"id":"1004647_SNC_ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_SAV_ 28_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_PH _ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"},{"id":"1004647_PCP_ 14_3FF","quantities":[{"unit_id":"kg","value":10.5},{"unit_id":"l","value":128.0},{"unit_id":"qte","value":10.0}],"visits_number":6,"minimum_lapse":80.0,"activity":{"point_id":"1004647","duration":80,"setup_duration":120,"timewindows":[{"start":37800,"end":64800,"day_index":0},{"start":37800,"end":64800,"day_index":1},{"start":37800,"end":64800,"day_index":2},{"start":37800,"end":64800,"day_index":3},{"start":37800,"end":64800,"day_index":4}]},"type":"service"}],"vehicles":[{"id":"vehicule1","start_point_id":"startvehicule1","end_point_id":"endvehicule1","router_mode":"car","speed_multiplier":0.75,"cost_time_multiplier":1.0,"router_dimension":"time","skills":[["vehicule1"]],"sequence_timewindows":[{"start":21600,"end":45000,"day_index":0},{"start":21600,"end":45000,"day_index":1},{"start":21600,"end":45000,"day_index":2},{"start":21600,"end":45000,"day_index":3},{"start":21600,"end":45000,"day_index":4}],"capacities":[{"unit_id":"kg","limit":850.0},{"unit_id":"l","limit":7435.0},{"unit_id":"qte","limit":9999.0}],"unavailable_work_day_indices":[5,6],"traffic":true,"track":true,"motorway":true,"toll":true,"max_walk_distance":750,"approach":"unrestricted"},{"id":"vehicule2","start_point_id":"startvehicule2","end_point_id":"endvehicule2","router_mode":"car","speed_multiplier":0.75,"cost_time_multiplier":1.0,"router_dimension":"time","skills":"vehicule1,vehicule2","sequence_timewindows":[{"start":21600,"end":45000,"day_index":0},{"start":21600,"end":45000,"day_index":1},{"start":21600,"end":45000,"day_index":2},{"start":21600,"end":45000,"day_index":3},{"start":21600,"end":45000,"day_index":4}],"capacities":[{"unit_id":"kg","limit":1210.0},{"unit_id":"l","limit":6254.0},{"unit_id":"qte","limit":9999.0}],"unavailable_work_day_indices":[5,6],"traffic":true,"track":true,"motorway":true,"toll":true,"max_walk_distance":750,"approach":"unrestricted"}],"configuration":{"preprocessing":{"use_periodic_heuristic":true,"prefer_short_segment":true,"partition_method":"balanced_kmeans","partition_metric":"duration"},"resolution":{"same_point_day":true,"solver_parameter":-1,"duration":225000,"initial_time_out":112500,"time_out_multiplier":2},"schedule":{"range_indices":{"start":0,"end":83}},"restitution":{"csv":true,"intermediate_solutions":false}}}} ================================================ FILE: benchmark/simple.rb ================================================ # frozen_string_literal: true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/ips' class API < Grape::API prefix :api version 'v1', using: :path get '/' do 'hello' end end options = { method: Rack::GET } env = Rack::MockRequest.env_for('/api/v1', options) 10.times do |i| env["HTTP_HEADER#{i}"] = '123' end Benchmark.ips do |ips| ips.report('simple') do API.call env end end ================================================ FILE: docker/Dockerfile ================================================ ARG RUBY_VERSION=4 FROM ruby:${RUBY_VERSION}-slim ENV BUNDLE_PATH /usr/local/bundle/gems ENV LIB_PATH /var/grape RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential curl git pkg-config libyaml-dev libjemalloc2 && \ gem update --system && gem install bundler ENV LD_PRELOAD libjemalloc.so.2 ENV MALLOC_CONF dirty_decay_ms:1000,narenas:2,background_thread:true ENV RUBYOPT --enable-frozen-string-literal --yjit WORKDIR $LIB_PATH COPY /docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh"] ================================================ FILE: docker/entrypoint.sh ================================================ #!/bin/sh set -e # Useful information echo -e "$(ruby --version)\nrubygems $(gem --version)\n$(bundle version)" if [ -z "${GEMFILE}" ] then echo "Running default Gemfile" else export BUNDLE_GEMFILE="./gemfiles/${GEMFILE}.gemfile" echo "Running gemfile: ${GEMFILE}" fi # Keep gems in the latest possible state (bundle check || bundle install) && bundle update && exec bundle exec ${@} ================================================ FILE: docker-compose.yml ================================================ volumes: gems: services: grape: build: context: . dockerfile: docker/Dockerfile args: - RUBY_VERSION=${RUBY_VERSION:-4.0} stdin_open: true tty: true volumes: - .:/var/grape - gems:/usr/local/bundle ================================================ FILE: gemfiles/dry_validation.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'dry-validation' ================================================ FILE: gemfiles/grape_entity.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'grape-entity' ================================================ FILE: gemfiles/hashie.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'hashie' ================================================ FILE: gemfiles/multi_json.gemfile ================================================ # frozen_string_literal: true gem 'multi_json' eval_gemfile '../Gemfile' ================================================ FILE: gemfiles/multi_xml.gemfile ================================================ # frozen_string_literal: true gem 'multi_xml' eval_gemfile '../Gemfile' ================================================ FILE: gemfiles/rack_2_2.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rack', '~> 2.2.0' ================================================ FILE: gemfiles/rack_3_0.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rack', '~> 3.0.0' ================================================ FILE: gemfiles/rack_3_1.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rack', '~> 3.1.0' ================================================ FILE: gemfiles/rack_3_2.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rack', '~> 3.2.0' ================================================ FILE: gemfiles/rack_edge.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rack', github: 'rack/rack' ================================================ FILE: gemfiles/rails_7_2.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rails', '~> 7.2.0' gem 'tzinfo-data', require: false ================================================ FILE: gemfiles/rails_8_0.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rails', '~> 8.0.0' gem 'tzinfo-data', require: false ================================================ FILE: gemfiles/rails_8_1.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rails', '~> 8.1.0' gem 'tzinfo-data', require: false ================================================ FILE: gemfiles/rails_edge.gemfile ================================================ # frozen_string_literal: true eval_gemfile '../Gemfile' gem 'rails', github: 'rails/rails' gem 'tzinfo-data', require: false ================================================ FILE: grape.gemspec ================================================ # frozen_string_literal: true require_relative 'lib/grape/version' Gem::Specification.new do |s| s.name = 'grape' s.version = Grape::VERSION s.platform = Gem::Platform::RUBY s.authors = ['Michael Bleigh'] s.email = ['michael@intridea.com'] s.homepage = 'https://github.com/ruby-grape/grape' s.summary = 'A simple Ruby framework for building REST-like APIs.' s.description = 'A Ruby framework for rapid API development with great conventions.' s.license = 'MIT' s.metadata = { 'bug_tracker_uri' => 'https://github.com/ruby-grape/grape/issues', 'changelog_uri' => "https://github.com/ruby-grape/grape/blob/v#{s.version}/CHANGELOG.md", 'documentation_uri' => "https://www.rubydoc.info/gems/grape/#{s.version}", 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}", 'rubygems_mfa_required' => 'true' } s.add_dependency 'activesupport', '>= 7.2' s.add_dependency 'dry-configurable' s.add_dependency 'dry-types', '>= 1.1' s.add_dependency 'mustermann-grape', '~> 1.1.0' s.add_dependency 'rack', '>= 2' s.add_dependency 'zeitwerk' s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec'] s.require_paths = ['lib'] s.required_ruby_version = '>= 3.2' end ================================================ FILE: lib/grape/api/instance.rb ================================================ # frozen_string_literal: true module Grape class API # The API Instance class, is the engine behind Grape::API. Each class that inherits # from this will represent a different API instance class Instance extend Grape::DSL::Settings extend Grape::DSL::Desc extend Grape::DSL::Validations extend Grape::DSL::Callbacks extend Grape::DSL::Logger extend Grape::DSL::Middleware extend Grape::DSL::RequestResponse extend Grape::DSL::Routing extend Grape::DSL::Helpers extend Grape::Middleware::Auth::DSL Boolean = Grape::API::Boolean class << self extend Forwardable attr_accessor :configuration def_delegators :@base, :to_s def base=(grape_api) @base = grape_api grape_api.instances << self end def base_instance? self == @base.base_instance end # A class-level lock to ensure the API is not compiled by multiple # threads simultaneously within the same process. LOCK = Mutex.new # Clears all defined routes, endpoints, etc., on this API. def reset! reset_endpoints! reset_routes! reset_validations! end # This is the interface point between Rack and Grape; it accepts a request # from Rack and ultimately returns an array of three values: the status, # the headers, and the body. See [the rack specification] # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more. def call(env) compile! @instance.call(env) end def compile! return if @instance LOCK.synchronize { @instance ||= new } end # see Grape::Router#recognize_path def recognize_path(path) compile! @instance.router.recognize_path(path) end # Wipe the compiled API so we can recompile after changes were made. def change! @instance = nil end protected def inherit_settings(other_settings) top_level_setting.inherit_from other_settings.point_in_time_copy # Propagate any inherited params down to our endpoints, and reset any # compiled routes. endpoints.each do |e| e.inherit_settings(top_level_setting.namespace_stackable) e.reset_routes! end reset_routes! end private def inherited(subclass) super subclass.reset! subclass.logger logger.clone end end attr_reader :router # Builds the routes from the defined endpoints, effectively compiling # this API into a usable form. def initialize @router = Router.new add_head_not_allowed_methods_and_options_methods self.class.endpoints.each do |endpoint| endpoint.mount_in(@router) end @router.compile! @router.freeze end # Handle a request. See Rack documentation for what `env` is. def call(env) status, headers, response = @router.call(env) unless cascade? headers = Grape::Util::Header.new.merge(headers) headers.delete('X-Cascade') end [status, headers, response] end # Some requests may return a HTTP 404 error if grape cannot find a matching # route. In this case, Grape::Router adds a X-Cascade header to the response # and sets it to 'pass', indicating to grape's parents they should keep # looking for a matching route on other resources. # # In some applications (e.g. mounting grape on rails), one might need to trap # errors from reaching upstream. This is effectivelly done by unsetting # X-Cascade. Default :cascade is true. def cascade? namespace_inheritable = self.class.inheritable_setting.namespace_inheritable return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade) return namespace_inheritable[:version_options][:cascade] if namespace_inheritable[:version_options]&.key?(:cascade) true end reset! private # For every resource add a 'OPTIONS' route that returns an HTTP 204 response # with a list of HTTP methods that can be called. Also add a route that # will return an HTTP 405 response for any HTTP method that the resource # cannot handle. def add_head_not_allowed_methods_and_options_methods # The paths we collected are prepared (cf. Path#prepare), so they # contain already versioning information when using path versioning. all_routes = self.class.endpoints.flat_map(&:routes) # Disable versioning so adding a route won't prepend versioning # informations again. without_root_prefix_and_versioning { collect_route_config_per_pattern(all_routes) } end def collect_route_config_per_pattern(all_routes) routes_by_regexp = all_routes.group_by(&:pattern_regexp) namespace_inheritable = self.class.inheritable_setting.namespace_inheritable # Build the configuration based on the first endpoint and the collection of methods supported. routes_by_regexp.each_value do |routes| next if routes.any? { |route| route.request_method == '*' } last_route = routes.last # Most of the configuration is taken from the last endpoint allowed_methods = routes.map(&:request_method) allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET) allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods last_route.app.options[:options_route_enabled] = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS) greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header: allow_header) @router.associate_routes(greedy_route) end end ROOT_PREFIX_VERSIONING_KEY = %i[version version_options root_prefix].freeze private_constant :ROOT_PREFIX_VERSIONING_KEY # Allows definition of endpoints that ignore the versioning configuration # used by the rest of your API. def without_root_prefix_and_versioning inheritable_setting = self.class.inheritable_setting deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEY) yield ensure ROOT_PREFIX_VERSIONING_KEY.each_with_index do |key, index| inheritable_setting.namespace_inheritable[key] = deleted_values[index] end end end end end ================================================ FILE: lib/grape/api.rb ================================================ # frozen_string_literal: true module Grape # The API class is the primary entry point for creating Grape APIs. Users # should subclass this class in order to build an API. class API # Class methods that we want to call on the API rather than on the API object NON_OVERRIDABLE = %i[base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting= top_level_setting].freeze Helpers = Grape::DSL::Helpers::BaseHelper class Boolean def self.build(val) return nil if val != true && val != false new end end class << self extend Forwardable attr_accessor :base_instance, :instances delegate_missing_to :base_instance # This is the interface point between Rack and Grape; it accepts a request # from Rack and ultimately returns an array of three values: the status, # the headers, and the body. See [the rack specification] # (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more. # NOTE: This will only be called on an API directly mounted on RACK def_delegators :base_instance, :new, :configuration, :call, :change!, :compile!, :recognize_path, :routes # Initialize the instance variables on the remountable class, and the base_instance # an instance that will be used to create the set up but will not be mounted def initial_setup(base_instance_parent) @instances = [] @setup = [] @base_parent = base_instance_parent @base_instance = mount_instance end # Redefines all methods so that are forwarded to add_setup and be recorded def override_all_methods! (base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override| define_singleton_method(method_override) do |*args, **kwargs, &block| add_setup(method: method_override, args: args, kwargs: kwargs, block: block) end end end # Configure an API from the outside. If a block is given, it'll pass a # configuration hash to the block which you can use to configure your # API. If no block is given, returns the configuration hash. # The configuration set here is accessible from inside an API with # `configuration` as normal. def configure config = @base_instance.configuration if block_given? yield config self else config end end # The remountable class can have a configuration hash to provide some dynamic class-level variables. # For instance, a description could be done using: `desc configuration[:description]` if it may vary # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration # too much, you may actually want to provide a new API rather than remount it. def mount_instance(configuration: nil) Class.new(@base_parent).tap do |instance| instance.configuration = Grape::Util::EndpointConfiguration.new(configuration || {}) instance.base = self replay_setup_on(instance) end end private # When inherited, will create a list of all instances (times the API was mounted) # It will listen to the setup required to mount that endpoint, and replicate it on any new instance def inherited(api) super api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance) api.override_all_methods! end # Replays the set up to produce an API as defined in this class, can be called # on classes that inherit from Grape::API def replay_setup_on(instance) @setup.each do |setup_step| replay_step_on(instance, **setup_step) end end # Adds a new stage to the set up require to get a Grape::API up and running def add_setup(**step) @setup << step last_response = nil @instances.each do |instance| last_response = replay_step_on(instance, **step) end refresh_mount_step if step[:method] != :mount last_response end # Updating all previously mounted classes in the case that new methods have been executed. def refresh_mount_step @setup.each do |setup_step| next if setup_step[:method] != :mount refresh_mount_step = setup_step.merge(method: :refresh_mounted_api) @setup << refresh_mount_step @instances.each do |instance| replay_step_on(instance, **refresh_mount_step) end end end def replay_step_on(instance, method:, args:, kwargs:, block:) return if skip_immediate_run?(instance, args, kwargs) eval_args = evaluate_arguments(instance.configuration, *args) eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first } response = instance.__send__(method, *eval_args, **eval_kwargs, &block) if skip_immediate_run?(instance, [response], kwargs) response else evaluate_arguments(instance.configuration, response).first end end # Skips steps that contain arguments to be lazily executed (on re-mount time) def skip_immediate_run?(instance, args, kwargs) instance.base_instance? && (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) } || any_lazy?(kwargs.values)) end def any_lazy?(args) args.any? { |argument| argument_lazy?(argument) } end def evaluate_arguments(configuration, *args) args.map do |argument| if argument_lazy?(argument) argument.evaluate_from(configuration) elsif argument.is_a?(Hash) argument.transform_values { |value| evaluate_arguments(configuration, value).first } elsif argument.is_a?(Array) evaluate_arguments(configuration, *argument) else argument end end end def argument_lazy?(argument) argument.respond_to?(:lazy?) && argument.lazy? end end end end ================================================ FILE: lib/grape/content_types.rb ================================================ # frozen_string_literal: true module Grape module ContentTypes module_function # Content types are listed in order of preference. DEFAULTS = { xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', binary: 'application/octet-stream', txt: 'text/plain' }.freeze MIME_TYPES = Grape::ContentTypes::DEFAULTS.except(:serializable_hash).invert.freeze def content_types_for(from_settings) from_settings.presence || DEFAULTS end def mime_types_for(from_settings) return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS from_settings.invert.transform_keys! { |k| k.include?(';') ? k.split(';', 2).first : k } end end end ================================================ FILE: lib/grape/cookies.rb ================================================ # frozen_string_literal: true module Grape class Cookies extend Forwardable DELETED_COOKIES_ATTRS = { max_age: '0', value: '', expires: Time.at(0) }.freeze def_delegators :cookies, :[], :each def initialize(rack_cookies) @cookies = rack_cookies @send_cookies = nil end def response_cookies return unless @send_cookies send_cookies.each do |name| yield name, cookies[name] end end def []=(name, value) cookies[name] = value send_cookies << name end # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340 def delete(name, **opts) self.[]=(name, opts.merge(DELETED_COOKIES_ATTRS)) end private def cookies return @cookies unless @cookies.is_a?(Proc) @cookies = @cookies.call.with_indifferent_access end def send_cookies @send_cookies ||= Set.new end end end ================================================ FILE: lib/grape/declared_params_handler.rb ================================================ # frozen_string_literal: true module Grape class DeclaredParamsHandler def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map: nil) @include_missing = include_missing @evaluate_given = evaluate_given @stringify = stringify @contract_key_map = contract_key_map end def call(passed_params, declared_params, route_params, renamed_params) recursive_declared( passed_params, declared_params: declared_params, route_params: route_params, renamed_params: renamed_params ) end private def recursive_declared(passed_params, declared_params:, route_params:, renamed_params:, params_nested_path: []) res = if passed_params.is_a?(Array) passed_params.map do |passed_param| recursive_declared(passed_param, declared_params:, params_nested_path:, renamed_params:, route_params:) end else declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:) end @contract_key_map&.each { |key_map| key_map.write(passed_params, res) } res end def declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:) declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo| next if @evaluate_given && !declared_param_attr.scope.attr_meets_dependency?(passed_params) declared_hash_attr( passed_params, declared_param: declared_param_attr.key, params_nested_path:, memo:, renamed_params:, route_params: ) end end def declared_hash_attr(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:) if declared_param.is_a?(Hash) declared_param.each_pair do |declared_parent_param, declared_children_params| next unless @include_missing || passed_params.key?(declared_parent_param) memo_key = build_memo_key(params_nested_path, declared_parent_param, renamed_params) passed_children_params = passed_params[declared_parent_param] || passed_params.class.new params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_parent_param.to_s memo[memo_key] = handle_passed_param(params_nested_path_dup, route_params:, has_passed_children: passed_children_params.any?) do recursive_declared( passed_children_params, declared_params: declared_children_params, params_nested_path: params_nested_path_dup, renamed_params:, route_params: ) end end else # If it is not a Hash then it does not have children. # Find its value or set it to nil. return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param)) memo_key = build_memo_key(params_nested_path, declared_param, renamed_params) passed_param = passed_params[declared_param] params_nested_path_dup = params_nested_path.dup params_nested_path_dup << declared_param.to_s memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup, route_params:) do passed_param end end end def build_memo_key(params_nested_path, declared_param, renamed_params) rename_path = params_nested_path + [declared_param.to_s] renamed_param_name = renamed_params[rename_path] param = renamed_param_name || declared_param @stringify ? param.to_s : param.to_sym end def handle_passed_param(params_nested_path, route_params:, has_passed_children: false, &_block) return yield if has_passed_children key = params_nested_path[0] key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1 type = route_params.dig(key, :type) has_children = route_params.keys.any? { |k| k != key && k.start_with?("#{key}[") } if type == 'Hash' && !has_children {} elsif type == 'Array' || (type&.start_with?('[') && !type.include?(',')) [] elsif type == 'Set' || type&.start_with?('# DryTypes::Strict::Bool, BigDecimal => DryTypes::Strict::Decimal, Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal, TrueClass => DryTypes::Strict::Bool.constrained(eql: true), FalseClass => DryTypes::Strict::Bool.constrained(eql: false) }.freeze def initialize super @cache = Hash.new do |h, strict_type| h[strict_type] = MAPPING.fetch(strict_type) do DryTypes.wrapped_dry_types_const_get(DryTypes::Strict, strict_type) end end end end class ParamsCache < Grape::Util::Cache MAPPING = { Grape::API::Boolean => DryTypes::Params::Bool, BigDecimal => DryTypes::Params::Decimal, Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal, TrueClass => DryTypes::Params::Bool.constrained(eql: true), FalseClass => DryTypes::Params::Bool.constrained(eql: false), String => DryTypes::Coercible::String }.freeze def initialize super @cache = Hash.new do |h, params_type| h[params_type] = MAPPING.fetch(params_type) do DryTypes.wrapped_dry_types_const_get(DryTypes::Params, params_type) end end end end def self.wrapped_dry_types_const_get(dry_type, type) dry_type.const_get(type.name, false) rescue NameError raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[]) end end end ================================================ FILE: lib/grape/dsl/callbacks.rb ================================================ # frozen_string_literal: true module Grape module DSL module Callbacks # before: execute the given block before validation, coercion, or any endpoint # before_validation: execute the given block after `before`, but prior to validation or coercion # after_validation: execute the given block after validations and coercions, but before any endpoint code # after: execute the given block after the endpoint code has run except in unsuccessful # finally: execute the given block after the endpoint code even if unsuccessful %w[before before_validation after_validation after finally].each do |callback_method| define_method callback_method.to_sym do |&block| inheritable_setting.namespace_stackable[callback_method.pluralize.to_sym] = block end end end end end ================================================ FILE: lib/grape/dsl/declared.rb ================================================ # frozen_string_literal: true module Grape module DSL module Declared # Denotes a situation where a DSL method has been invoked in a # filter which it should not yet be available in class MethodNotYetAvailable < StandardError def initialize(msg = '#declared is not available prior to parameter validation') super end end # A filtering method that will return a hash # consisting only of keys that have been declared by a # `params` statement against the current/target endpoint or parent # namespaces. # @param params [Hash] The initial hash to filter. Usually this will just be `params` # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces` # options. `:include_parent_namespaces` defaults to true, hence must be set to false if # you want only to return params declared against the current/target endpoint. def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false) raise MethodNotYetAvailable unless before_filter_passed contract_key_map = inheritable_setting.namespace_stackable[:contract_key_map] handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map: contract_key_map) declared_params = include_parent_namespaces ? inheritable_setting.route[:declared_params] : (inheritable_setting.namespace_stackable[:declared_params].last || []) renamed_params = inheritable_setting.route[:renamed_params] || {} route_params = options.dig(:route_options, :params) || {} # options = endpoint's option handler.call(passed_params, declared_params, route_params, renamed_params) end end end end ================================================ FILE: lib/grape/dsl/desc.rb ================================================ # frozen_string_literal: true module Grape module DSL module Desc extend Grape::DSL::Settings # Add a description to the next namespace or function. # @param description [String] descriptive string for this endpoint # or namespace # @param options [Hash] other properties you can set to describe the # endpoint or namespace. Optional. # @option options :detail [String] additional detail about this endpoint # @option options :summary [String] summary for this endpoint # @option options :params [Hash] param types and info. normally, you set # these via the `params` dsl method. # @option options :entity [Grape::Entity] the entity returned upon a # successful call to this action # @option options :http_codes [Array[Array]] possible HTTP codes this # endpoint may return, with their meanings, in a 2d array # @option options :named [String] a specific name to help find this route # @option options :body_name [String] override the autogenerated body name param # @option options :headers [Hash] HTTP headers this method can accept # @option options :hidden [Boolean] hide the endpoint or not # @option options :deprecated [Boolean] deprecate the endpoint or not # @option options :is_array [Boolean] response entity is array or not # @option options :nickname [String] nickname of the endpoint # @option options :produces [Array[String]] a list of MIME types the endpoint produce # @option options :consumes [Array[String]] a list of MIME types the endpoint consume # @option options :security [Array[Hash]] a list of security schemes # @option options :tags [Array[String]] a list of tags # @yield a block yielding an instance context with methods mapping to # each of the above, except that :entity is also aliased as #success # and :http_codes is aliased as #failure. # # @example # # desc 'create a user' # post '/users' do # # ... # end # # desc 'find a user' do # detail 'locates the user from the given user ID' # failure [ [404, 'Couldn\'t find the given user' ] ] # success User::Entity # end # get '/user/:id' do # # ... # end # def desc(description, options = {}, &config_block) settings = if config_block endpoint_config = defined?(configuration) ? configuration : nil Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings else options.merge(description: description) end inheritable_setting.namespace[:description] = settings inheritable_setting.route[:description] = settings end end end end ================================================ FILE: lib/grape/dsl/headers.rb ================================================ # frozen_string_literal: true module Grape module DSL module Headers # This method has four responsibilities: # 1. Set a specifc header value by key # 2. Retrieve a specifc header value by key # 3. Retrieve all headers that have been set # 4. Delete a specifc header key-value pair def header(key = nil, val = nil) if key val ? header[key] = val : header.delete(key) else @header ||= Grape::Util::Header.new end end alias headers header end end end ================================================ FILE: lib/grape/dsl/helpers.rb ================================================ # frozen_string_literal: true module Grape module DSL module Helpers # Add helper methods that will be accessible from any # endpoint within this namespace (and child namespaces). # # When called without a block, all known helpers within this scope # are included. # # @param [Array] new_modules optional array of modules to include # @param [Block] block optional block of methods to include # # @example Define some helpers. # # class ExampleAPI < Grape::API # helpers do # def current_user # User.find_by_id(params[:token]) # end # end # end # # @example Include many modules # # class ExampleAPI < Grape::API # helpers Authentication, Mailer, OtherModule # end # def helpers(*new_modules, &block) include_new_modules(new_modules) include_block(block) include_all_in_scope if !block && new_modules.empty? end private def include_new_modules(modules) return if modules.empty? modules.each { |mod| make_inclusion(mod) } end def include_block(block) return unless block Module.new.tap do |mod| make_inclusion(mod) { mod.class_eval(&block) } end end def make_inclusion(mod, &) define_boolean_in_mod(mod) inject_api_helpers_to_mod(mod, &) inheritable_setting.namespace_stackable[:helpers] = mod end def include_all_in_scope Module.new.tap do |mod| namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include } change! end end def define_boolean_in_mod(mod) return if defined? mod::Boolean mod.const_set(:Boolean, Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &block) mod.extend(BaseHelper) unless mod.is_a?(BaseHelper) yield if block mod.api_changed(self) end # This module extends user defined helpers # to provide some API-specific functionality. module BaseHelper attr_accessor :api def params(name, &block) @named_params ||= {} @named_params[name] = block end def api_changed(new_api) @api = new_api process_named_params end protected def process_named_params return if @named_params.blank? api.inheritable_setting.namespace_stackable[:named_params] = @named_params end end end end end ================================================ FILE: lib/grape/dsl/inside_route.rb ================================================ # frozen_string_literal: true module Grape module DSL module InsideRoute include Declared # Backward compatibility: alias exception class to previous location MethodNotYetAvailable = Declared::MethodNotYetAvailable # The API version as specified in the URL. def version env[Grape::Env::API_VERSION] end def configuration options[:for].configuration.evaluate end # End the request and display an error to the # end user with the specified message. # # @param message [String] The message to display. # @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set. # @param additional_headers [Hash] Addtional headers for the response. # @param backtrace [Array] The backtrace of the exception that caused the error. # @param original_exception [Exception] The original exception that caused the error. def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil) status = self.status(status || inheritable_setting.namespace_inheritable[:default_error_status]) headers = additional_headers.present? ? header.merge(additional_headers) : header throw :error, message: message, status: status, headers: headers, backtrace: backtrace, original_exception: original_exception end # Redirect to a new url. # # @param url [String] The url to be redirect. # @param permanent [Boolean] default false. # @param body default a short message including the URL. def redirect(url, permanent: false, body: nil) body_message = body if permanent status 301 body_message ||= "This resource has been moved permanently to #{url}." elsif http_version == 'HTTP/1.1' && !request.get? status 303 body_message ||= "An alternate resource is located at #{url}." else status 302 body_message ||= "This resource has been moved temporarily to #{url}." end header 'Location', url content_type 'text/plain' body body_message end # Set or retrieve the HTTP status code. # # @param status [Integer] The HTTP Status Code to return for this request. def status(status = nil) case status when Symbol raise ArgumentError, "Status code :#{status} is invalid." unless Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(status) @status = Rack::Utils.status_code(status) when Integer @status = status when nil return @status if @status if request.post? 201 elsif request.delete? if @body.present? 200 else 204 end else 200 end else raise ArgumentError, 'Status code must be Integer or Symbol.' end end # Set response content-type def content_type(val = nil) if val header(Rack::CONTENT_TYPE, val) else header[Rack::CONTENT_TYPE] end end # Allows you to define the response body as something other than the # return value. # # @example # get '/body' do # body "Body" # "Not the Body" # end # # GET /body # => "Body" def body(value = nil) if value @body = value elsif value == false @body = '' status 204 else @body end end # Allows you to explicitly return no content. # # @example # delete :id do # return_no_content # "not returned" # end # # DELETE /12 # => 204 No Content, "" def return_no_content status 204 body false end # Allows you to send a file to the client via sendfile. # # @example # get '/file' do # sendfile FileStreamer.new(...) # end # # GET /file # => "contents of file" def sendfile(value = nil) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) elsif !value.is_a?(NilClass) raise ArgumentError, 'Argument must be a file path' else stream end end # Allows you to define the response as a streamable object. # # If Content-Length and Transfer-Encoding are blank (among other conditions), # Rack assumes this response can be streamed in chunks. # # @example # get '/stream' do # stream FileStreamer.new(...) # end # # GET /stream # => "chunked contents of file" # # See: # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/chunked.rb # * https://github.com/rack/rack/blob/99293fa13d86cd48021630fcc4bd5acc9de5bdc3/lib/rack/etag.rb def stream(value = nil) return if value.nil? && @stream.nil? header Rack::CONTENT_LENGTH, nil header 'Transfer-Encoding', nil header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front) if value.is_a?(String) file_body = Grape::ServeStream::FileBody.new(value) @stream = Grape::ServeStream::StreamResponse.new(file_body) elsif value.respond_to?(:each) @stream = Grape::ServeStream::StreamResponse.new(value) elsif !value.is_a?(NilClass) raise ArgumentError, 'Stream object must respond to :each.' else @stream end end # Allows you to make use of Grape Entities by setting # the response body to the serializable hash of the # entity provided in the `:with` option. This has the # added benefit of automatically passing along environment # and version information to the serialization, making it # very easy to do conditional exposures. See Entity docs # for more info. # # @example # # get '/users/:id' do # present User.find(params[:id]), # with: API::Entities::User, # admin: current_user.admin? # end def present(*args, **options) key, object = if args.count == 2 && args.first.is_a?(Symbol) args else [nil, args.first] end entity_class = entity_class_for_obj(object, options) root = options.delete(:root) representation = if entity_class entity_representation_for(entity_class, object, options) else object end representation = { root => representation } if root if key representation = (body || {}).merge(key => representation) elsif entity_class.present? && body raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge) representation = body.merge(representation) end body representation end # Returns route information for the current request. # # @example # # desc "Returns the route description." # get '/' do # route.description # end def route env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] end # Attempt to locate the Entity class for a given object, if not given # explicitly. This is done by looking for the presence of Klass::Entity, # where Klass is the class of the `object` parameter, or one of its # ancestors. # @param object [Object] the object to locate the Entity class for # @param options [Hash] # @option options :with [Class] the explicit entity class to use # @return [Class] the located Entity class, or nil if none is found def entity_class_for_obj(object, options) entity_class = options.delete(:with) return entity_class if entity_class # entity class not explicitly defined, auto-detect from relation#klass or first object in the collection object_class = if object.respond_to?(:klass) object.klass else object.respond_to?(:first) ? object.first.class : object.class end representations = inheritable_setting.namespace_stackable_with_hash(:representations) if representations potential = object_class.ancestors.detect { |potential| representations.key?(potential) } entity_class = representations[potential] if potential end entity_class = object_class.const_get(:Entity) if !entity_class && object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent) entity_class end # @return the representation of the given object as done through # the given entity_class. def entity_representation_for(entity_class, object, options) embeds = { env: env } embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION) entity_class.represent(object, **embeds, **options) end def http_version env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] } end def api_format(format) env[Grape::Env::API_FORMAT] = format end def context self end end end end ================================================ FILE: lib/grape/dsl/logger.rb ================================================ # frozen_string_literal: true module Grape module DSL module Logger # Set or retrive the configured logger. If none was configured, this # method will create a new one, logging to stdout. # @param logger [Object] the new logger to use def logger(logger = nil) global_settings = inheritable_setting.global if logger global_settings[:logger] = logger else global_settings[:logger] || global_settings[:logger] = ::Logger.new($stdout) end end end end end ================================================ FILE: lib/grape/dsl/middleware.rb ================================================ # frozen_string_literal: true module Grape module DSL module Middleware # Apply a custom middleware to the API. Applies # to the current namespace and any children, but # not parents. # # @param middleware_class [Class] The class of the middleware you'd like # to inject. def use(middleware_class, *args, &block) arr = [:use, middleware_class, *args] arr << block if block inheritable_setting.namespace_stackable[:middleware] = arr end %i[insert insert_before insert_after].each do |method_name| define_method method_name do |*args, &block| arr = [method_name, *args] arr << block if block inheritable_setting.namespace_stackable[:middleware] = arr end end # Retrieve an array of the middleware classes # and arguments that are currently applied to the # application. def middleware inheritable_setting.namespace_stackable[:middleware] || [] end end end end ================================================ FILE: lib/grape/dsl/parameters.rb ================================================ # frozen_string_literal: true module Grape module DSL # Defines DSL methods, meant to be applied to a ParamsScope, which define # and describe the parameters accepted by an endpoint, or all endpoints # within a namespace. module Parameters # Set the module used to build the request.params. # # @param build_with the ParamBuilder module to use when building request.params # Available builders are: # # * Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder (default) # * Grape::Extensions::Hash::ParamBuilder # * Grape::Extensions::Hashie::Mash::ParamBuilder # # @example # # require 'grape/extenstions/hashie_mash' # class API < Grape::API # desc "Get collection" # params do # build_with :hashie_mash # requires :user_id, type: Integer # end # get do # params['user_id'] # end # end def build_with(build_with) @api.inheritable_setting.namespace_inheritable[:build_params_with] = build_with end # Include reusable params rules among current. # You can define reusable params with helpers method. # # @example # # class API < Grape::API # helpers do # params :pagination do # optional :page, type: Integer # optional :per_page, type: Integer # end # end # # desc "Get collection" # params do # use :pagination # end # get do # Collection.page(params[:page]).per(params[:per_page]) # end # end def use(*names, **options) named_params = @api.inheritable_setting.namespace_stackable_with_hash(:named_params) || {} names.each do |name| params_block = named_params.fetch(name) do raise "Params :#{name} not found!" end if options.empty? instance_exec(options, ¶ms_block) else instance_exec(**options, ¶ms_block) end end end alias use_scope use alias includes use # Require one or more parameters for the current endpoint. # # @param attrs list of parameters names, or, if :using is # passed as an option, which keys to include (:all or :none) from # the :using hash. The last key can be a hash, which specifies # options for the parameters # @option attrs :type [Class] the type to coerce this parameter to before # passing it to the endpoint. See {Grape::Validations::Types} for a list of # types that are supported automatically. Custom classes may be used # where they define a class-level `::parse` method, or in conjunction # with the `:coerce_with` parameter. `JSON` may be supplied to denote # `JSON`-formatted objects or arrays of objects. `Array[JSON]` accepts # the same values as `JSON` but will wrap single objects in an `Array`. # @option attrs :types [Array] may be supplied in place of +:type+ # to declare an attribute that has multiple allowed types. See # {Validations::Types::MultipleTypeCoercer} for more details on coercion # and validation rules for variant-type parameters. # @option attrs :desc [String] description to document this parameter # @option attrs :default [Object] default value, if parameter is optional # @option attrs :values [Array] permissable values for this field. If any # other value is given, it will be handled as a validation error # @option attrs :using [Hash[Symbol => Hash]] a hash defining keys and # options, like that returned by {Grape::Entity#documentation}. The value # of each key is an options hash accepting the same parameters # @option attrs :except [Array[Symbol]] a list of keys to exclude from # the :using Hash. The meaning of this depends on if :all or :none was # passed; :all + :except will make the :except fields optional, whereas # :none + :except will make the :except fields required # @option attrs :coerce_with [#parse, #call] method to be used when coercing # the parameter to the type named by `attrs[:type]`. Any class or object # that defines `::parse` or `::call` may be used. # # @example # # params do # # Basic usage: require a parameter of a certain type # requires :user_id, type: Integer # # # You don't need to specify type; String is default # requires :foo # # # Multiple params can be specified at once if they share # # the same options. # requires :x, :y, :z, type: Date # # # Nested parameters can be handled as hashes. You must # # pass in a block, within which you can use any of the # # parameters DSL methods. # requires :user, type: Hash do # requires :name, type: String # end # end def requires(*attrs, **opts, &block) opts[:presence] = { value: true, message: opts[:message] } opts = @group.deep_merge(opts) if @group if opts[:using] require_required_and_optional_fields(attrs.first, using: opts[:using], except: opts[:except]) else validate_attributes(attrs, **opts, &block) block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as]) end end # Allow, but don't require, one or more parameters for the current # endpoint. # @param (see #requires) # @option (see #requires) def optional(*attrs, **opts, &block) type = opts[:type] opts = @group.deep_merge(opts) if @group # check type for optional parameter group if attrs && block raise Grape::Exceptions::MissingGroupType if type.nil? raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type) end if opts[:using] require_optional_fields(attrs.first, using: opts[:using], except: opts[:except]) else validate_attributes(attrs, **opts, &block) block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as]) end end # Define common settings for one or more parameters # @param (see #requires) # @option (see #requires) def with(**opts, &) new_group_attrs = [@group, opts].compact.reduce(&:deep_merge) new_group_scope(new_group_attrs, &) end %i[mutually_exclusive exactly_one_of at_least_one_of all_or_none_of].each do |validator| define_method validator do |*attrs, message: nil| validates(attrs, validator => { value: true, message: message }) end end # Define a block of validations which should be applied if and only if # the given parameter is present. The parameters are not nested. # @param attr [Symbol] the parameter which, if present, triggers the # validations # @raise Grape::Exceptions::UnknownParameter if `attr` has not been # defined in this scope yet # @yield a parameter definition DSL def given(*attrs, &) attrs.each do |attr| proxy_attr = first_hash_key_or_param(attr) raise Grape::Exceptions::UnknownParameter.new(proxy_attr) unless declared_param?(proxy_attr) end new_lateral_scope(dependent_on: attrs, &) end # Test for whether a certain parameter has been defined in this params # block yet. # @return [Boolean] whether the parameter has been defined def declared_param?(param) if lateral? # Elements of @declared_params of lateral scope are pushed in @parent. So check them in @parent. @parent.declared_param?(param) else # @declared_params also includes hashes of options and such, but those # won't be flattened out. @declared_params.flatten.any? do |declared_param_attr| first_hash_key_or_param(declared_param_attr.key) == param end end end alias group requires class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass def map_params(params, element, is_array = false) if params.is_a?(Array) params.map do |el| map_params(el, element, true) end elsif params.is_a?(Hash) params[element] || (@optional && is_array ? EmptyOptionalValue : {}) elsif params == EmptyOptionalValue EmptyOptionalValue else {} end end # @param params [Hash] initial hash of parameters # @return hash of parameters relevant for the current scope # @api private def params(params) params = @parent.qualifying_params.presence || @parent.params(params) if @parent params = map_params(params, @element) if @element params end private def first_hash_key_or_param(parameter) parameter.is_a?(Hash) ? parameter.keys.first : parameter end end end end ================================================ FILE: lib/grape/dsl/request_response.rb ================================================ # frozen_string_literal: true module Grape module DSL module RequestResponse # Specify the default format for the API's serializers. # May be `:json` or `:txt` (default). def default_format(new_format = nil) return inheritable_setting.namespace_inheritable[:default_format] if new_format.nil? inheritable_setting.namespace_inheritable[:default_format] = new_format.to_sym end # Specify the format for the API's serializers. # May be `:json`, `:xml`, `:txt`, etc. def format(new_format = nil) return inheritable_setting.namespace_inheritable[:format] if new_format.nil? symbolic_new_format = new_format.to_sym inheritable_setting.namespace_inheritable[:format] = symbolic_new_format inheritable_setting.namespace_inheritable[:default_error_formatter] = Grape::ErrorFormatter.formatter_for(symbolic_new_format) content_type = content_types[symbolic_new_format] raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type inheritable_setting.namespace_stackable[:content_types] = { symbolic_new_format => content_type } end # Specify a custom formatter for a content-type. def formatter(content_type, new_formatter) inheritable_setting.namespace_stackable[:formatters] = { content_type.to_sym => new_formatter } end # Specify a custom parser for a content-type. def parser(content_type, new_parser) inheritable_setting.namespace_stackable[:parsers] = { content_type.to_sym => new_parser } end # Specify a default error formatter. def default_error_formatter(new_formatter_name = nil) return inheritable_setting.namespace_inheritable[:default_error_formatter] if new_formatter_name.nil? new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name) inheritable_setting.namespace_inheritable[:default_error_formatter] = new_formatter end def error_formatter(format, options) formatter = if options.is_a?(Hash) && options.key?(:with) options[:with] else options end inheritable_setting.namespace_stackable[:error_formatters] = { format.to_sym => formatter } end # Specify additional content-types, e.g.: # content_type :xls, 'application/vnd.ms-excel' def content_type(key, val) inheritable_setting.namespace_stackable[:content_types] = { key.to_sym => val } end # All available content types. def content_types c_types = inheritable_setting.namespace_stackable_with_hash(:content_types) Grape::ContentTypes.content_types_for c_types end # Specify the default status code for errors. def default_error_status(new_status = nil) return inheritable_setting.namespace_inheritable[:default_error_status] if new_status.nil? inheritable_setting.namespace_inheritable[:default_error_status] = new_status end # Allows you to rescue certain exceptions that occur to return # a grape error rather than raising all the way to the # server level. # # @example Rescue from custom exceptions # class ExampleAPI < Grape::API # class CustomError < StandardError; end # # rescue_from CustomError # end # # @overload rescue_from(*exception_classes, **options) # @param [Array] exception_classes A list of classes that you want to rescue, or # the symbol :all to rescue from all exceptions. # @param [Block] block Execution block to handle the given exception. # @param [Hash] options Options for the rescue usage. # @option options [Boolean] :backtrace Include a backtrace in the rescue response. # @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes # @param [Proc] handler Execution proc to handle the given exception as an # alternative to passing a block. def rescue_from(*args, **options, &block) if args.last.is_a?(Proc) handler = args.pop elsif block handler = block end raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with) handler ||= extract_with(options) if args.include?(:all) inheritable_setting.namespace_inheritable[:rescue_all] = true inheritable_setting.namespace_inheritable[:all_rescue_handler] = handler elsif args.include?(:grape_exceptions) inheritable_setting.namespace_inheritable[:rescue_all] = true inheritable_setting.namespace_inheritable[:rescue_grape_exceptions] = true inheritable_setting.namespace_inheritable[:grape_exceptions_rescue_handler] = handler else handler_type = case options[:rescue_subclasses] when nil, true :rescue_handlers else :base_only_rescue_handlers end inheritable_setting.namespace_reverse_stackable[handler_type] = args.to_h { |arg| [arg, handler] } end inheritable_setting.namespace_stackable[:rescue_options] = options end # Allows you to specify a default representation entity for a # class. This allows you to map your models to their respective # entities once and then simply call `present` with the model. # # @example # class ExampleAPI < Grape::API # represent User, with: Entity::User # # get '/me' do # present current_user # with: Entity::User is assumed # end # end # # Note that Grape will automatically go up the class ancestry to # try to find a representing entity, so if you, for example, define # an entity to represent `Object` then all presented objects will # bubble up and utilize the entity provided on that `represent` call. # # @param model_class [Class] The model class that will be represented. # @option options [Class] :with The entity class that will represent the model. def represent(model_class, options) raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class) inheritable_setting.namespace_stackable[:representations] = { model_class => options[:with] } end private def extract_with(options) return unless options.key?(:with) with_option = options.delete(:with) return with_option if with_option.instance_of?(Proc) return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String) raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc" end end end end ================================================ FILE: lib/grape/dsl/routing.rb ================================================ # frozen_string_literal: true module Grape module DSL module Routing attr_reader :endpoints def given(conditional_option, &) return unless conditional_option mounted(&) end def mounted(&block) evaluate_as_instance_with_configuration(block, lazy: true) end def cascade(value = nil) return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable[:cascade].nil? : true if value.nil? inheritable_setting.namespace_inheritable[:cascade] = value end # Specify an API version. # # @example API with legacy support. # class MyAPI < Grape::API # version 'v2' # # get '/main' do # {some: 'data'} # end # # version 'v1' do # get '/main' do # {legacy: 'data'} # end # end # end # def version(*args, **options, &block) if args.any? options = options.reverse_merge(using: :path) requested_versions = args.flatten.map(&:to_s) raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor) @versions = versions | requested_versions if block within_namespace do inheritable_setting.namespace_inheritable[:version] = requested_versions inheritable_setting.namespace_inheritable[:version_options] = options instance_eval(&block) end else inheritable_setting.namespace_inheritable[:version] = requested_versions inheritable_setting.namespace_inheritable[:version_options] = options end end @versions&.last end # Define a root URL prefix for your entire API. def prefix(prefix = nil) return inheritable_setting.namespace_inheritable[:root_prefix] if prefix.nil? inheritable_setting.namespace_inheritable[:root_prefix] = prefix.to_s end # Create a scope without affecting the URL. # # @param _name [Symbol] Purely placebo, just allows to name the scope to # make the code more readable. def scope(_name = nil, &block) within_namespace do nest(block) end end def build_with(build_with) inheritable_setting.namespace_inheritable[:build_params_with] = build_with end # Do not route HEAD requests to GET requests automatically. def do_not_route_head! inheritable_setting.namespace_inheritable[:do_not_route_head] = true end # Do not automatically route OPTIONS. def do_not_route_options! inheritable_setting.namespace_inheritable[:do_not_route_options] = true end def lint! inheritable_setting.namespace_inheritable[:lint] = true end def do_not_document! inheritable_setting.namespace_inheritable[:do_not_document] = true end def mount(mounts, *opts) mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair) mounts.each_pair do |app, path| if app.respond_to?(:mount_instance) opts_with = opts.any? ? opts.first[:with] : {} mount({ app.mount_instance(configuration: opts_with) => path }, *opts) next end in_setting = inheritable_setting if app.respond_to?(:inheritable_setting, true) mount_path = Grape::Router.normalize_path(path) app.top_level_setting.namespace_stackable[:mount_path] = mount_path app.inherit_settings(inheritable_setting) in_setting = app.top_level_setting app.change! change! end # When trying to mount multiple times the same endpoint, remove the previous ones # from the list of endpoints if refresh_already_mounted parameter is true refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false if refresh_already_mounted && !endpoints.empty? endpoints.delete_if do |endpoint| endpoint.options[:app].to_s == app.to_s end end endpoints << Grape::Endpoint.new( in_setting, method: :any, path: path, app: app, route_options: { anchor: false }, forward_match: !app.respond_to?(:inheritable_setting), for: self ) end end # Defines a route that will be recognized # by the Grape API. # # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted. # @param paths [String] One or more strings representing the URL segment(s) for this route. # # @example Defining a basic route. # class MyAPI < Grape::API # route(:any, '/hello') do # {hello: 'world'} # end # end def route(methods, paths = ['/'], route_options = {}, &) method = methods == :any ? '*' : methods endpoint_params = inheritable_setting.namespace_stackable_with_hash(:params) || {} endpoint_description = inheritable_setting.route[:description] all_route_options = { params: endpoint_params } all_route_options.deep_merge!(endpoint_description) if endpoint_description all_route_options.deep_merge!(route_options) if route_options&.any? new_endpoint = Grape::Endpoint.new( inheritable_setting, method: method, path: paths, for: self, route_options: all_route_options, & ) endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) } inheritable_setting.route_end reset_validations! end Grape::HTTP_SUPPORTED_METHODS.each do |supported_method| define_method supported_method.downcase do |*args, **options, &block| paths = args.first || ['/'] route(supported_method, paths, options, &block) end end # Declare a "namespace", which prefixes all subordinate routes with its # name. Any endpoints within a namespace, group, resource or segment, # etc., will share their parent context as well as any configuration # done in the namespace context. # # @example # # namespace :foo do # get 'bar' do # # defines the endpoint: GET /foo/bar # end # end def namespace(space = nil, requirements: nil, **options, &block) return Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) unless space || block within_namespace do nest(block) do inheritable_setting.namespace_stackable[:namespace] = Grape::Namespace.new(space, requirements: requirements, **options) if space end end end alias group namespace alias resource namespace alias resources namespace alias segment namespace # An array of API routes. def routes @routes ||= endpoints.map(&:routes).flatten end # This method allows you to quickly define a parameter route segment # in your API. # # @param param [Symbol] The name of the parameter you wish to declare. # @option options [Regexp] You may supply a regular expression that the declared parameter must meet. def route_param(param, requirements: nil, type: nil, **, &) requirements = { param.to_sym => requirements } if requirements.is_a?(Regexp) Grape::Validations::ParamsScope.new(api: self) do requires param, type: type end if type namespace(":#{param}", requirements: requirements, **, &) end # @return array of defined versions def versions @versions ||= [] end private # Remove all defined routes. def reset_routes! endpoints.each(&:reset_routes!) @routes = nil end def reset_endpoints! @endpoints = [] end def refresh_mounted_api(mounts, *opts) opts << { refresh_already_mounted: true } mount(mounts, *opts) end # Execute first the provided block, then each of the # block passed in. Allows for simple 'before' setups # of settings stack pushes. def nest(*blocks, &block) blocks.compact! if blocks.any? evaluate_as_instance_with_configuration(block) if block blocks.each { |b| evaluate_as_instance_with_configuration(b) } reset_validations! else instance_eval(&block) end end def evaluate_as_instance_with_configuration(block, lazy: false) lazy_block = Grape::Util::Lazy::Block.new do |configuration| value_for_configuration = configuration self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? response = instance_eval(&block) self.configuration = value_for_configuration response end if @base && base_instance? && lazy lazy_block else lazy_block.evaluate_from(configuration) end end end end end ================================================ FILE: lib/grape/dsl/settings.rb ================================================ # frozen_string_literal: true module Grape module DSL # Keeps track of settings (implemented as key-value pairs, grouped by # types), in two contexts: top-level settings which apply globally no # matter where they're defined, and inheritable settings which apply only # in the current scope and scopes nested under it. module Settings attr_writer :inheritable_setting # Fetch our top-level settings, which apply to all endpoints in the API. def top_level_setting @top_level_setting ||= Grape::Util::InheritableSetting.new.tap do |setting| # Doesn't try to inherit settings from +Grape::API::Instance+ which also responds to # +inheritable_setting+, however, it doesn't contain any user-defined settings. # Otherwise, it would lead to an extra instance of +Grape::Util::InheritableSetting+ # in the chain for every endpoint. setting.inherit_from superclass.inheritable_setting if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance end end # Fetch our current inheritable settings, which are inherited by # nested scopes but not shared across siblings. def inheritable_setting @inheritable_setting ||= Grape::Util::InheritableSetting.new.tap { |new_settings| new_settings.inherit_from top_level_setting } end def global_setting(key, value = nil) get_or_set(inheritable_setting.global, key, value) end def route_setting(key, value = nil) get_or_set(inheritable_setting.route, key, value) end def namespace_setting(key, value = nil) get_or_set(inheritable_setting.namespace, key, value) end private # Execute the block within a context where our inheritable settings are forked # to a new copy (see #namespace_start). def within_namespace new_inheritable_settings = Grape::Util::InheritableSetting.new new_inheritable_settings.inherit_from inheritable_setting @inheritable_setting = new_inheritable_settings result = yield inheritable_setting.route_end @inheritable_setting = inheritable_setting.parent reset_validations! result end def get_or_set(setting, key, value) return setting[key] if value.nil? setting[key] = value end end end end ================================================ FILE: lib/grape/dsl/validations.rb ================================================ # frozen_string_literal: true module Grape module DSL module Validations # Opens a root-level ParamsScope, defining parameter coercions and # validations for the endpoint. # @yield instance context of the new scope def params(&) Grape::Validations::ParamsScope.new(api: self, type: Hash, &) end # Declare the contract to be used for the endpoint's parameters. # @param contract [Class | Dry::Schema::Processor] # The contract or schema to be used for validation. Optional. # @yield a block yielding a new instance of Dry::Schema::Params # subclass, allowing to define the schema inline. When the # +contract+ parameter is a schema, it will be used as a parent. Optional. def contract(contract = nil, &block) raise ArgumentError, 'Either contract or block must be provided' unless contract || block raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema) Grape::Validations::ContractScope.new(self, contract, &block) end private # Clears all defined parameters and validations. The main purpose of it is to clean up # settings, so next endpoint won't interfere with previous one. # # params do # # params for the endpoint below this block # end # post '/current' do # # whatever # end # # # somewhere between them the reset_validations! method gets called # # params do # # params for the endpoint below this block # end # post '/next' do # # whatever # end def reset_validations! inheritable_setting.namespace_stackable.delete(:declared_params, :params, :validations) end end end end ================================================ FILE: lib/grape/endpoint.rb ================================================ # frozen_string_literal: true module Grape # An Endpoint is the proxy scope in which all routing # blocks are executed. In other words, any methods # on the instance level of this class may be called # from inside a `get`, `post`, etc. class Endpoint extend Forwardable include Grape::DSL::Settings include Grape::DSL::Headers include Grape::DSL::InsideRoute attr_reader :env, :request, :source, :options def_delegators :request, :params, :headers, :cookies def_delegator :cookies, :response_cookies class << self def before_each(new_setup = false, &block) @before_each ||= [] if new_setup == false return @before_each unless block @before_each << block elsif new_setup @before_each = [new_setup] else @before_each.clear end end def run_before_each(endpoint) superclass.run_before_each(endpoint) unless self == Endpoint before_each.each { |blk| blk.call(endpoint) } end def block_to_unbound_method(block) return unless block define_method :temp_unbound_method, block method = instance_method(:temp_unbound_method) remove_method :temp_unbound_method method end end # Create a new endpoint. # @param new_settings [InheritableSetting] settings to determine the params, # validations, and other properties from. # @param options [Hash] attributes of this endpoint # @option options path [String or Array] the path to this endpoint, within # the current scope. # @option options method [String or Array] which HTTP method(s) can be used # to reach this endpoint. # @option options route_options [Hash] # @note This happens at the time of API definition, so in this context the # endpoint does not know if it will be mounted under a different endpoint. # @yield a block defining what your API should do when this endpoint is hit def initialize(new_settings, **options, &block) self.inheritable_setting = new_settings.point_in_time_copy # now +namespace_stackable(:declared_params)+ contains all params defined for # this endpoint and its parents, but later it will be cleaned up, # see +reset_validations!+ in lib/grape/dsl/validations.rb inheritable_setting.route[:declared_params] = inheritable_setting.namespace_stackable[:declared_params].flatten inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations] inheritable_setting.namespace_stackable[:representations] = [] unless inheritable_setting.namespace_stackable[:representations] inheritable_setting.namespace_inheritable[:default_error_status] = 500 unless inheritable_setting.namespace_inheritable[:default_error_status] @options = options @options[:path] = Array(options[:path]) @options[:path] << '/' if options[:path].empty? @options[:method] = Array(options[:method]) @status = nil @stream = nil @body = nil @source = self.class.block_to_unbound_method(block) @before_filter_passed = false end # Update our settings from a given set of stackable parameters. Used when # the endpoint's API is mounted under another one. def inherit_settings(namespace_stackable) parent_validations = namespace_stackable[:validations] inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any? parent_declared_params = namespace_stackable[:declared_params] inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any? endpoints&.each { |e| e.inherit_settings(namespace_stackable) } end def routes @routes ||= endpoints&.collect(&:routes)&.flatten || to_routes end def reset_routes! endpoints&.each(&:reset_routes!) @namespace = nil @routes = nil end def mount_in(router) if endpoints compile! return endpoints.each { |e| e.mount_in(router) } end reset_routes! compile! routes.each do |route| router.append(route.apply(self)) next unless !inheritable_setting.namespace_inheritable[:do_not_route_head] && route.request_method == Rack::GET route.dup.then do |head_route| head_route.convert_to_head_request! router.append(head_route.apply(self)) end end end def namespace @namespace ||= Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) end def call(env) dup.call!(env) end def call!(env) env[Grape::Env::API_ENDPOINT] = self @env = env # this adds the helpers only to the instance singleton_class.include(@helpers) if @helpers @app.call(env) end # Return the collection of endpoints within this endpoint. # This is the case when an Grape::API mounts another Grape::API. def endpoints @endpoints ||= options[:app].respond_to?(:endpoints) ? options[:app].endpoints : nil end def equals?(endpoint) (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash) end # The purpose of this override is solely for stripping internals when an error occurs while calling # an endpoint through an api. See https://github.com/ruby-grape/grape/issues/2398 # Otherwise, it calls super. def inspect return super unless env "#{self.class} in '#{route.origin}' endpoint" end protected def run ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do @request = Grape::Request.new(env, build_params_with: inheritable_setting.namespace_inheritable[:build_params_with]) begin self.class.run_before_each(self) run_filters befores, :before @before_filter_passed = true if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS) header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ') raise Grape::Exceptions::MethodNotAllowed.new(header) unless options? header 'Allow', header['Allow'] response_object = '' status 204 else run_filters before_validations, :before_validation run_validators validations, request run_filters after_validations, :after_validation response_object = execute end run_filters afters, :after build_response_cookies # status verifies body presence when DELETE @body ||= response_object # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object response_object = stream || [body] [status, header, response_object] ensure run_filters finallies, :finally end end end def execute return unless @source ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self) do @source.bind_call(self) end end def run_validators(validators, request) validation_errors = [] Grape::Validations::ParamScopeTracker.track do ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do validators.each do |validator| validator.validate(request) rescue Grape::Exceptions::Validation => e validation_errors << e break if validator.fail_fast? rescue Grape::Exceptions::ValidationArrayErrors => e validation_errors.concat e.errors break if validator.fail_fast? end end end validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header)) end def run_filters(filters, type = :other) return unless filters ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do filters.each { |filter| instance_eval(&filter) } end end %i[befores before_validations after_validations afters finallies].each do |method| define_method method do inheritable_setting.namespace_stackable[method] end end def validations saved_validations = inheritable_setting.route[:saved_validations] return if saved_validations.nil? return enum_for(:validations) unless block_given? saved_validations.each do |saved_validation| yield Grape::Validations::ValidatorFactory.create_validator(saved_validation) end end def options? options[:options_route_enabled] && env[Rack::REQUEST_METHOD] == Rack::OPTIONS end private attr_reader :before_filter_passed def compile! @app = options[:app] || build_stack @helpers = build_helpers end def to_routes route_options = options[:route_options] default_route_options = prepare_default_route_attributes(route_options) complete_route_options = route_options.merge(default_route_options) path_settings = prepare_default_path_settings options[:method].flat_map do |method| options[:path].map do |path| prepared_path = Path.new(path, default_route_options[:namespace], path_settings) pattern = Grape::Router::Pattern.new( origin: prepared_path.origin, suffix: prepared_path.suffix, anchor: default_route_options[:anchor], params: route_options[:params], format: options[:format], version: default_route_options[:version], requirements: default_route_options[:requirements] ) Grape::Router::Route.new(self, method, pattern, complete_route_options) end end end def prepare_default_route_attributes(route_options) { namespace: namespace, version: prepare_version(inheritable_setting.namespace_inheritable[:version]), requirements: prepare_routes_requirements(route_options[:requirements]), prefix: inheritable_setting.namespace_inheritable[:root_prefix], anchor: route_options.fetch(:anchor, true), settings: inheritable_setting.route.except(:declared_params, :saved_validations), forward_match: options[:forward_match] } end def prepare_default_path_settings namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash namespace_stackable_hash.merge!(namespace_inheritable_hash) end def prepare_routes_requirements(route_options_requirements) namespace_requirements = inheritable_setting.namespace_stackable[:namespace].filter_map(&:requirements) namespace_requirements << route_options_requirements if route_options_requirements.present? namespace_requirements.reduce({}, :merge) end def prepare_version(namespace_inheritable_version) return if namespace_inheritable_version.blank? namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version end def build_stack stack = Grape::Middleware::Stack.new content_types = inheritable_setting.namespace_stackable_with_hash(:content_types) format = inheritable_setting.namespace_inheritable[:format] stack.use Rack::Head stack.use Rack::Lint if lint? stack.use Grape::Middleware::Error, format: format, content_types: content_types, default_status: inheritable_setting.namespace_inheritable[:default_error_status], rescue_all: inheritable_setting.namespace_inheritable[:rescue_all], rescue_grape_exceptions: inheritable_setting.namespace_inheritable[:rescue_grape_exceptions], default_error_formatter: inheritable_setting.namespace_inheritable[:default_error_formatter], error_formatters: inheritable_setting.namespace_stackable_with_hash(:error_formatters), rescue_options: inheritable_setting.namespace_stackable_with_hash(:rescue_options), rescue_handlers: rescue_handlers, base_only_rescue_handlers: inheritable_setting.namespace_stackable_with_hash(:base_only_rescue_handlers), all_rescue_handler: inheritable_setting.namespace_inheritable[:all_rescue_handler], grape_exceptions_rescue_handler: inheritable_setting.namespace_inheritable[:grape_exceptions_rescue_handler] stack.concat inheritable_setting.namespace_stackable[:middleware] if inheritable_setting.namespace_inheritable[:version].present? stack.use Grape::Middleware::Versioner.using(inheritable_setting.namespace_inheritable[:version_options][:using]), versions: inheritable_setting.namespace_inheritable[:version].flatten, version_options: inheritable_setting.namespace_inheritable[:version_options], prefix: inheritable_setting.namespace_inheritable[:root_prefix], mount_path: inheritable_setting.namespace_stackable[:mount_path].first end stack.use Grape::Middleware::Formatter, format: format, default_format: inheritable_setting.namespace_inheritable[:default_format] || :txt, content_types: content_types, formatters: inheritable_setting.namespace_stackable_with_hash(:formatters), parsers: inheritable_setting.namespace_stackable_with_hash(:parsers) builder = stack.build builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run } builder.to_app end def build_helpers helpers = inheritable_setting.namespace_stackable[:helpers] return if helpers.empty? Module.new { helpers.each { |mod_to_include| include mod_to_include } } end def build_response_cookies response_cookies do |name, value| cookie_value = value.is_a?(Hash) ? value : { value: value } Rack::Utils.set_cookie_header! header, name, cookie_value end end def lint? inheritable_setting.namespace_inheritable[:lint] || Grape.config.lint end def rescue_handlers rescue_handlers = inheritable_setting.namespace_reverse_stackable[:rescue_handlers] return if rescue_handlers.blank? rescue_handlers.each_with_object({}) do |rescue_handler, result| result.merge!(rescue_handler) { |_k, s1, _s2| s1 } end end end end ================================================ FILE: lib/grape/env.rb ================================================ # frozen_string_literal: true module Grape module Env API_VERSION = 'api.version' API_ENDPOINT = 'api.endpoint' API_REQUEST_INPUT = 'api.request.input' API_REQUEST_BODY = 'api.request.body' API_TYPE = 'api.type' API_SUBTYPE = 'api.subtype' API_VENDOR = 'api.vendor' API_FORMAT = 'api.format' GRAPE_REQUEST = 'grape.request' GRAPE_REQUEST_HEADERS = 'grape.request.headers' GRAPE_REQUEST_PARAMS = 'grape.request.params' GRAPE_ROUTING_ARGS = 'grape.routing_args' GRAPE_ALLOWED_METHODS = 'grape.allowed_methods' end end ================================================ FILE: lib/grape/error_formatter/base.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter class Base class << self def call(message, backtrace, options = {}, env = nil, original_exception = nil) merge_backtrace = backtrace.present? && options.dig(:rescue_options, :backtrace) merge_original_exception = original_exception && options.dig(:rescue_options, :original_exception) wrapped_message = wrap_message(present(message, env)) if wrapped_message.is_a?(Hash) wrapped_message[:backtrace] = backtrace if merge_backtrace wrapped_message[:original_exception] = original_exception.inspect if merge_original_exception end format_structured_message(wrapped_message) end def present(message, env) present_options = {} presented_message = message if presented_message.is_a?(Hash) presented_message = presented_message.dup present_options[:with] = presented_message.delete(:with) end presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options) unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil? # env['api.endpoint'].route does not work when the error occurs within a middleware # the Endpoint does not have a valid env at this moment http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || [] found_code = http_codes.find do |http_code| (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent) end if env[Grape::Env::API_ENDPOINT].request presenter = found_code[2] if found_code end if presenter embeds = { env: env } embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION) presented_message = presenter.represent(presented_message, embeds).serializable_hash end presented_message end def wrap_message(message) return message if message.is_a?(Hash) { message: message } end def format_structured_message(_structured_message) raise NotImplementedError end private def inherited(klass) super ErrorFormatter.register(klass) end end end end end ================================================ FILE: lib/grape/error_formatter/json.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter class Json < Base class << self def format_structured_message(structured_message) ::Grape::Json.dump(structured_message) end private def wrap_message(message) return message if message.is_a?(Hash) return message.as_json if message.is_a?(Exceptions::ValidationErrors) { error: ensure_utf8(message) } end def ensure_utf8(message) return message unless message.respond_to? :encode message.encode('UTF-8', invalid: :replace, undef: :replace) end end end end end ================================================ FILE: lib/grape/error_formatter/serializable_hash.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter class SerializableHash < Json; end end end ================================================ FILE: lib/grape/error_formatter/txt.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter class Txt < Base def self.format_structured_message(structured_message) message = structured_message[:message] || Grape::Json.dump(structured_message) Array.wrap(message).tap do |final_message| if structured_message.key?(:backtrace) final_message << 'backtrace:' final_message.concat(structured_message[:backtrace]) end if structured_message.key?(:original_exception) final_message << 'original exception:' final_message << structured_message[:original_exception] end end.join("\r\n ") end end end end ================================================ FILE: lib/grape/error_formatter/xml.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter class Xml < Base def self.format_structured_message(structured_message) structured_message.respond_to?(:to_xml) ? structured_message.to_xml(root: :error) : structured_message.to_s end end end end ================================================ FILE: lib/grape/error_formatter.rb ================================================ # frozen_string_literal: true module Grape module ErrorFormatter extend Grape::Util::Registry module_function def formatter_for(format, error_formatters = nil, default_error_formatter = nil) return error_formatters[format] if error_formatters&.key?(format) registry[format] || default_error_formatter || Grape::ErrorFormatter::Txt end end end ================================================ FILE: lib/grape/exceptions/base.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class Base < StandardError include Grape::Util::Translation MESSAGE_STEPS = %w[problem summary resolution].to_h { |s| [s, s.capitalize] }.freeze attr_reader :status, :headers def initialize(status: nil, message: nil, headers: nil) super(message) @status = status @headers = headers end def [](index) __send__ index end private def compose_message(key, **) short_message = translate_message(key, **) return short_message unless short_message.is_a?(Hash) MESSAGE_STEPS.filter_map do |step, label| detail = translate_message(:"#{key}.#{step}", **) "\n#{label}:\n #{detail}" if detail.present? end.join end def translate_message(translation_key, **) case translation_key when Symbol translate(translation_key, **) when Hash translation_key => { key:, **opts } translate(key, **opts) when Proc translation_key.call else translation_key end end end end end ================================================ FILE: lib/grape/exceptions/conflicting_types.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class ConflictingTypes < Base def initialize super(message: compose_message(:conflicting_types), status: 400) end end end end ================================================ FILE: lib/grape/exceptions/empty_message_body.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class EmptyMessageBody < Base def initialize(body_format) super(message: compose_message(:empty_message_body, body_format: body_format), status: 400) end end end end ================================================ FILE: lib/grape/exceptions/incompatible_option_values.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class IncompatibleOptionValues < Base def initialize(option1, value1, option2, value2) super(message: compose_message(:incompatible_option_values, option1: option1, value1: value1, option2: option2, value2: value2)) end end end end ================================================ FILE: lib/grape/exceptions/invalid_accept_header.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidAcceptHeader < Base def initialize(message, headers) super(message: compose_message(:invalid_accept_header, message: message), status: 406, headers: headers) end end end end ================================================ FILE: lib/grape/exceptions/invalid_formatter.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidFormatter < Base def initialize(klass, to_format) super(message: compose_message(:invalid_formatter, klass: klass, to_format: to_format)) end end end end ================================================ FILE: lib/grape/exceptions/invalid_message_body.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidMessageBody < Base def initialize(body_format) super(message: compose_message(:invalid_message_body, body_format: body_format), status: 400) end end end end ================================================ FILE: lib/grape/exceptions/invalid_parameters.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidParameters < Base def initialize super(message: compose_message(:invalid_parameters), status: 400) end end end end ================================================ FILE: lib/grape/exceptions/invalid_response.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidResponse < Base def initialize super(message: compose_message(:invalid_response)) end end end end ================================================ FILE: lib/grape/exceptions/invalid_version_header.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidVersionHeader < Base def initialize(message, headers) super(message: compose_message(:invalid_version_header, message: message), status: 406, headers: headers) end end end end ================================================ FILE: lib/grape/exceptions/invalid_versioner_option.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidVersionerOption < Base def initialize(strategy) super(message: compose_message(:invalid_versioner_option, strategy: strategy)) end end end end ================================================ FILE: lib/grape/exceptions/invalid_with_option_for_represent.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class InvalidWithOptionForRepresent < Base def initialize super(message: compose_message(:invalid_with_option_for_represent)) end end end end ================================================ FILE: lib/grape/exceptions/method_not_allowed.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class MethodNotAllowed < Base def initialize(headers) super(message: '405 Not Allowed', status: 405, headers: headers) end end end end ================================================ FILE: lib/grape/exceptions/missing_group_type.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class MissingGroupType < Base def initialize super(message: compose_message(:missing_group_type)) end end end end ================================================ FILE: lib/grape/exceptions/missing_mime_type.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class MissingMimeType < Base def initialize(new_format) super(message: compose_message(:missing_mime_type, new_format: new_format)) end end end end ================================================ FILE: lib/grape/exceptions/missing_vendor_option.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class MissingVendorOption < Base def initialize super(message: compose_message(:missing_vendor_option)) end end end end ================================================ FILE: lib/grape/exceptions/too_deep_parameters.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class TooDeepParameters < Base def initialize(limit) super(message: compose_message(:too_deep_parameters, limit: limit), status: 400) end end end end ================================================ FILE: lib/grape/exceptions/too_many_multipart_files.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class TooManyMultipartFiles < Base def initialize(limit) super(message: compose_message(:too_many_multipart_files, limit: limit), status: 413) end end end end ================================================ FILE: lib/grape/exceptions/unknown_auth_strategy.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class UnknownAuthStrategy < Base def initialize(strategy:) super(message: compose_message(:unknown_auth_strategy, strategy: strategy)) end end end end ================================================ FILE: lib/grape/exceptions/unknown_parameter.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class UnknownParameter < Base def initialize(param) super(message: compose_message(:unknown_parameter, param: param)) end end end end ================================================ FILE: lib/grape/exceptions/unknown_params_builder.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class UnknownParamsBuilder < Base def initialize(params_builder_type) super(message: compose_message(:unknown_params_builder, params_builder_type: params_builder_type)) end end end end ================================================ FILE: lib/grape/exceptions/unknown_validator.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class UnknownValidator < Base def initialize(validator_type) super(message: compose_message(:unknown_validator, validator_type: validator_type)) end end end end ================================================ FILE: lib/grape/exceptions/unsupported_group_type.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class UnsupportedGroupType < Base def initialize super(message: compose_message(:unsupported_group_type)) end end end end ================================================ FILE: lib/grape/exceptions/validation.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class Validation < Base attr_reader :params, :message_key def initialize(params:, message: nil, status: nil, headers: nil) @params = Array(params) if message @message_key = case message when Symbol then message when Hash then message[:key] end message = translate_message(message) end super(status: status, message: message, headers: headers) end # Remove all the unnecessary stuff from Grape::Exceptions::Base like status # and headers when converting a validation error to json or string def as_json(*_args) to_s end end end end ================================================ FILE: lib/grape/exceptions/validation_array_errors.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class ValidationArrayErrors < Base attr_reader :errors def initialize(errors) super() @errors = errors end end end end ================================================ FILE: lib/grape/exceptions/validation_errors.rb ================================================ # frozen_string_literal: true module Grape module Exceptions class ValidationErrors < Base include Enumerable attr_reader :errors def initialize(errors: [], headers: {}) @errors = errors.group_by(&:params) super(message: full_messages.join(', '), status: 400, headers: headers) end def each errors.each_pair do |attribute, errors| errors.each do |error| yield attribute, error end end end def as_json(**_opts) errors.map do |k, v| { params: k, messages: v.map(&:to_s) } end end def to_json(*_opts) as_json.to_json end def full_messages messages = map do |attributes, error| translate( :format, scope: 'grape.errors', default: '%s %s', attributes: translate_attributes(attributes), message: error.message ) end messages.uniq! messages end private def translate_attributes(keys) keys.map do |key| translate(key, scope: 'grape.errors.attributes', default: key.to_s) end.join(', ') end end end end ================================================ FILE: lib/grape/formatter/base.rb ================================================ # frozen_string_literal: true module Grape module Formatter class Base def self.call(_object, _env) raise NotImplementedError end def self.inherited(klass) super Formatter.register(klass) end end end end ================================================ FILE: lib/grape/formatter/json.rb ================================================ # frozen_string_literal: true module Grape module Formatter class Json < Base def self.call(object, _env) return object.to_json if object.respond_to?(:to_json) ::Grape::Json.dump(object) end end end end ================================================ FILE: lib/grape/formatter/serializable_hash.rb ================================================ # frozen_string_literal: true module Grape module Formatter class SerializableHash < Base class << self def call(object, _env) return object if object.is_a?(String) return ::Grape::Json.dump(serialize(object)) if serializable?(object) return object.to_json if object.respond_to?(:to_json) ::Grape::Json.dump(object) end private def serializable?(object) object.respond_to?(:serializable_hash) || array_serializable?(object) || object.is_a?(Hash) end def serialize(object) if object.respond_to? :serializable_hash object.serializable_hash elsif array_serializable?(object) object.map(&:serializable_hash) elsif object.is_a?(Hash) object.transform_values { |v| serialize(v) } else object end end def array_serializable?(object) object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } end end end end end ================================================ FILE: lib/grape/formatter/txt.rb ================================================ # frozen_string_literal: true module Grape module Formatter class Txt < Base def self.call(object, _env) object.respond_to?(:to_txt) ? object.to_txt : object.to_s end end end end ================================================ FILE: lib/grape/formatter/xml.rb ================================================ # frozen_string_literal: true module Grape module Formatter class Xml < Base def self.call(object, _env) return object.to_xml if object.respond_to?(:to_xml) raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml') end end end end ================================================ FILE: lib/grape/formatter.rb ================================================ # frozen_string_literal: true module Grape module Formatter extend Grape::Util::Registry module_function DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj } def formatter_for(api_format, formatters) return formatters[api_format] if formatters&.key?(api_format) registry[api_format] || DEFAULT_LAMBDA_FORMATTER end end end ================================================ FILE: lib/grape/json.rb ================================================ # frozen_string_literal: true module Grape if defined?(::MultiJson) Json = ::MultiJson else Json = ::JSON Json::ParseError = Json::ParserError end end ================================================ FILE: lib/grape/locale/en.yml ================================================ --- en: grape: errors: format: '%{attributes} %{message}' messages: all_or_none: 'provide all or none of parameters' at_least_one: 'are missing, at least one parameter must be provided' blank: 'is empty' coerce: 'is invalid' conflicting_types: 'query params contains conflicting types' empty_message_body: 'empty message body supplied with %{body_format} content-type' exactly_one: 'are missing, exactly one parameter must be provided' except_values: 'has a value not allowed' incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}' invalid_accept_header: problem: 'invalid accept header' resolution: '%{message}' invalid_formatter: 'cannot convert %{klass} to %{to_format}' invalid_message_body: problem: 'message body does not match declared format' resolution: 'when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request''s ''body'' ' invalid_parameters: 'query params contains invalid format or byte sequence' invalid_response: 'Invalid response' invalid_version_header: problem: 'invalid version header' resolution: '%{message}' invalid_versioner_option: problem: 'unknown :using for versioner: %{strategy}' resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param' invalid_with_option_for_represent: problem: 'you must specify an entity class in the :with option' resolution: 'eg: represent User, :with => Entity::User' length: 'is expected to have length within %{min} and %{max}' length_is: 'is expected to have length exactly equal to %{is}' length_max: 'is expected to have length less than or equal to %{max}' length_min: 'is expected to have length greater than or equal to %{min}' missing_group_type: 'group type is required' missing_mime_type: problem: 'missing mime type for %{new_format}' resolution: 'you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES or add your own with content_type :%{new_format}, ''application/%{new_format}'' ' missing_option: 'you must specify :%{option} options' missing_vendor_option: problem: 'missing :vendor option' resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter''' summary: 'when version using header, you must specify :vendor option' mutual_exclusion: 'are mutually exclusive' presence: 'is missing' regexp: 'is invalid' same_as: 'is not the same as %{parameter}' too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})' too_many_multipart_files: 'the number of uploaded files exceeded the system''s configured limit (%{limit})' unknown_auth_strategy: 'unknown auth strategy: %{strategy}' unknown_options: 'unknown options: %{options}' unknown_parameter: 'unknown parameter: %{param}' unknown_params_builder: 'unknown params_builder: %{params_builder_type}' unknown_validator: 'unknown validator: %{validator_type}' unsupported_group_type: 'group type must be Array, Hash, JSON or Array[JSON]' values: 'does not have a valid value' ================================================ FILE: lib/grape/middleware/auth/base.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Auth class Base < Grape::Middleware::Base def initialize(app, **options) super @auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy| raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy end end def call!(env) @env = env @auth_strategy.create(app, options) do |*args| context.instance_exec(*args, &options[:proc]) end.call(env) end end end end end ================================================ FILE: lib/grape/middleware/auth/dsl.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Auth module DSL def auth(type = nil, options = {}, &block) namespace_inheritable = inheritable_setting.namespace_inheritable return namespace_inheritable[:auth] unless type namespace_inheritable[:auth] = options.reverse_merge(type: type.to_sym, proc: block) use Grape::Middleware::Auth::Base, namespace_inheritable[:auth] end # Add HTTP Basic authorization to the API. # # @param [Hash] options A hash of options. # @option options [String] :realm "API Authorization" The HTTP Basic realm. def http_basic(options = {}, &) options[:realm] ||= 'API Authorization' auth(:http_basic, options, &) end def http_digest(options = {}, &) options[:realm] ||= 'API Authorization' if options[:realm].respond_to?(:values_at) options[:realm][:opaque] ||= 'secret' else options[:opaque] ||= 'secret' end auth(:http_digest, options, &) end end end end end ================================================ FILE: lib/grape/middleware/auth/strategies.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Auth module Strategies module_function def add(label, strategy, option_fetcher = ->(_) { [] }) auth_strategies[label] = StrategyInfo.new(strategy, option_fetcher) end def auth_strategies @auth_strategies ||= { http_basic: StrategyInfo.new(Rack::Auth::Basic, ->(settings) { [settings[:realm]] }) } end def [](label) auth_strategies[label] end end end end end ================================================ FILE: lib/grape/middleware/auth/strategy_info.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Auth StrategyInfo = Struct.new(:auth_class, :settings_fetcher) do def create(app, options, &block) strategy_args = settings_fetcher.call(options) auth_class.new(app, *strategy_args, &block) end end end end end ================================================ FILE: lib/grape/middleware/base.rb ================================================ # frozen_string_literal: true module Grape module Middleware class Base include Grape::DSL::Headers attr_reader :app, :env, :options # @param [Rack Application] app The standard argument for a Rack middleware. # @param [Hash] options A hash of options, simply stored for use by subclasses. def initialize(app, **options) @app = app @options = merge_default_options(options) @app_response = nil end def call(env) dup.call!(env).to_a end def call!(env) @env = env before begin @app_response = @app.call(@env) ensure begin after_response = after rescue StandardError => e warn "caught error of type #{e.class} in after callback inside #{self.class.name} : #{e.message}" raise e end end response = after_response || @app_response merge_headers response response end # @abstract # Called before the application is called in the middleware lifecycle. def before; end # @abstract # Called after the application is called in the middleware lifecycle. # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards. def after; end def rack_request @rack_request ||= Rack::Request.new(env) end def context env[Grape::Env::API_ENDPOINT] end def response return @app_response if @app_response.is_a?(Rack::Response) @app_response = Rack::Response[*@app_response] end def content_types @content_types ||= Grape::ContentTypes.content_types_for(options[:content_types]) end def mime_types @mime_types ||= Grape::ContentTypes.mime_types_for(content_types) end def content_type_for(format) content_types_indifferent_access[format] end def content_type content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html' end def query_params rack_request.GET rescue Rack::QueryParser::ParamsTooDeepError raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit) rescue Rack::Utils::ParameterTypeError raise Grape::Exceptions::ConflictingTypes end private def merge_headers(response) return unless headers.is_a?(Hash) case response when Rack::Response then response.headers.merge!(headers) when Array then response[1].merge!(headers) end end def content_types_indifferent_access @content_types_indifferent_access ||= content_types.with_indifferent_access end def merge_default_options(options) if respond_to?(:default_options) default_options.deep_merge(options) elsif self.class.const_defined?(:DEFAULT_OPTIONS) self.class::DEFAULT_OPTIONS.deep_merge(options) else options end end def try_scrub(obj) obj.respond_to?(:valid_encoding?) && !obj.valid_encoding? ? obj.scrub : obj end end end end ================================================ FILE: lib/grape/middleware/error.rb ================================================ # frozen_string_literal: true module Grape module Middleware class Error < Base DEFAULT_OPTIONS = { default_status: 500, default_message: '', format: :txt, rescue_all: false, rescue_grape_exceptions: false, rescue_subclasses: true, rescue_options: { backtrace: false, original_exception: false }.freeze }.freeze def call!(env) @env = env error_response(catch(:error) { return @app.call(@env) }) rescue Exception => e # rubocop:disable Lint/RescueException run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT]) end private def rack_response(status, headers, message) message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html' Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers)) end def format_message(message, backtrace, original_exception = nil) format = env[Grape::Env::API_FORMAT] || options[:format] formatter = Grape::ErrorFormatter.formatter_for(format, options[:error_formatters], options[:default_error_formatter]) return formatter.call(message, backtrace, options, env, original_exception) if formatter throw :error, status: 406, message: "The requested format '#{format}' is not supported.", backtrace: backtrace, original_exception: original_exception end def find_handler(klass) rescue_handler_for_base_only_class(klass) || rescue_handler_for_class_or_its_ancestor(klass) || rescue_handler_for_grape_exception(klass) || rescue_handler_for_any_class(klass) || raise end def error_response(error = {}) status = error[:status] || options[:default_status] env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called message = error[:message] || options[:default_message] headers = { Rack::CONTENT_TYPE => content_type }.tap do |h| h.merge!(error[:headers]) if error[:headers].is_a?(Hash) end backtrace = error[:backtrace] || error[:original_exception]&.backtrace || [] original_exception = error.is_a?(Exception) ? error : error[:original_exception] rack_response(status, headers, format_message(message, backtrace, original_exception)) end def default_rescue_handler(exception) error_response(message: exception.message, backtrace: exception.backtrace, original_exception: exception) end def rescue_handler_for_base_only_class(klass) error, handler = options[:base_only_rescue_handlers]&.find { |err, _handler| klass == err } return unless error handler || method(:default_rescue_handler) end def rescue_handler_for_class_or_its_ancestor(klass) error, handler = options[:rescue_handlers]&.find { |err, _handler| klass <= err } return unless error handler || method(:default_rescue_handler) end def rescue_handler_for_grape_exception(klass) return unless klass <= Grape::Exceptions::Base return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader return unless options[:rescue_grape_exceptions] || !options[:rescue_all] options[:grape_exceptions_rescue_handler] || method(:error_response) end def rescue_handler_for_any_class(klass) return unless klass <= StandardError return unless options[:rescue_all] || options[:rescue_grape_exceptions] options[:all_rescue_handler] || method(:default_rescue_handler) end def run_rescue_handler(handler, error, endpoint) handler = endpoint.public_method(handler) if handler.instance_of?(Symbol) response = catch(:error) do handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler) end if error?(response) error_response(response) elsif response.is_a?(Rack::Response) response else run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint) end end def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil) env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route rack_response( status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type), format_message(message, backtrace, original_exception) ) end def error?(response) return false unless response.is_a?(Hash) response.key?(:message) && response.key?(:status) && response.key?(:headers) end end end end ================================================ FILE: lib/grape/middleware/filter.rb ================================================ # frozen_string_literal: true module Grape module Middleware # This is a simple middleware for adding before and after filters # to Grape APIs. It is used like so: # # use Grape::Middleware::Filter, before: -> { do_something }, after: -> { do_something } class Filter < Base def before app.instance_eval(&options[:before]) if options[:before] end def after app.instance_eval(&options[:after]) if options[:after] end end end end ================================================ FILE: lib/grape/middleware/formatter.rb ================================================ # frozen_string_literal: true module Grape module Middleware class Formatter < Base DEFAULT_OPTIONS = { default_format: :txt }.freeze ALL_MEDIA_TYPES = '*/*' def before negotiate_content_type read_body_input end def after return unless @app_response status, headers, bodies = *@app_response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) [status, headers, []] else build_formatted_response(status, headers, bodies) end end private def build_formatted_response(status, headers, bodies) headers = ensure_content_type(headers) if bodies.is_a?(Grape::ServeStream::StreamResponse) Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp| resp.body = bodies.stream end else # Allow content-type to be explicitly overwritten formatter = fetch_formatter(headers, options) bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do bodies.collect { |body| formatter.call(body, env) } end Rack::Response.new(bodymap, status, headers) end rescue Grape::Exceptions::InvalidFormatter => e throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e end def fetch_formatter(headers, options) api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] } Grape::Formatter.formatter_for(api_format, options[:formatters]) end # Set the content type header for the API format if it is not already present. # # @param headers [Hash] # @return [Hash] def ensure_content_type(headers) if headers[Rack::CONTENT_TYPE] headers else headers.merge(Rack::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT])) end end def read_body_input input = rack_request.body # reads RACK_INPUT return if input.nil? return unless read_body_input? rewind = input.respond_to?(:rewind) input.rewind if rewind body = env[Grape::Env::API_REQUEST_INPUT] = input.read begin read_rack_input(body) ensure input.rewind if rewind end end def read_rack_input(body) return if body.empty? media_type = rack_request.media_type fmt = media_type ? mime_types[media_type] : options[:default_format] throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt) parser = Grape::Parser.parser_for fmt, options[:parsers] if parser begin body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env)) if body.is_a?(Hash) env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH) env[Rack::RACK_REQUEST_FORM_HASH].merge(body) else body end env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT] end rescue Grape::Exceptions::Base => e raise e rescue StandardError => e throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e end else env[Grape::Env::API_REQUEST_BODY] = body end end # this middleware will not try to format the following content-types since Rack already handles them # when calling Rack's `params` function # - application/x-www-form-urlencoded # - multipart/form-data # - multipart/related # - multipart/mixed def read_body_input? (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) && !(rack_request.form_data? && rack_request.content_type) && !rack_request.parseable_data? && (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked') end def negotiate_content_type fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format] if content_type_for(fmt) env[Grape::Env::API_FORMAT] = fmt.to_sym else throw :error, status: 406, message: "The requested format '#{fmt}' is not supported." end end def format_from_extension request_path = try_scrub(rack_request.path) dot_pos = request_path.rindex('.') return unless dot_pos extension = request_path[(dot_pos + 1)..] extension if content_type_for(extension) end def format_from_header accept_header = try_scrub(env['HTTP_ACCEPT']) return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys) mime_types[media_type] if media_type end end end end ================================================ FILE: lib/grape/middleware/globals.rb ================================================ # frozen_string_literal: true module Grape module Middleware class Globals < Base def before request = Grape::Request.new(@env, build_params_with: @options[:build_params_with]) @env[Grape::Env::GRAPE_REQUEST] = request @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT] end end end end ================================================ FILE: lib/grape/middleware/stack.rb ================================================ # frozen_string_literal: true module Grape module Middleware # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack # It allows to insert and insert after class Stack extend Forwardable class Middleware attr_reader :args, :block, :klass def initialize(klass, args, block) @klass = klass @args = args @block = block end def name klass.name end def ==(other) case other when Middleware klass == other.klass when Class klass == other || (name.nil? && klass.superclass == other) end end def inspect klass.to_s end def build(builder) # we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords # like ActionDispatch::RequestId since middleware arguments are serialized # https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash) builder.use(klass, *args, &block) end end include Enumerable attr_accessor :middlewares, :others def_delegators :middlewares, :each, :size, :last, :[] def initialize @middlewares = [] @others = [] end def insert(index, klass, *args, &block) index = assert_index(index, :before) middlewares.insert(index, self.class::Middleware.new(klass, args, block)) end alias insert_before insert def insert_after(index, ...) index = assert_index(index, :after) insert(index + 1, ...) end def use(klass, *args, &block) middleware = self.class::Middleware.new(klass, args, block) middlewares.push(middleware) end def merge_with(middleware_specs) middleware_specs.each do |operation, klass, *args| if args.last.is_a?(Proc) last_proc = args.pop public_send(operation, klass, *args, &last_proc) else public_send(operation, klass, *args) end end end # @return [Rack::Builder] the builder object with our middlewares applied def build Rack::Builder.new.tap do |builder| others.shift(others.size).each { |m| merge_with(m) } middlewares.each do |m| m.build(builder) end end end # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]]) def concat(other_specs) use, not_use = other_specs.partition { |o| o.first == :use } others << not_use merge_with(use) end protected def assert_index(index, where) i = index.is_a?(Integer) ? index : middlewares.index(index) i || raise("No such middleware to insert #{where}: #{index.inspect}") end end end end ================================================ FILE: lib/grape/middleware/versioner/accept_version_header.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the HTTP Accept-Version header # # Example: For request header # Accept-Version: v1 # # The following rack env variables are set: # # env['api.version'] => 'v1' # # If version does not match this route, then a 406 is raised with # X-Cascade header to alert Grape::Router to attempt the next matched # route. class AcceptVersionHeader < Base def before potential_version = try_scrub(env['HTTP_ACCEPT_VERSION']) not_acceptable!('Accept-Version header must be set.') if strict && potential_version.blank? return if potential_version.blank? not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version) env[Grape::Env::API_VERSION] = potential_version end private def not_acceptable!(message) throw :error, status: 406, headers: error_headers, message: message end end end end end ================================================ FILE: lib/grape/middleware/versioner/base.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Versioner class Base < Grape::Middleware::Base DEFAULT_OPTIONS = { pattern: /.*/i, prefix: nil, mount_path: nil, version_options: { strict: false, cascade: true, parameter: 'apiver', vendor: nil }.freeze }.freeze CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze DEFAULT_OPTIONS.each_key do |key| define_method key do options[key] end end DEFAULT_OPTIONS[:version_options].each_key do |key| define_method key do options[:version_options][key] end end def self.inherited(klass) super Versioner.register(klass) end attr_reader :error_headers, :versions def initialize(app, **options) super @error_headers = cascade ? CASCADE_PASS_HEADER : {} @versions = options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match end def potential_version_match?(potential_version) versions.blank? || versions.include?(potential_version) end def version_not_found! throw :error, status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER end private def available_media_types @available_media_types ||= begin media_types = [] base_media_type = "application/vnd.#{vendor}" content_types.each_key do |extension| versions&.reverse_each do |version| media_types << "#{base_media_type}-#{version}+#{extension}" media_types << "#{base_media_type}-#{version}" end media_types << "#{base_media_type}+#{extension}" end media_types << base_media_type media_types.concat(content_types.values.flatten) media_types end end end end end end ================================================ FILE: lib/grape/middleware/versioner/header.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the HTTP Accept header with the pattern: # application/vnd.:vendor-:version+:format # # Example: For request header # Accept: application/vnd.mycompany.a-cool-resource-v1+json # # The following rack env variables are set: # # env['api.type'] => 'application' # env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json' # env['api.vendor] => 'mycompany.a-cool-resource' # env['api.version] => 'v1' # env['api.format] => 'json' # # If version does not match this route, then a 406 is raised with # X-Cascade header to alert Grape::Router to attempt the next matched # route. class Header < Base def before match_best_quality_media_type! do |media_type| env.update( Grape::Env::API_TYPE => media_type.type, Grape::Env::API_SUBTYPE => media_type.subtype, Grape::Env::API_VENDOR => media_type.vendor, Grape::Env::API_VERSION => media_type.version, Grape::Env::API_FORMAT => media_type.format ) end end private def match_best_quality_media_type! return unless vendor strict_header_checks! media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types) if media_type yield media_type else fail! end end def accept_header env['HTTP_ACCEPT'] end def strict_header_checks! return unless strict accept_header_check! version_and_vendor_check! end def accept_header_check! return if accept_header.present? invalid_accept_header!('Accept header must be set.') end def version_and_vendor_check! return if versions.blank? || version_and_vendor? invalid_accept_header!('API vendor or version not found.') end def q_values_mime_types @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first) end def version_and_vendor? q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) } end def invalid_accept_header!(message) raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers) end def invalid_version_header!(message) raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers) end def fail! return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present? media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) } vendor_not_found!(media_types) || version_not_found!(media_types) end def vendor_not_found!(media_types) return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor } invalid_accept_header!('API vendor not found.') end def version_not_found!(media_types) return unless media_types.all? { |media_type| media_type&.version && versions && !versions.include?(media_type.version) } invalid_version_header!('API version not found.') end end end end end ================================================ FILE: lib/grape/middleware/versioner/param.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the request parameters and removes that parameter from the # request parameters for subsequent middleware and API. # If the version substring does not match any potential initialized # versions, a 404 error is thrown. # If the version substring is not passed the version (highest mounted) # version will be used. # # Example: For a uri path # /resource?apiver=v1 # # The following rack env variables are set and path is rewritten to # '/resource': # # env['api.version'] => 'v1' class Param < Base def before potential_version = query_params[parameter] return if potential_version.blank? version_not_found! unless potential_version_match?(potential_version) env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter) end end end end end ================================================ FILE: lib/grape/middleware/versioner/path.rb ================================================ # frozen_string_literal: true module Grape module Middleware module Versioner # This middleware sets various version related rack environment variables # based on the uri path and removes the version substring from the uri # path. If the version substring does not match any potential initialized # versions, a 404 error is thrown. # # Example: For a uri path # /v1/resource # # The following rack env variables are set and path is rewritten to # '/resource': # # env['api.version'] => 'v1' # class Path < Base def before path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO]) return if path_info == '/' [mount_path, Grape::Router.normalize_path(prefix)].each do |path| path_info = path_info.delete_prefix(path) if path.present? && path != '/' && path_info.start_with?(path) end slash_position = path_info.index('/', 1) # omit the first one return unless slash_position potential_version = path_info[1..(slash_position - 1)] return unless potential_version.match?(pattern) version_not_found! unless potential_version_match?(potential_version) env[Grape::Env::API_VERSION] = potential_version end end end end end ================================================ FILE: lib/grape/middleware/versioner.rb ================================================ # frozen_string_literal: true # Versioners set env['api.version'] when a version is defined on an API and # on the requests. The current methods for determining version are: # # :header - version from HTTP Accept header. # :accept_version_header - version from HTTP Accept-Version header # :path - version from uri. e.g. /v1/resource # :param - version from uri query string, e.g. /v1/resource?apiver=v1 # See individual classes for details. module Grape module Middleware module Versioner extend Grape::Util::Registry module_function # @param strategy [Symbol] :path, :header, :accept_version_header or :param # @return a middleware class based on strategy def using(strategy) raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy) registry[strategy] end end end end ================================================ FILE: lib/grape/namespace.rb ================================================ # frozen_string_literal: true module Grape # A container for endpoints or other namespaces, which allows for both # logical grouping of endpoints as well as sharing common configuration. # May also be referred to as group, segment, or resource. class Namespace attr_reader :space, :requirements, :options # @param space [String] the name of this namespace # @param options [Hash] options hash # @option options :requirements [Hash] param-regex pairs, all of which must # be met by a request's params for all endpoints in this namespace, or # validation will fail and return a 422. def initialize(space, requirements: nil, **options) @space = space.to_s @requirements = requirements @options = options end # (see ::joined_space_path) def self.joined_space(settings) settings&.map(&:space) end def eql?(other) other.class == self.class && other.space == space && other.requirements == requirements && other.options == options end alias == eql? def hash [self.class, space, requirements, options].hash end # Join the namespaces from a list of settings to create a path prefix. # @param settings [Array] list of Grape::Util::InheritableSettings. def self.joined_space_path(settings) JoinedSpaceCache[joined_space(settings)] end class JoinedSpaceCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, joined_space| h[joined_space] = Grape::Router.normalize_path(joined_space.join('/')) end end end end end ================================================ FILE: lib/grape/params_builder/base.rb ================================================ # frozen_string_literal: true module Grape module ParamsBuilder class Base class << self def call(_params) raise NotImplementedError end private def inherited(klass) super ParamsBuilder.register(klass) end end end end end ================================================ FILE: lib/grape/params_builder/hash.rb ================================================ # frozen_string_literal: true module Grape module ParamsBuilder class Hash < Base def self.call(params) params.deep_symbolize_keys end end end end ================================================ FILE: lib/grape/params_builder/hash_with_indifferent_access.rb ================================================ # frozen_string_literal: true module Grape module ParamsBuilder class HashWithIndifferentAccess < Base def self.call(params) params.with_indifferent_access end end end end ================================================ FILE: lib/grape/params_builder/hashie_mash.rb ================================================ # frozen_string_literal: true module Grape module ParamsBuilder class HashieMash < Base def self.call(params) ::Hashie::Mash.new(params) end end end end ================================================ FILE: lib/grape/params_builder.rb ================================================ # frozen_string_literal: true module Grape module ParamsBuilder extend Grape::Util::Registry module_function def params_builder_for(short_name) raise Grape::Exceptions::UnknownParamsBuilder, short_name unless registry.key?(short_name) registry[short_name] end end end ================================================ FILE: lib/grape/parser/base.rb ================================================ # frozen_string_literal: true module Grape module Parser class Base def self.call(_object, _env) raise NotImplementedError end def self.inherited(klass) super Parser.register(klass) end end end end ================================================ FILE: lib/grape/parser/json.rb ================================================ # frozen_string_literal: true module Grape module Parser class Json < Base def self.call(object, _env) ::Grape::Json.load(object) rescue ::Grape::Json::ParseError # handle JSON parsing errors via the rescue handlers or provide error message raise Grape::Exceptions::InvalidMessageBody.new('application/json') end end end end ================================================ FILE: lib/grape/parser/xml.rb ================================================ # frozen_string_literal: true module Grape module Parser class Xml < Base def self.call(object, _env) ::Grape::Xml.parse(object) rescue ::Grape::Xml::ParseError # handle XML parsing errors via the rescue handlers or provide error message raise Grape::Exceptions::InvalidMessageBody.new('application/xml') end end end end ================================================ FILE: lib/grape/parser.rb ================================================ # frozen_string_literal: true module Grape module Parser extend Grape::Util::Registry module_function def parser_for(format, parsers = nil) return parsers[format] if parsers&.key?(format) registry[format] end end end ================================================ FILE: lib/grape/path.rb ================================================ # frozen_string_literal: true module Grape # Represents a path to an endpoint. class Path DEFAULT_FORMAT_SEGMENT = '(/.:format)' NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)' VERSION_SEGMENT = ':version' attr_reader :origin, :suffix def initialize(raw_path, raw_namespace, settings) @origin = PartsCache[build_parts(raw_path, raw_namespace, settings)] @suffix = build_suffix(raw_path, raw_namespace, settings) end def to_s "#{origin}#{suffix}" end private def build_suffix(raw_path, raw_namespace, settings) if uses_specific_format?(settings) "(.#{settings[:format]})" elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path)) NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT else DEFAULT_FORMAT_SEGMENT end end def build_parts(raw_path, raw_namespace, settings) [].tap do |parts| add_part(parts, settings[:mount_path]) add_part(parts, settings[:root_prefix]) parts << VERSION_SEGMENT if uses_path_versioning?(settings) add_part(parts, raw_namespace) add_part(parts, raw_path) end end def add_part(parts, value) parts << value if value && not_slash?(value) end def not_slash?(value) value != '/' end def uses_specific_format?(settings) return false unless settings.key?(:format) && settings.key?(:content_types) settings[:format] && Array(settings[:content_types]).size == 1 end def uses_path_versioning?(settings) return false unless settings.key?(:version) && settings[:version_options]&.key?(:using) settings[:version] && settings[:version_options][:using] == :path end def valid_part?(part) part&.match?(/^\S/) && not_slash?(part) end class PartsCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, parts| h[parts] = Grape::Router.normalize_path(parts.join('/')) end end end end end ================================================ FILE: lib/grape/presenters/presenter.rb ================================================ # frozen_string_literal: true module Grape module Presenters class Presenter def self.represent(object, **_options) object end end end end ================================================ FILE: lib/grape/railtie.rb ================================================ # frozen_string_literal: true module Grape class Railtie < ::Rails::Railtie initializer 'grape.deprecator' do |app| app.deprecators[:grape] = Grape.deprecator end end end ================================================ FILE: lib/grape/request.rb ================================================ # frozen_string_literal: true module Grape class Request < Rack::Request # Based on rack 3 KNOWN_HEADERS # https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/headers.rb#L10 KNOWN_HEADERS = %w[ Accept Accept-CH Accept-Encoding Accept-Language Accept-Patch Accept-Ranges Accept-Version Access-Control-Allow-Credentials Access-Control-Allow-Headers Access-Control-Allow-Methods Access-Control-Allow-Origin Access-Control-Expose-Headers Access-Control-Max-Age Age Allow Alt-Svc Authorization Cache-Control Client-Ip Connection Content-Disposition Content-Encoding Content-Language Content-Length Content-Location Content-MD5 Content-Range Content-Security-Policy Content-Security-Policy-Report-Only Content-Type Cookie Date Delta-Base Dnt ETag Expect-CT Expires Feature-Policy Forwarded Host If-Modified-Since If-None-Match IM Last-Modified Link Location NEL P3P Permissions-Policy Pragma Preference-Applied Proxy-Authenticate Public-Key-Pins Range Referer Referrer-Policy Refresh Report-To Retry-After Sec-Fetch-Dest Sec-Fetch-Mode Sec-Fetch-Site Sec-Fetch-User Server Set-Cookie Status Strict-Transport-Security Timing-Allow-Origin Tk Trailer Transfer-Encoding Upgrade Upgrade-Insecure-Requests User-Agent Vary Version Via Warning WWW-Authenticate X-Accel-Buffering X-Accel-Charset X-Accel-Expires X-Accel-Limit-Rate X-Accel-Mapping X-Accel-Redirect X-Access-Token X-Auth-Request-Access-Token X-Auth-Request-Email X-Auth-Request-Groups X-Auth-Request-Preferred-Username X-Auth-Request-Redirect X-Auth-Request-Token X-Auth-Request-User X-Cascade X-Client-Ip X-Content-Duration X-Content-Security-Policy X-Content-Type-Options X-Correlation-Id X-Download-Options X-Forwarded-Access-Token X-Forwarded-Email X-Forwarded-For X-Forwarded-Groups X-Forwarded-Host X-Forwarded-Port X-Forwarded-Preferred-Username X-Forwarded-Proto X-Forwarded-Scheme X-Forwarded-Ssl X-Forwarded-Uri X-Forwarded-User X-Frame-Options X-HTTP-Method-Override X-Permitted-Cross-Domain-Policies X-Powered-By X-Real-IP X-Redirect-By X-Request-Id X-Requested-With X-Runtime X-Sendfile X-Sendfile-Type X-UA-Compatible X-WebKit-CS X-XSS-Protection ].each_with_object({}) do |header, response| response["HTTP_#{header.upcase.tr('-', '_')}"] = header end.freeze alias rack_params params alias rack_cookies cookies def initialize(env, build_params_with: nil) super(env) @params_builder = Grape::ParamsBuilder.params_builder_for(build_params_with || Grape.config.param_builder) end def params @params ||= make_params end def headers @headers ||= build_headers end def cookies @cookies ||= Grape::Cookies.new(-> { rack_cookies }) end # needs to be public until extensions param_builder are removed def grape_routing_args # preserve version from query string parameters env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {} end private def make_params @params_builder.call(rack_params).deep_merge!(grape_routing_args) rescue EOFError raise Grape::Exceptions::EmptyMessageBody.new(content_type) rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit) rescue Rack::QueryParser::ParamsTooDeepError raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit) rescue Rack::Utils::ParameterTypeError raise Grape::Exceptions::ConflictingTypes rescue Rack::Utils::InvalidParameterError raise Grape::Exceptions::InvalidParameters end def build_headers each_header.with_object(Grape::Util::Header.new) do |(k, v), headers| next unless k.start_with? 'HTTP_' transformed_header = KNOWN_HEADERS.fetch(k) { -k[5..].tr('_', '-').downcase } headers[transformed_header] = v end end end end ================================================ FILE: lib/grape/router/base_route.rb ================================================ # frozen_string_literal: true module Grape class Router class BaseRoute extend Forwardable delegate_missing_to :@options attr_reader :options, :pattern def_delegators :@pattern, :path, :origin def_delegators :@options, :description, :version, :requirements, :prefix, :anchor, :settings, :forward_match, *Grape::Util::ApiDescription::DSL_METHODS def initialize(pattern, options = {}) @pattern = pattern @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options) end # see https://github.com/ruby-grape/grape/issues/1348 def namespace @namespace ||= @options[:namespace] end def regexp_capture_index @regexp_capture_index ||= CaptureIndexCache[@index] end def pattern_regexp @pattern.to_regexp end def to_regexp(index) @index = index Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})") end class CaptureIndexCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, index| h[index] = "_#{index}" end end end end end end ================================================ FILE: lib/grape/router/greedy_route.rb ================================================ # frozen_string_literal: true # Act like a Grape::Router::Route but for greedy_match # see @neutral_map module Grape class Router class GreedyRoute < BaseRoute extend Forwardable def_delegators :@endpoint, :call attr_reader :endpoint, :allow_header def initialize(pattern, endpoint:, allow_header:) super(pattern) @endpoint = endpoint @allow_header = allow_header end def params(_input = nil) nil end end end end ================================================ FILE: lib/grape/router/pattern.rb ================================================ # frozen_string_literal: true module Grape class Router class Pattern extend Forwardable DEFAULT_CAPTURES = %w[format version].freeze attr_reader :origin, :path, :pattern, :to_regexp def_delegators :pattern, :params def_delegators :to_regexp, :=== alias match? === def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:) @origin = origin @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]] @pattern = Mustermann::Grape.new(@path, uri_decode: true, params: params, capture: extract_capture(format, version, requirements)) @to_regexp = @pattern.to_regexp end def captures_default to_regexp.names .delete_if { |n| DEFAULT_CAPTURES.include?(n) } .to_h { |k| [k, ''] } end private def extract_capture(format, version, requirements) capture = {} capture[:format] = map_str(format) if format.present? capture[:version] = map_str(version) if version.present? return capture if requirements.blank? requirements.merge(capture) end def build_path_from_pattern(pattern, anchor) if pattern.end_with?('*path') pattern.dup.insert(pattern.rindex('/') + 1, '?') elsif anchor pattern elsif pattern.end_with?('/') "#{pattern}?*path" else "#{pattern}/?*path" end end def map_str(value) Array.wrap(value).map(&:to_s) end class PatternCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, (pattern, suffix)| h[[pattern, suffix]] = -"#{pattern}#{suffix}" end end end end end end ================================================ FILE: lib/grape/router/route.rb ================================================ # frozen_string_literal: true module Grape class Router class Route < BaseRoute extend Forwardable FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) } NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) } attr_reader :app, :request_method, :index def_delegators :@app, :call def initialize(endpoint, method, pattern, options) super(pattern, options) @app = endpoint @request_method = upcase_method(method) @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD end def convert_to_head_request! @request_method = Rack::HEAD end def apply(app) @app = app self end def match?(input) return false if input.blank? @match_function.call(input, pattern) end def params(input = nil) return params_without_input if input.blank? parsed = pattern.params(input) return unless parsed parsed.compact.symbolize_keys end private def params_without_input @params_without_input ||= pattern.captures_default.merge(options[:params]) end def upcase_method(method) method_s = method.to_s Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase end end end end ================================================ FILE: lib/grape/router.rb ================================================ # frozen_string_literal: true module Grape class Router # Taken from Rails # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" # normalize_path("foo") # => "/foo" # normalize_path("") # => "/" # normalize_path("/%ab") # => "/%AB" # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19 def self.normalize_path(path) return '/' unless path return path if path == '/' # Fast path for the overwhelming majority of paths that don't need to be normalized return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//})) # Slow path encoding = path.encoding path = "/#{path}" path.squeeze!('/') unless path == '/' path.delete_suffix!('/') path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase } end path.force_encoding(encoding) end def initialize @neutral_map = [] @neutral_regexes = [] @map = Hash.new { |hash, key| hash[key] = [] } @optimized_map = Hash.new { |hash, key| hash[key] = // } end def compile! return if @compiled @union = Regexp.union(@neutral_regexes) @neutral_regexes = nil (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method| next unless @map.key?(method) routes = @map[method] optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) } @optimized_map[method] = Regexp.union(optimized_map) end @compiled = true end def append(route) @map[route.request_method] << route end def associate_routes(greedy_route) @neutral_regexes << greedy_route.to_regexp(@neutral_map.length) @neutral_map << greedy_route end def call(env) with_optimization do input = Router.normalize_path(env[Rack::PATH_INFO]) method = env[Rack::REQUEST_METHOD] response, route = identity(input, method, env) response || rotation(input, method, env, route) end end def recognize_path(input) any = with_optimization { greedy_match?(input) } return if any == default_response any.endpoint end private def identity(input, method, env) route = nil response = transaction(input, method, env) do route = match?(input, method) process_route(route, input, env) if route end [response, route] end def rotation(input, method, env, exact_route) response = nil @map[method].each do |route| next if exact_route == route next unless route.match?(input) response = process_route(route, input, env) break unless cascade?(response) end response end def transaction(input, method, env) # using a Proc is important since `return` will exit the enclosing function cascade_or_return_response = proc do |response| if response cascade?(response).tap do |cascade| return response unless cascade # we need to close the body if possible before dismissing response[2].close if response[2].respond_to?(:close) end end end response = yield last_response_cascade = cascade_or_return_response.call(response) last_neighbor_route = greedy_match?(input) # If last_neighbor_route exists and request method is OPTIONS, # return response by using #include_allow_header. return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && method == Rack::OPTIONS && last_neighbor_route route = match?(input, '*') return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route last_response_cascade = cascade_or_return_response.call(process_route(route, input, env)) if route return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && last_neighbor_route nil end def process_route(route, input, env, include_allow_header: false) args = env[Grape::Env::GRAPE_ROUTING_ARGS] || { route_info: route } route_params = route.params(input) routing_args = args.merge(route_params || {}) env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header route.call(env) end def with_optimization compile! yield || default_response end def default_response headers = Grape::Util::Header.new.merge('X-Cascade' => 'pass') [404, headers, ['404 Not Found']] end def match?(input, method) @optimized_map[method].match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } } end def greedy_match?(input) @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } } end def cascade?(response) response && response[1]['X-Cascade'] == 'pass' end end end ================================================ FILE: lib/grape/serve_stream/file_body.rb ================================================ # frozen_string_literal: true module Grape module ServeStream CHUNK_SIZE = 16_384 # Class helps send file through API class FileBody attr_reader :path # @param path [String] def initialize(path) @path = path end # Need for Rack::Sendfile middleware # # @return [String] def to_path path end def each File.open(path, 'rb') do |file| while (chunk = file.read(CHUNK_SIZE)) yield chunk end end end def ==(other) path == other.path end end end end ================================================ FILE: lib/grape/serve_stream/sendfile_response.rb ================================================ # frozen_string_literal: true module Grape module ServeStream # Response should respond to to_path method # for using Rack::SendFile middleware class SendfileResponse < Rack::Response def respond_to?(method_name, include_all = false) if method_name == :to_path @body.respond_to?(:to_path, include_all) else super end end def to_path @body.to_path end end end end ================================================ FILE: lib/grape/serve_stream/stream_response.rb ================================================ # frozen_string_literal: true module Grape module ServeStream # A simple class used to identify responses which represent streams (or files) and do not # need to be formatted or pre-read by Rack::Response class StreamResponse attr_reader :stream # @param stream [Object] def initialize(stream) @stream = stream end # Equality provided mostly for tests. # # @return [Boolean] def ==(other) stream == other.stream end end end end ================================================ FILE: lib/grape/util/api_description.rb ================================================ # frozen_string_literal: true module Grape module Util class ApiDescription DSL_METHODS = %i[ body_name consumes default deprecated detail entity headers hidden http_codes is_array named nickname params produces security summary tags ].freeze def initialize(description, endpoint_configuration, &) @endpoint_configuration = endpoint_configuration @attributes = { description: description } instance_eval(&) end DSL_METHODS.each do |attribute| define_method attribute do |value| @attributes[attribute] = value end end alias success entity alias failure http_codes def configuration @configuration ||= eval_endpoint_config(@endpoint_configuration) end def settings @attributes end private def eval_endpoint_config(configuration) return configuration if configuration.is_a?(Hash) configuration.evaluate end end end end ================================================ FILE: lib/grape/util/base_inheritable.rb ================================================ # frozen_string_literal: true module Grape module Util # Base for classes which need to operate with own values kept # in the hash and inherited values kept in a Hash-like object. class BaseInheritable attr_accessor :inherited_values, :new_values # @param inherited_values [Object] An object implementing an interface # of the Hash class. def initialize(inherited_values = nil) @inherited_values = inherited_values || {} @new_values = {} end def delete(*keys) keys.map do |key| # since delete returns the deleted value, seems natural to `map` the result new_values.delete key end end def initialize_copy(other) super self.inherited_values = other.inherited_values self.new_values = other.new_values.dup end def keys if new_values.any? inherited_values.keys.tap do |combined| combined.concat(new_values.keys) combined.uniq! end else inherited_values.keys end end def key?(name) inherited_values.key?(name) || new_values.key?(name) end end end end ================================================ FILE: lib/grape/util/cache.rb ================================================ # frozen_string_literal: true module Grape module Util class Cache include Singleton attr_reader :cache class << self extend Forwardable def_delegators :cache, :[] def_delegators :instance, :cache end end end end ================================================ FILE: lib/grape/util/endpoint_configuration.rb ================================================ # frozen_string_literal: true module Grape module Util class EndpointConfiguration < Lazy::ValueHash end end end ================================================ FILE: lib/grape/util/header.rb ================================================ # frozen_string_literal: true module Grape module Util if Gem::Version.new(Rack.release) >= Gem::Version.new('3') require 'rack/headers' Header = Rack::Headers else require 'rack/utils' Header = Rack::Utils::HeaderHash end end end ================================================ FILE: lib/grape/util/inheritable_setting.rb ================================================ # frozen_string_literal: true module Grape module Util # A branchable, inheritable settings object which can store both stackable # and inheritable values (see InheritableValues and StackableValues). class InheritableSetting attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies # Retrieve global settings. def self.global @global ||= {} end # Clear all global settings. # @api private # @note only for testing def self.reset_global! @global = {} end # Instantiate a new settings instance, with blank values. The fresh # instance can then be set to inherit from an existing instance (see # #inherit_from). def initialize self.route = {} self.api_class = {} self.namespace = InheritableValues.new # only inheritable from a parent when # used with a mount, or should every API::Class be a separate namespace by default? self.namespace_inheritable = InheritableValues.new self.namespace_stackable = StackableValues.new self.namespace_reverse_stackable = ReverseStackableValues.new self.point_in_time_copies = [] self.parent = nil end # Return the class-level global properties. def global self.class.global end # Set our inherited values to the given parent's current values. Also, # update the inherited values on any settings instances which were forked # from us. # @param parent [InheritableSetting] def inherit_from(parent) return if parent.nil? self.parent = parent namespace_inheritable.inherited_values = parent.namespace_inheritable namespace_stackable.inherited_values = parent.namespace_stackable namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable self.route = parent.route.merge(route) point_in_time_copies.map { |cloned_one| cloned_one.inherit_from parent } end # Create a point-in-time copy of this settings instance, with clones of # all our values. Note that, should this instance's parent be set or # changed via #inherit_from, it will copy that inheritence to any copies # which were made. def point_in_time_copy self.class.new.tap do |new_setting| point_in_time_copies << new_setting new_setting.point_in_time_copies = [] new_setting.namespace = namespace.clone new_setting.namespace_inheritable = namespace_inheritable.clone new_setting.namespace_stackable = namespace_stackable.clone new_setting.namespace_reverse_stackable = namespace_reverse_stackable.clone new_setting.route = route.clone new_setting.api_class = api_class new_setting.inherit_from(parent) end end # Resets the instance store of per-route settings. # @api private def route_end @route = {} end # Return a serializable hash of our values. def to_hash { global: global.clone, route: route.clone, namespace: namespace.to_hash, namespace_inheritable: namespace_inheritable.to_hash, namespace_stackable: namespace_stackable.to_hash, namespace_reverse_stackable: namespace_reverse_stackable.to_hash } end def namespace_stackable_with_hash(key) data = namespace_stackable[key] return if data.blank? data.each_with_object({}) { |value, result| result.deep_merge!(value) } end end end end ================================================ FILE: lib/grape/util/inheritable_values.rb ================================================ # frozen_string_literal: true module Grape module Util class InheritableValues < BaseInheritable def [](name) values[name] end def []=(name, value) new_values[name] = value end def merge(new_hash) values.merge!(new_hash) end def to_hash values end protected def values @inherited_values.merge(@new_values) end end end end ================================================ FILE: lib/grape/util/lazy/block.rb ================================================ # frozen_string_literal: true module Grape module Util module Lazy class Block def initialize(&new_block) @block = new_block end def evaluate_from(configuration) @block.call(configuration) end def evaluate @block.call({}) end def lazy? true end def to_s evaluate.to_s end end end end end ================================================ FILE: lib/grape/util/lazy/value.rb ================================================ # frozen_string_literal: true module Grape module Util module Lazy class Value attr_reader :access_keys def initialize(value, access_keys = []) @value = value @access_keys = access_keys end def evaluate_from(configuration) matching_lazy_value = configuration.fetch(@access_keys) matching_lazy_value.evaluate end def evaluate @value end def lazy? true end def reached_by(parent_access_keys, access_key) @access_keys = parent_access_keys + [access_key] self end def to_s evaluate.to_s end end end end end ================================================ FILE: lib/grape/util/lazy/value_array.rb ================================================ # frozen_string_literal: true module Grape module Util module Lazy class ValueArray < ValueEnumerable def initialize(array) super @value_hash = [] array.each_with_index do |value, index| self[index] = value end end def evaluate @value_hash.map(&:evaluate) end end end end end ================================================ FILE: lib/grape/util/lazy/value_enumerable.rb ================================================ # frozen_string_literal: true module Grape module Util module Lazy class ValueEnumerable < Value def [](key) if @value_hash[key].nil? Value.new(nil).reached_by(access_keys, key) else @value_hash[key].reached_by(access_keys, key) end end def fetch(access_keys) fetched_keys = access_keys.dup value = self[fetched_keys.shift] fetched_keys.any? ? value.fetch(fetched_keys) : value end def []=(key, value) @value_hash[key] = case value when Hash ValueHash.new(value) when Array ValueArray.new(value) else Value.new(value) end end end end end end ================================================ FILE: lib/grape/util/lazy/value_hash.rb ================================================ # frozen_string_literal: true module Grape module Util module Lazy class ValueHash < ValueEnumerable def initialize(hash) super @value_hash = ActiveSupport::HashWithIndifferentAccess.new hash.each do |key, value| self[key] = value end end def evaluate @value_hash.transform_values(&:evaluate) end end end end end ================================================ FILE: lib/grape/util/media_type.rb ================================================ # frozen_string_literal: true module Grape module Util class MediaType attr_reader :type, :subtype, :vendor, :version, :format # based on the HTTP Accept header with the pattern: # application/vnd.:vendor-:version+:format VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?[a-z0-9.\-_!^]+?)(?:-(?[a-z0-9*.]+))?(?:\+(?[a-z0-9*\-.]+))?\z/ def initialize(type:, subtype:) @type = type @subtype = subtype VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m| @vendor = m[:vendor] @version = m[:version] @format = m[:format] end end def ==(other) eql?(other) end def eql?(other) self.class == other.class && other.type == type && other.subtype == subtype && other.vendor == vendor && other.version == version && other.format == format end def hash [self.class, type, subtype, vendor, version, format].hash end class << self def best_quality(header, available_media_types) parse(best_quality_media_type(header, available_media_types)) end def parse(media_type) return if media_type.blank? type, subtype = media_type.split('/', 2) return if type.blank? || subtype.blank? new(type: type, subtype: subtype) end def match?(media_type) return false if media_type.blank? subtype = media_type.split('/', 2).last return false if subtype.blank? VENDOR_VERSION_HEADER_REGEX.match?(subtype) end def best_quality_media_type(header, available_media_types) header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header, available_media_types) end end private_class_method :best_quality_media_type end end end ================================================ FILE: lib/grape/util/registry.rb ================================================ # frozen_string_literal: true module Grape module Util module Registry def register(klass) short_name = build_short_name(klass) return if short_name.nil? warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name) registry[short_name] = klass end private def build_short_name(klass) return if klass.name.blank? klass.name.demodulize.underscore end def registry @registry ||= {}.with_indifferent_access end end end end ================================================ FILE: lib/grape/util/reverse_stackable_values.rb ================================================ # frozen_string_literal: true module Grape module Util class ReverseStackableValues < StackableValues protected def concat_values(inherited_value, new_value) return inherited_value unless new_value new_value + inherited_value end end end end ================================================ FILE: lib/grape/util/stackable_values.rb ================================================ # frozen_string_literal: true module Grape module Util class StackableValues < BaseInheritable # Even if there is no value, an empty array will be returned. def [](name) inherited_value = inherited_values[name] new_value = new_values[name] return new_value || [] unless inherited_value concat_values(inherited_value, new_value) end def []=(name, value) new_values[name] ||= [] new_values[name].push value end def to_hash keys.each_with_object({}) do |key, result| result[key] = self[key] end end protected def concat_values(inherited_value, new_value) return inherited_value unless new_value inherited_value + new_value end end end end ================================================ FILE: lib/grape/util/translation.rb ================================================ # frozen_string_literal: true module Grape module Util module Translation FALLBACK_LOCALE = :en private_constant :FALLBACK_LOCALE # Sentinel returned by I18n when a key is missing (passed as the default: # value). Using a named class rather than plain Object.new makes it # identifiable in debug output and immune to backends that call .to_s on # the default before returning it. MISSING = Class.new { def inspect = 'Grape::Util::Translation::MISSING' }.new.freeze private_constant :MISSING private # Extra keyword args (**) are forwarded verbatim to I18n as interpolation # variables (e.g. +min:+, +max:+ from LengthValidator's Hash message). # Callers must not pass unintended keyword arguments — any extra keyword # will silently become an I18n interpolation variable. def translate(key, default: MISSING, scope: 'grape.errors.messages', locale: nil, **) i18n_opts = { default:, scope:, ** } i18n_opts[:locale] = locale if locale message = ::I18n.translate(key, **i18n_opts) return message unless message.equal?(MISSING) effective_default = default.equal?(MISSING) ? [*Array(scope), key].join('.') : default return effective_default if fallback_locale?(locale) || fallback_locale_unavailable? ::I18n.translate(key, default: effective_default, scope:, locale: FALLBACK_LOCALE, **) end def fallback_locale?(locale) (locale || ::I18n.locale) == FALLBACK_LOCALE end def fallback_locale_unavailable? ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE) end end end end ================================================ FILE: lib/grape/validations/attributes_iterator.rb ================================================ # frozen_string_literal: true module Grape module Validations class AttributesIterator include Enumerable attr_reader :scope def initialize(attrs, scope, params) @attrs = attrs @scope = scope @original_params = scope.params(params) @params = Array.wrap(@original_params) end def each(&) do_each(@params, &) # because we need recursion for nested arrays end private def do_each(params_to_process, parent_indices = [], &block) params_to_process.each_with_index do |resource_params, index| # when we get arrays of arrays it means that target element located inside array # we need this because we want to know parent arrays indices if resource_params.is_a?(Array) do_each(resource_params, [index] + parent_indices, &block) next end if @scope.type == Array next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array store_indices(@scope, index, parent_indices) elsif @original_params.is_a?(Array) # Lateral scope (no @element) whose params resolved to an array — # delegate index tracking to the nearest array-typed ancestor so # that full_name produces the correct bracketed index. target = @scope.nearest_array_ancestor store_indices(target, index, parent_indices) if target end yield_attributes(resource_params, &block) end end def store_indices(target_scope, index, parent_indices) # No tracker means we're outside a ParamScopeTracker.track block (e.g. # a unit test that invokes a validator directly). Index tracking is # skipped — full_name will produce bracket-less names — but validation # continues rather than crashing. tracker = ParamScopeTracker.current or return parent_scope = target_scope.parent parent_indices.each do |parent_index| break unless parent_scope tracker.store_index(parent_scope, parent_index) parent_scope = parent_scope.parent end tracker.store_index(target_scope, index) end def yield_attributes(_resource_params) raise NotImplementedError end # This is a special case so that we can ignore trees where option # values are missing lower down. Unfortunately we can't remove this # at the parameter parsing stage as they are required to ensure # the correct indexing is maintained def skip?(val) val == Grape::DSL::Parameters::EmptyOptionalValue end end end end ================================================ FILE: lib/grape/validations/contract_scope.rb ================================================ # frozen_string_literal: true module Grape module Validations class ContractScope # Declare the contract to be used for the endpoint's parameters. # @param api [API] the API endpoint to modify. # @param contract the contract or schema to be used for validation. Optional. # @yield a block yielding a new schema class. Optional. def initialize(api, contract = nil, &block) # When block is passed, the first arg is either schema or nil. contract = Dry::Schema.Params(parent: contract, &block) if block if contract.respond_to?(:schema) # It's a Dry::Validation::Contract, then. contract = contract.new key_map = contract.schema.key_map else # Dry::Schema::Processor, hopefully. key_map = contract.key_map end api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map validator_options = { validator_class: Grape::Validations.require_validator(:contract_scope), opts: { schema: contract, fail_fast: false } } api.inheritable_setting.namespace_stackable[:validations] = validator_options end end end end ================================================ FILE: lib/grape/validations/multiple_attributes_iterator.rb ================================================ # frozen_string_literal: true module Grape module Validations class MultipleAttributesIterator < AttributesIterator private def yield_attributes(resource_params) yield resource_params unless skip?(resource_params) end end end end ================================================ FILE: lib/grape/validations/param_scope_tracker.rb ================================================ # frozen_string_literal: true module Grape module Validations # Holds per-request mutable state that must not live on shared ParamsScope # instances. Both trackers are identity-keyed hashes so that ParamsScope # objects can serve as keys without relying on value equality. # # Lifecycle is managed by Endpoint#run_validators via +.track+. # Use +.current+ to access the instance for the running request. class ParamScopeTracker # Fiber-local key used to store the current tracker. # Fiber[] (Ruby 3.0+) is used instead of Thread.current[] so that # fiber-based servers (e.g. Falcon with async) isolate each request's # tracker within its own fiber rather than sharing state across all # fibers running on the same thread. FIBER_KEY = :grape_param_scope_tracker EMPTY_PARAMS = [].freeze def self.track previous = Fiber[FIBER_KEY] Fiber[FIBER_KEY] = new yield ensure Fiber[FIBER_KEY] = previous end def self.current Fiber[FIBER_KEY] end def initialize @index_tracker = {}.compare_by_identity @qualifying_params_tracker = {}.compare_by_identity end def store_index(scope, index) @index_tracker.store(scope, index) end def index_for(scope) @index_tracker[scope] end # Returns qualifying params for +scope+, or EMPTY_PARAMS if none were stored. # Note: an explicitly stored empty array and "never stored" are treated identically # by callers (both yield a blank result that falls through to the parent params). def qualifying_params(scope) @qualifying_params_tracker.fetch(scope, EMPTY_PARAMS) end def store_qualifying_params(scope, params) @qualifying_params_tracker.store(scope, params) end end end end ================================================ FILE: lib/grape/validations/params_documentation.rb ================================================ # frozen_string_literal: true module Grape module Validations # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an # internal API), the class only cleans up attributes to avoid junk in RAM. module ParamsDocumentation def document_params(attrs, validations, type = nil, values = nil, except_values = nil) return validations.except!(:desc, :description, :documentation) if @api.inheritable_setting.namespace_inheritable[:do_not_document] documented_attrs = attrs.each_with_object({}) do |name, memo| memo[full_name(name)] = extract_details(validations, type, values, except_values) end @api.inheritable_setting.namespace_stackable[:params] = documented_attrs end private def extract_details(validations, type, values, except_values) {}.tap do |details| details[:required] = validations.key?(:presence) details[:type] = TypeCache[type] if type details[:values] = values if values details[:except_values] = except_values if except_values details[:default] = validations[:default] if validations.key?(:default) if validations.key?(:length) details[:min_length] = validations[:length][:min] if validations[:length].key?(:min) details[:max_length] = validations[:length][:max] if validations[:length].key?(:max) end desc = validations.delete(:desc) || validations.delete(:description) details[:desc] = desc if desc documentation = validations.delete(:documentation) details[:documentation] = documentation if documentation end end class TypeCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, type| h[type] = type.to_s end end end end end end ================================================ FILE: lib/grape/validations/params_scope.rb ================================================ # frozen_string_literal: true module Grape module Validations class ParamsScope attr_accessor :element, :parent attr_reader :type, :nearest_array_ancestor def qualifying_params ParamScopeTracker.current&.qualifying_params(self) end include Grape::DSL::Parameters include Grape::Validations::ParamsDocumentation # There are a number of documentation options on entities that don't have # corresponding validators. Since there is nowhere that enumerates them all, # we maintain a list of them here and skip looking up validators for them. RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze SPECIAL_JSON = [JSON, Array[JSON]].freeze class Attr attr_accessor :key, :scope # Open up a new ParamsScope::Attr # @param key [Hash, Symbol] key of attr # @param scope [Grape::Validations::ParamsScope] scope of attr def initialize(key, scope) @key = key @scope = scope end # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr def self.attrs_keys(declared_params) declared_params.map do |declared_param_attr| attr_key(declared_param_attr) end end def self.attr_key(declared_param_attr) return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self) if declared_param_attr.is_a?(Hash) declared_param_attr.transform_values { |value| attrs_keys(value) } else declared_param_attr end end end # Open up a new ParamsScope, allowing parameter definitions per # Grape::DSL::Params. # @param api [API] the API endpoint to modify # @param element [Symbol] the element that contains this scope; for # this to be relevant, parent must be set # @param element_renamed [Symbol, nil] whenever this scope should # be renamed and to what, given +nil+ no renaming is done # @param parent [ParamsScope] the scope containing this scope # @param optional [Boolean] whether or not this scope needs to have # any parameters set or not # @param type [Class] a type meant to govern this scope (deprecated) # @param type [Hash] group options for this scope # @param dependent_on [Symbol] if present, this scope should only # validate if this param is present in the parent scope # @yield the instance context, open for parameter definitions def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block) @element = element @element_renamed = element_renamed @parent = parent @api = api @optional = optional @type = type @group = group @dependent_on = dependent_on # Must be an ivar: push_declared_params is dispatched on self during # instance_eval, so local variables from initialize are unreachable. # configure_declared_params consumes it and clears @declared_params to nil. @declared_params = [] instance_eval(&block) if block configure_declared_params @nearest_array_ancestor = find_nearest_array_ancestor end def configuration (@api.configuration.respond_to?(:evaluate) && @api.configuration.evaluate) || @api.configuration end # @return [Boolean] whether or not this entire scope needs to be # validated def should_validate?(parameters) scoped_params = params(parameters) return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params)) return false unless meets_dependency?(scoped_params, parameters) return true if parent.nil? parent.should_validate?(parameters) end def meets_dependency?(params, request_params) return true unless @dependent_on return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) if params.is_a?(Array) filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) } ParamScopeTracker.current&.store_qualifying_params(self, filtered) return filtered.present? end meets_hash_dependency?(params) end def attr_meets_dependency?(params) return true unless @dependent_on return false if @parent.present? && !@parent.attr_meets_dependency?(params) meets_hash_dependency?(params) end def meets_hash_dependency?(params) # params might be anything what looks like a hash, so it must implement a `key?` method return false unless params.respond_to?(:key?) @dependent_on.each do |dependency| if dependency.is_a?(Hash) dependency_key = dependency.keys[0] proc = dependency.values[0] return false unless proc.call(params[dependency_key]) elsif params[dependency].blank? return false end end true end # @return [String] the proper attribute name, with nesting considered. def full_name(name, index: nil) tracker = ParamScopeTracker.current if nested? # Find our containing element's name, and append ours. resolved_index = index || tracker&.index_for(self) "#{@parent.full_name(@element)}#{brackets(resolved_index)}#{brackets(name)}" elsif lateral? # Find the name of the element as if it was at the same nesting level # as our parent. We need to forward our index upward to achieve this. @parent.full_name(name, index: tracker&.index_for(self)) else # We must be the root scope, so no prefix needed. name.to_s end end def brackets(val) "[#{val}]" if val end # @return [Boolean] whether or not this scope is the root-level scope def root? !@parent end # A nested scope is contained in one of its parent's elements. # @return [Boolean] whether or not this scope is nested def nested? @parent && @element end # A lateral scope is subordinate to its parent, but its keys are at the # same level as its parent and thus is not contained within an element. # @return [Boolean] whether or not this scope is lateral def lateral? @parent && !@element end # @return [Boolean] whether or not this scope needs to be present, or can # be blank def required? !@optional end protected # Adds a parameter declaration to our list of validations. # @param attrs [Array] (see Grape::DSL::Parameters#requires) def push_declared_params(attrs, **opts) opts[:declared_params_scope] = self unless opts.key?(:declared_params_scope) return @parent.push_declared_params(attrs, **opts) if lateral? push_renamed_param(full_path + [attrs.first], opts[:as]) if opts[:as] @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) }) end # Get the full path of the parameter scope in the hierarchy. # # @return [Array] the nesting/path of the current parameter scope def full_path if nested? (@parent.full_path + [@element]) elsif lateral? @parent.full_path else [] end end private # Add a new parameter which should be renamed when using the +#declared+ # method. # # @param path [Array] the full path of the parameter # (including the parameter name as last array element) # @param new_name [String, Symbol] the new name of the parameter (the # renamed name, with the +as: ...+ semantic) def push_renamed_param(path, new_name) api_route_setting = @api.inheritable_setting.route base = api_route_setting[:renamed_params] || {} base[Array(path).map(&:to_s)] = new_name.to_s api_route_setting[:renamed_params] = base end def require_required_and_optional_fields(context, using:, except: nil) except_fields = Array.wrap(except) using_fields = using.keys.delete_if { |f| except_fields.include?(f) } if context == :all optional_fields = except_fields required_fields = using_fields else # context == :none required_fields = except_fields optional_fields = using_fields end required_fields.each do |field| field_opts = using[field] raise ArgumentError, "required field not exist: #{field}" unless field_opts requires(field, **field_opts) end optional_fields.each do |field| field_opts = using[field] optional(field, **field_opts) if field_opts end end def require_optional_fields(context, using:, except: nil) optional_fields = using.keys unless context == :all except_fields = Array.wrap(except) optional_fields.delete_if { |f| except_fields.include?(f) } end optional_fields.each do |field| field_opts = using[field] optional(field, **field_opts) if field_opts end end def validate_attributes(attrs, **opts, &block) opts[:type] ||= Array if block validates(attrs, opts) end # Returns a new parameter scope, subordinate to the current one and nested # under the given element. # @param element [Symbol] the parameter name under which this scope is nested # @param type [Class] the type governing this scope # @param as [Symbol, nil] optional renamed name for the element # @param optional [Boolean] whether the parameter this scope is nested under # is optional or not (and hence, whether this block's params will be). # @yield parameter scope def new_scope(element, type:, as:, optional: false, &) # if required params are grouped and no type or unsupported type is provided, raise an error if element && !optional raise Grape::Exceptions::MissingGroupType if type.nil? raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type) end self.class.new( api: @api, element: element, element_renamed: as, parent: self, optional: optional, type: type || Array, group: @group, & ) end # Returns a new parameter scope, not nested under any current-level param # but instead at the same level as the current scope. # @param dependent_on [Symbol] if given, specifies that this scope should # only validate if this parameter from the above scope is present # @yield parameter scope def new_lateral_scope(dependent_on:, &) self.class.new( api: @api, parent: self, optional: @optional, type: type == Array ? Array : Hash, dependent_on:, & ) end # Returns a new parameter scope, subordinate to the current one, sharing # the given group options with all parameters defined within. # @param group [Hash] common options to merge into each parameter in the scope # @yield parameter scope def new_group_scope(group, &) self.class.new(api: @api, parent: self, group: group, &) end # Pushes declared params to parent or settings def configure_declared_params push_renamed_param(full_path, @element_renamed) if @element_renamed if nested? @parent.push_declared_params [element => @declared_params] else @api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params end # params were stored in settings, it can be cleaned from the params scope @declared_params = nil end def find_nearest_array_ancestor scope = @parent scope = scope.parent while scope && scope.type != Array scope end def validates(attrs, validations) coerce_type = infer_coercion(validations) required = validations.key?(:presence) default = validations[:default] values = validations[:values].is_a?(Hash) ? validations.dig(:values, :value) : validations[:values] except_values = validations[:except_values].is_a?(Hash) ? validations.dig(:except_values, :value) : validations[:except_values] # NB. values and excepts should be nil, Proc, Array, or Range. # Specifically, values should NOT be a Hash # use values or excepts to guess coerce type when stated type is Array coerce_type = guess_coerce_type(coerce_type, values, except_values) # default value should be present in values array, if both exist and are not procs check_incompatible_option_values(default, values, except_values) # type should be compatible with values array, if both exist validate_value_coercion(coerce_type, values, except_values) document_params attrs, validations, coerce_type, values, except_values opts = derive_validator_options(validations) # Validate for presence before any other validators validates_presence(validations, attrs, opts) # Before we run the rest of the validators, let's handle # whatever coercion so that we are working with correctly # type casted values coerce_type validations, attrs, required, opts validations.each do |type, options| # Don't try to look up validators for documentation params that don't have one. next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type) validate(type, options, attrs, required, opts) end end # Validate and comprehend the +:type+, +:types+, and +:coerce_with+ # options that have been supplied to the parameter declaration. # The +:type+ and +:types+ options will be removed from the # validations list, replaced appropriately with +:coerce+ and # +:coerce_with+ options that will later be passed to # {Validators::CoerceValidator}. The type that is returned may be # used for documentation and further validation of parameter # options. # # @param validations [Hash] list of validations supplied to the # parameter declaration # @return [class-like] type to which the parameter will be coerced # @raise [ArgumentError] if the given type options are invalid def infer_coercion(validations) raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types) validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type) validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type) validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types) validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types) validations.delete(:types) if validations.key?(:types) coerce_type = validations[:coerce] # Special case - when the argument is a single type that is a # variant-type collection. if Types.multiple?(coerce_type) && validations.key?(:type) validations[:coerce] = Types::VariantCollectionCoercer.new( coerce_type, validations.delete(:coerce_with) ) end validations.delete(:type) coerce_type end # Enforce correct usage of :coerce_with parameter. # We do not allow coercion without a type, nor with # +JSON+ as a type since this defines its own coercion # method. def check_coerce_with(validations) return unless validations.key?(:coerce_with) # type must be supplied for coerce_with.. raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce) # but not special JSON types, which # already imply coercion method return unless SPECIAL_JSON.include?(validations[:coerce]) raise ArgumentError, 'coerce_with disallowed for type: JSON' end # Add type coercion validation to this scope, # if any has been specified. # This validation has special handling since it is # composited from more than one +requires+/+optional+ # parameter, and needs to be run before most other # validations. def coerce_type(validations, attrs, required, opts) check_coerce_with(validations) return unless validations.key?(:coerce) coerce_options = { type: validations[:coerce], method: validations[:coerce_with], message: validations[:coerce_message] } validate('coerce', coerce_options, attrs, required, opts) validations.delete(:coerce_with) validations.delete(:coerce) validations.delete(:coerce_message) end def guess_coerce_type(coerce_type, *values_list) return coerce_type unless coerce_type == Array values_list.each do |values| next if !values || values.is_a?(Proc) return values.first.class if values.is_a?(Range) || !values.empty? end coerce_type end def check_incompatible_option_values(default, values, except_values) return unless default && !default.is_a?(Proc) raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) } return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) } raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) end def validate(type, options, attrs, required, opts) validator_options = { attributes: attrs, options: options, required: required, params_scope: self, opts: opts, validator_class: Validations.require_validator(type) } @api.inheritable_setting.namespace_stackable[:validations] = validator_options end def validate_value_coercion(coerce_type, *values_list) return unless coerce_type coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable) values_list.each do |values| next if !values || values.is_a?(Proc) value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type) end end def extract_message_option(attrs) return nil unless attrs.is_a?(Array) opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end def options_key?(type, key, validations) validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil? end def all_element_blank?(scoped_params) scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?) end # Validators don't have access to each other and they don't need, however, # some validators might influence others, so their options should be shared def derive_validator_options(validations) allow_blank = validations[:allow_blank] { allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank, fail_fast: validations.delete(:fail_fast) || false } end def validates_presence(validations, attrs, opts) return unless validations.key?(:presence) && validations[:presence] validate('presence', validations.delete(:presence), attrs, true, opts) validations.delete(:message) if validations.key?(:message) end end end end ================================================ FILE: lib/grape/validations/single_attribute_iterator.rb ================================================ # frozen_string_literal: true module Grape module Validations class SingleAttributeIterator < AttributesIterator private def yield_attributes(val) return if skip?(val) @attrs.each do |attr_name| yield val, attr_name, empty?(val) end end # Primitives like Integers and Booleans don't respond to +empty?+. # It could be possible to use +blank?+ instead, but # # false.blank? # => true def empty?(val) val.respond_to?(:empty?) ? val.empty? : val.nil? end end end end ================================================ FILE: lib/grape/validations/types/array_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Coerces elements in an array. It might be an array of strings or integers or # an array of arrays of integers. # # It could've been possible to use an +of+ # method (https://dry-rb.org/gems/dry-types/main/array-with-member/) # provided by dry-types. Unfortunately, it doesn't work for Grape because of # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer` # maintains Virtus behavior in coercing. class ArrayCoercer < DryTypeCoercer def initialize(type, strict = false) super @coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array @subtype = type.first end def call(_val) collection = super return collection if collection.is_a?(InvalidValue) coerce_elements collection end protected attr_reader :subtype def coerce_elements(collection) return if collection.nil? collection.each_with_index do |elem, index| return InvalidValue.new if reject?(elem) coerced_elem = elem_coercer.call(elem) return coerced_elem if coerced_elem.is_a?(InvalidValue) collection[index] = coerced_elem end collection end # This method maintains logic which was defined by Virtus for arrays. # Virtus doesn't allow nil in arrays. def reject?(val) val.nil? end def elem_coercer @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict) end end end end end ================================================ FILE: lib/grape/validations/types/custom_type_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # This class will detect type classes that implement # a class-level +parse+ method. The method should accept one # +String+ argument and should return the value coerced to # the appropriate type. The method may raise an exception if # there are any problems parsing the string. # # Alternately an optional +method+ may be supplied (see the # +coerce_with+ option of {Grape::Dsl::Parameters#requires}). # This may be any class or object implementing +parse+ or +call+, # with the same contract as described above. # # Type Checking # ------------- # # Calls to +coerced?+ will consult this class to check # that the coerced value produced above is in fact of the # expected type. By default this class performs a basic check # against the type supplied, but this behaviour will be # overridden if the class implements a class-level # +coerced?+ or +parsed?+ method. This method # will receive a single parameter that is the coerced value # and should return +true+ if the value meets type expectations. # Arbitrary assertions may be made here but the grape validation # system should be preferred. # # Alternately a proc or other object responding to +call+ may be # supplied in place of a type. This should implement the same # contract as +coerced?+, and must be supplied with a coercion # +method+. class CustomTypeCoercer # A new coercer for the given type specification # and coercion method. # # @param type [Class,#coerced?,#parsed?,#call?] # specifier for the target type. See class docs. # @param method [#parse,#call] # optional coercion method. See class docs. def initialize(type, method = nil) coercion_method = infer_coercion_method type, method @method = enforce_symbolized_keys type, coercion_method @type_check = infer_type_check(type) end # Coerces the given value. # # @param value [String] value to be coerced, in grape # this should always be a string. # @return [Object] the coerced result def call(val) coerced_val = @method.call(val) return coerced_val if coerced_val.is_a?(InvalidValue) return InvalidValue.new unless coerced?(coerced_val) coerced_val end def coerced?(val) val.nil? || @type_check.call(val) end private # Determine the coercion method we're expected to use # based on the parameters given. # # @param type see #new # @param method see #new # @return [#call] coercion method def infer_coercion_method(type, method) if method if method.respond_to? :parse method.method :parse else method end else # Try to use parse() declared on the target type. # This may raise an exception, but we are out of ideas anyway. type.method :parse end end # Determine how the type validity of a coerced # value should be decided. # # @param type see #new # @return [#call] a procedure which accepts a single parameter # and returns +true+ if the passed object is of the correct type. def infer_type_check(type) # First check for special class methods if type.respond_to? :coerced? type.method :coerced? elsif type.respond_to? :parsed? type.method :parsed? elsif type.respond_to? :call # Arbitrary proc passed for type validation. # Note that this will fail unless a method is also # passed, or if the type also implements a parse() method. type elsif type.is_a?(Enumerable) lambda do |value| value.is_a?(Enumerable) && value.all? do |val| recursive_type_check(type.first, val) end end else # By default, do a simple type check ->(value) { value.is_a? type } end end def recursive_type_check(type, value) if type.is_a?(Enumerable) && value.is_a?(Enumerable) value.all? { |val| recursive_type_check(type.first, val) } else !type.is_a?(Enumerable) && value.is_a?(type) end end # Enforce symbolized keys for complex types # by wrapping the coercion method such that # any Hash objects in the immediate heirarchy # have their keys recursively symbolized. # This helps common libs such as JSON to work easily. # # @param type see #new # @param method see #infer_coercion_method # @return [#call] +method+ wrapped in an additional # key-conversion step, or just returns +method+ # itself if no conversion is deemed to be # necessary. def enforce_symbolized_keys(type, method) # Collections have all values processed individually if [Array, Set].include?(type) lambda do |val| method.call(val).tap do |new_val| new_val.map do |item| item.is_a?(Hash) ? item.deep_symbolize_keys : item end end end # Hash objects are processed directly elsif type == Hash lambda do |val| method.call(val).deep_symbolize_keys end # Simple types are not processed. # This includes Array types. else method end end end end end end ================================================ FILE: lib/grape/validations/types/custom_type_collection_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # See {CustomTypeCoercer} for details on types # that will be supported by this by this coercer. # This coercer works in the same way as +CustomTypeCoercer+ # except that it expects to receive an array of strings to # coerce and will return an array (or optionally, a set) # of coerced values. # # +CustomTypeCoercer+ is already capable of providing type # checking for arrays where an independent coercion method # is supplied. As such, +CustomTypeCollectionCoercer+ does # not allow for such a method to be supplied independently # of the type. class CustomTypeCollectionCoercer < CustomTypeCoercer # A new coercer for collections of the given type. # # @param type [Class,#parse] # type to which items in the array should be coerced. # Must implement a +parse+ method which accepts a string, # and for the purposes of type-checking it may either be # a class, or it may implement a +coerced?+, +parsed?+ or # +call+ method (in that order of precedence) which # accepts a single argument and returns true if the given # array item has been coerced correctly. # @param set [Boolean] # when true, a +Set+ will be returned by {#call} instead # of an +Array+ and duplicate items will be discarded. def initialize(type, set = false) super(type) @set = set end # Coerces the given value. # # @param value [Array] an array of values to be coerced # @return [Array,Set] the coerced result. May be an +Array+ or a # +Set+ depending on the setting given to the constructor def call(value) coerced = value.map do |item| coerced_item = super(item) return coerced_item if coerced_item.is_a?(InvalidValue) coerced_item end @set ? Set.new(coerced) : coerced end end end end end ================================================ FILE: lib/grape/validations/types/dry_type_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # A base class for classes which must identify a coercer to be used. # If the +strict+ argument is true, it won't coerce the given value # but check its type. More information there # https://dry-rb.org/gems/dry-types/main/built-in-types/ class DryTypeCoercer class << self # Returns a collection coercer which corresponds to a given type. # Example: # # collection_coercer_for(Array) # #=> Grape::Validations::Types::ArrayCoercer def collection_coercer_for(type) case type when Array ArrayCoercer when Set SetCoercer else raise ArgumentError, "Unknown type: #{type}" end end # Returns an instance of a coercer for a given type def coercer_instance_for(type, strict = false) klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type) klass.new(type, strict) end end def initialize(type, strict = false) @type = type @strict = strict @cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache end # Coerces the given value to a type which was specified during # initialization as a type argument. # # @param val [Object] def call(val) return if val.nil? @coercer[val] rescue Dry::Types::CoercionError InvalidValue.new end protected attr_reader :type, :strict, :cache_coercer end end end end ================================================ FILE: lib/grape/validations/types/file.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Implementation for parameters that are multipart file objects. # Actual handling of these objects is provided by +Rack::Request+; # this class is here only to assert that rack's handling has succeeded. class File class << self def parse(input) return if input.nil? return InvalidValue.new unless parsed?(input) # Processing of multipart file objects # is already taken care of by Rack::Request. # Nothing to do here. input end def parsed?(value) # Rack::Request creates a Hash with filename, # content type and an IO object. Do a bit of basic # duck-typing. value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile) end end end end end end ================================================ FILE: lib/grape/validations/types/invalid_value.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Instances of this class may be used as tokens to denote that a parameter value could not be # coerced. The given message will be used as a validation error. class InvalidValue attr_reader :message def initialize(message = nil) @message = message end end end end end ================================================ FILE: lib/grape/validations/types/json.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Handles coercion and type checking for parameters that are complex # types given as JSON-encoded strings. It accepts both JSON objects # and arrays of objects, and will coerce the input to a +Hash+ # or +Array+ object respectively. In either case the Grape # validation system will apply nested validation rules to # all returned objects. class Json class << self # Coerce the input into a JSON-like data structure. # # @param input [String] a JSON-encoded parameter value # @return [Hash,Array,nil] def parse(input) return input if parsed?(input) # Allow nulls and blank strings return if input.nil? || input.match?(/^\s*$/) JSON.parse(input, symbolize_names: true) end # Checks that the input was parsed successfully # and isn't something odd such as an array of primitives. # # @param value [Object] result of {#parse} # @return [true,false] def parsed?(value) value.is_a?(::Hash) || coerced_collection?(value) end protected # Is the value an array of JSON-like objects? # # @param value [Object] result of {#parse} # @return [true,false] def coerced_collection?(value) value.is_a?(::Array) && value.all?(::Hash) end end end # Specialization of the {Json} attribute that is guaranteed # to return an array of objects. Accepts both JSON-encoded # objects and arrays of objects, but wraps single objects # in an Array. class JsonArray < Json class << self # See {Json#parse}. Wraps single objects in an array. # # @param input [String] JSON-encoded parameter value # @return [Array] def parse(input) json = super Array.wrap(json) unless json.nil? end # See {Json#coerced_collection?} def parsed?(value) coerced_collection? value end end end end end end ================================================ FILE: lib/grape/validations/types/multiple_type_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # This class is intended for use with Grape endpoint parameters that # have been declared to be of variant-type using the +:types+ option. # +MultipleTypeCoercer+ will build a coercer for each type declared # in the array passed to +:types+ using {Types.build_coercer}. It will # apply these coercers to parameter values in the order given to # +:types+, and will return the value returned by the first coercer # to successfully coerce the parameter value. Therefore if +String+ is # an allowed type it should be declared last, since it will always # successfully "coerce" the value. class MultipleTypeCoercer # Construct a new coercer that will attempt to coerce # values to the given list of types in the given order. # # @param types [Array] list of allowed types # @param method [#call,#parse] method by which values should be # coerced. See class docs for default behaviour. def initialize(types, method = nil) @method = method.respond_to?(:parse) ? method.method(:parse) : method @type_coercers = types.map do |type| if Types.multiple? type VariantCollectionCoercer.new type, @method else Types.build_coercer type, strict: !@method.nil? end end end # Coerces the given value. # # @param val [String] value to be coerced, in grape # this should always be a string. # @return [Object,InvalidValue] the coerced result, or an instance # of {InvalidValue} if the value could not be coerced. def call(val) # once the value is coerced by the custom method, its type should be checked val = @method.call(val) if @method coerced_val = InvalidValue.new @type_coercers.each do |coercer| coerced_val = coercer.call(val) return coerced_val unless coerced_val.is_a?(InvalidValue) end coerced_val end end end end end ================================================ FILE: lib/grape/validations/types/primitive_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Coerces the given value to a type defined via a +type+ argument during # initialization. When +strict+ is true, it doesn't coerce a value but check # that it has the proper type. class PrimitiveCoercer < DryTypeCoercer def initialize(type, strict = false) super @coercer = cache_coercer[type] end def call(val) return InvalidValue.new if reject?(val) return nil if val.nil? || treat_as_nil?(val) super end protected attr_reader :type # This method maintains logic which was defined by Virtus. For example, # dry-types is ok to convert an array or a hash to a string, it is supported, # but Virtus wouldn't accept it. So, this method only exists to not introduce # breaking changes. def reject?(val) (val.is_a?(Array) && type == String) || (val.is_a?(String) && type == Hash) || (val.is_a?(Hash) && type == String) end # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as # absence of a value and coerces it into nil. See a discussion there # https://github.com/ruby-grape/grape/pull/2045 def treat_as_nil?(val) val == '' && type != String end end end end end ================================================ FILE: lib/grape/validations/types/set_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # Takes the given array and converts it to a set. Every element of the set # is also coerced. class SetCoercer < ArrayCoercer def initialize(type, strict = false) super @coercer = nil end def call(value) return InvalidValue.new unless value.is_a?(Array) coerce_elements(value) end protected def coerce_elements(collection) collection.each_with_object(Set.new) do |elem, memo| coerced_elem = elem_coercer.call(elem) return coerced_elem if coerced_elem.is_a?(InvalidValue) memo.add(coerced_elem) end end end end end end ================================================ FILE: lib/grape/validations/types/variant_collection_coercer.rb ================================================ # frozen_string_literal: true module Grape module Validations module Types # This class wraps {MultipleTypeCoercer}, for use with collections # that allow members of more than one type. class VariantCollectionCoercer # Construct a new coercer that will attempt to coerce # a list of values such that all members are of one of # the given types. The container may also optionally be # coerced to a +Set+. An arbitrary coercion +method+ may # be supplied, which will be passed the entire collection # as a parameter and should return a new collection, or # may return the same one if no coercion was required. # # @param types [Array,Set] list of allowed types, # also specifying the container type # @param method [#call,#parse] method by which values should be coerced def initialize(types, method = nil) @types = types @method = method.respond_to?(:parse) ? method.method(:parse) : method # If we have a coercion method, pass it in here to save # building another one, even though we call it directly. @member_coercer = MultipleTypeCoercer.new types, method end # Coerce the given value. # # @param value [Array] collection of values to be coerced # @return [Array,Set,InvalidValue] # the coerced result, or an instance # of {InvalidValue} if the value could not be coerced. def call(value) return unless value.is_a? Array value = if @method @method.call(value) else value.map { |v| @member_coercer.call(v) } end return Set.new value if @types.is_a? Set value end end end end end ================================================ FILE: lib/grape/validations/types.rb ================================================ # frozen_string_literal: true module Grape module Validations # Module for code related to grape's system for # coercion and type validation of incoming request # parameters. # # Grape uses a number of tests and assertions to # work out exactly how a parameter should be handled, # based on the +type+ and +coerce_with+ options that # may be supplied to {Grape::Dsl::Parameters#requires} # and {Grape::Dsl::Parameters#optional}. The main # entry point for this process is {Types.build_coercer}. module Types module_function PRIMITIVES = [ # Numerical Integer, Float, BigDecimal, Numeric, # Date/time Date, DateTime, Time, # Misc Grape::API::Boolean, String, Symbol, TrueClass, FalseClass ].freeze # Types representing data structures. STRUCTURES = [Hash, Array, Set].freeze SPECIAL = { ::JSON => Json, Array[JSON] => JsonArray, ::File => File, Rack::Multipart::UploadedFile => File }.freeze GROUPS = [Array, Hash, JSON, Array[JSON]].freeze # Is the given class a primitive type as recognized by Grape? # # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # type for a single value def primitive?(type) PRIMITIVES.include?(type) end # Is the given class a standard data structure (collection or map) # as recognized by Grape? # # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # data structure type def structure?(type) STRUCTURES.include?(type) end # Is the declared type in fact an array of multiple allowed types? # For example the declaration +types: [Integer,String]+ will attempt # first to coerce given values to integer, but will also accept any # other string. # # @param type [Array,Set] type (or type list!) to check # @return [Boolean] +true+ if the given value will be treated as # a list of types. def multiple?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1 end # Does Grape provide special coercion and validation # routines for the given class? This does not include # automatic handling for primitives, structures and # otherwise recognized types. See {Types::SPECIAL}. # # @param type [Class] type to check # @return [Boolean] +true+ if special routines are available def special?(type) SPECIAL.key? type end # Is the declared type a supported group type? # Currently supported group types are Array, Hash, JSON, and Array[JSON] # # @param type [Array,Class] type to check # @return [Boolean] +true+ if the type is a supported group type def group?(type) GROUPS.include? type end # A valid custom type must implement a class-level `parse` method, taking # one String argument and returning the parsed value in its correct type. # # @param type [Class] type to check # @return [Boolean] whether or not the type can be used as a custom type def custom?(type) !primitive?(type) && !structure?(type) && !multiple?(type) && type.respond_to?(:parse) && type.method(:parse).arity == 1 end # Is the declared type an +Array+ or +Set+ of a {#custom?} type? # # @param type [Array,Class] type to check # @return [Boolean] true if +type+ is a collection of a type that implements # its own +#parse+ method. def collection_of_custom?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.length == 1 && (custom?(type.first) || special?(type.first)) end def map_special(type) SPECIAL.fetch(type, type) end # Chooses the best coercer for the given type. For example, if the type # is Integer, it will return a coercer which will be able to coerce a value # to the integer. # # There are a few very special coercers which might be returned. # # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when # the given type implies values in an array with different types. # For example, +[Integer, String]+ allows integer and string values in # an array. # # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when # a method is specified by a user with +coerce_with+ option or the user # specifies a custom type which implements requirments of # +Grape::Types::CustomTypeCoercer+. # # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the # previous one, but it expects an array or set of values having a custom # type implemented by the user. # # There is also a group of custom types implemented by Grape, check # +Grape::Validations::Types::SPECIAL+ to get the full list. # # @param type [Class] the type to which input strings # should be coerced # @param method [Class,#call] the coercion method to use # @return [Object] object to be used # for coercion and type validation def build_coercer(type, method: nil, strict: false) # no cache since unique return create_coercer_instance(type, method, strict) if method.respond_to?(:call) CoercerCache[[type, method, strict]] end def create_coercer_instance(type, method, strict) # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!! type = Types.map_special(type) # Use a special coercer for multiply-typed parameters. if Types.multiple?(type) MultipleTypeCoercer.new(type, method) # Use a special coercer for custom types and coercion methods. elsif method || Types.custom?(type) CustomTypeCoercer.new(type, method) # Special coercer for collections of types that implement a parse method. # CustomTypeCoercer (above) already handles such types when an explicit coercion # method is supplied. elsif Types.collection_of_custom?(type) Types::CustomTypeCollectionCoercer.new( Types.map_special(type.first), type.is_a?(Set) ) else DryTypeCoercer.coercer_instance_for(type, strict) end end class CoercerCache < Grape::Util::Cache def initialize super @cache = Hash.new do |h, (type, method, strict)| h[[type, method, strict]] = Grape::Validations::Types.create_coercer_instance(type, method, strict) end end end end end end ================================================ FILE: lib/grape/validations/validator_factory.rb ================================================ # frozen_string_literal: true module Grape module Validations class ValidatorFactory def self.create_validator(options) options[:validator_class].new(options[:attributes], options[:options], options[:required], options[:params_scope], options[:opts]) end end end end ================================================ FILE: lib/grape/validations/validators/all_or_none_of_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class AllOrNoneOfValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.empty? || keys.length == all_keys.length raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none)) end end end end end ================================================ FILE: lib/grape/validations/validators/allow_blank_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class AllowBlankValidator < Base def validate_param!(attr_name, params) return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash) value = params[attr_name] value = value.scrub if value.respond_to?(:valid_encoding?) && !value.valid_encoding? return if value == false || value.present? raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank)) end end end end end ================================================ FILE: lib/grape/validations/validators/as_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class AsValidator < Base # We use a validator for renaming parameters. This is just a marker for # the parameter scope to handle the renaming. No actual validation # happens here. def validate_param!(*); end end end end end ================================================ FILE: lib/grape/validations/validators/at_least_one_of_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class AtLeastOneOfValidator < MultipleParamsBase def validate_params!(params) return unless keys_in_common(params).empty? raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one)) end end end end end ================================================ FILE: lib/grape/validations/validators/base.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class Base include Grape::Util::Translation attr_reader :attrs # Creates a new Validator from options specified # by a +requires+ or +optional+ directive during # parameter definition. # @param attrs [Array] names of attributes to which the Validator applies # @param options [Object] implementation-dependent Validator options # @param required [Boolean] attribute(s) are required or optional # @param scope [ParamsScope] parent scope for this Validator # @param opts [Hash] additional validation options def initialize(attrs, options, required, scope, opts) @attrs = Array(attrs) @option = options @required = required @scope = scope @fail_fast = opts[:fail_fast] @allow_blank = opts[:allow_blank] end # Validates a given request. # @note Override #validate! unless you need to access the entire request. # @param request [Grape::Request] the request currently being handled # @raise [Grape::Exceptions::Validation] if validation failed # @return [void] def validate(request) return unless @scope.should_validate?(request.params) validate!(request.params) end # Validates a given parameter hash. # @note Override #validate if you need to access the entire request. # @param params [Hash] parameters to validate # @raise [Grape::Exceptions::Validation] if validation failed # @return [void] def validate!(params) attributes = SingleAttributeIterator.new(@attrs, @scope, params) # we collect errors inside array because # there may be more than one error per field array_errors = [] attributes.each do |val, attr_name, empty_val| next if !@scope.required? && empty_val next unless @scope.meets_dependency?(val, params) validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name)) rescue Grape::Exceptions::Validation => e array_errors << e end raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end def self.inherited(klass) super Validations.register(klass) end def message(default_key = nil) options = instance_variable_get(:@option) options_key?(:message) ? options[:message] : default_key end def options_key?(key, options = nil) options = instance_variable_get(:@option) if options.nil? options.respond_to?(:key?) && options.key?(key) && !options[key].nil? end def fail_fast? @fail_fast end end end end end ================================================ FILE: lib/grape/validations/validators/coerce_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class CoerceValidator < Base def initialize(attrs, options, required, scope, opts) super @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) type else Types.build_coercer(type, method: @option[:method]) end end def validate_param!(attr_name, params) raise validation_exception(attr_name) unless params.is_a? Hash new_value = coerce_value(params[attr_name]) raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value) # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash # which looses wrappers for hashes and arrays after reassigning values # # h = Hashie::Mash.new(list: [1, 2, 3, 4]) # => #> # list = h.list # h[:list] = list # h # => # return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value params[attr_name] = new_value end private # @!attribute [r] converter # Object that will be used for parameter coercion and type checking. # # See {Types.build_coercer} # # @return [Object] attr_reader :converter def valid_type?(val) !val.is_a?(Types::InvalidValue) end def coerce_value(val) converter.call(val) # Some custom types might fail, so it should be treated as an invalid value rescue StandardError Types::InvalidValue.new end # Type to which the parameter will be coerced. # # @return [Class] def type @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end def validation_exception(attr_name, custom_msg = nil) Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: custom_msg || message(:coerce) ) end end end end end ================================================ FILE: lib/grape/validations/validators/contract_scope_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class ContractScopeValidator < Base attr_reader :schema def initialize(_attrs, _options, _required, _scope, opts) super @schema = opts.fetch(:schema) end # Validates a given request. # @param request [Grape::Request] the request currently being handled # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed # @return [void] def validate(request) res = schema.call(request.params) if res.success? request.params.deep_merge!(res.to_h) return end raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages)) end private def build_errors_from_messages(messages) messages.map do |message| full_name = message.path.first.to_s full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1 Grape::Exceptions::Validation.new(params: [full_name], message: message.text) end end end end end end ================================================ FILE: lib/grape/validations/validators/default_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class DefaultValidator < Base def initialize(attrs, options, required, scope, opts = {}) @default = options super end def validate_param!(attr_name, params) params[attr_name] = if @default.is_a? Proc if @default.parameters.empty? @default.call else @default.call(params) end elsif @default.frozen? || !@default.duplicable? @default else @default.dup end end def validate!(params) attrs = SingleAttributeIterator.new(@attrs, @scope, params) attrs.each do |resource_params, attr_name| next unless @scope.meets_dependency?(resource_params, params) validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil? end end end end end end ================================================ FILE: lib/grape/validations/validators/exactly_one_of_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class ExactlyOneOfValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.length == 1 raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.empty? raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end end end end ================================================ FILE: lib/grape/validations/validators/except_values_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class ExceptValuesValidator < Base def initialize(attrs, options, required, scope, opts) @except = options.is_a?(Hash) ? options[:value] : options super end def validate_param!(attr_name, params) return unless params.respond_to?(:key?) && params.key?(attr_name) excepts = @except.is_a?(Proc) ? @except.call : @except return if excepts.nil? param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) } end end end end end ================================================ FILE: lib/grape/validations/validators/length_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class LengthValidator < Base def initialize(attrs, options, required, scope, opts) @min = options[:min] @max = options[:max] @is = options[:is] super raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?) raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?) raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max return if @is.nil? raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive? raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil? end def validate_param!(attr_name, params) param = params[attr_name] return unless param.respond_to?(:length) return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message) end def build_message if options_key?(:message) @option[:message] elsif @min && @max translate(:length, min: @min, max: @max) elsif @min translate(:length_min, min: @min) elsif @max translate(:length_max, max: @max) else translate(:length_is, is: @is) end end end end end end ================================================ FILE: lib/grape/validations/validators/multiple_params_base.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class MultipleParamsBase < Base def validate!(params) attributes = MultipleAttributesIterator.new(@attrs, @scope, params) array_errors = [] attributes.each do |resource_params| validate_params!(resource_params) rescue Grape::Exceptions::Validation => e array_errors << e end raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end private def keys_in_common(resource_params) return [] unless resource_params.is_a?(Hash) all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } end def all_keys @attrs.map { |attr| @scope.full_name(attr) } end end end end end ================================================ FILE: lib/grape/validations/validators/mutually_exclusive_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class MutuallyExclusiveValidator < MultipleParamsBase def validate_params!(params) keys = keys_in_common(params) return if keys.length <= 1 raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end end end end ================================================ FILE: lib/grape/validations/validators/presence_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class PresenceValidator < Base def validate_param!(attr_name, params) return if params.respond_to?(:key?) && params.key?(attr_name) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) end end end end end ================================================ FILE: lib/grape/validations/validators/regexp_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class RegexpValidator < Base def validate_param!(attr_name, params) return unless params.respond_to?(:key) && params.key?(attr_name) value = options_key?(:value) ? @option[:value] : @option return if Array.wrap(params[attr_name]).all? { |param| param.nil? || scrub(param.to_s).match?(value) } raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) end private def scrub(param) return param if param.valid_encoding? param.scrub end end end end end ================================================ FILE: lib/grape/validations/validators/same_as_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class SameAsValidator < Base def validate_param!(attr_name, params) confirmation = options_key?(:value) ? @option[:value] : @option return if params[attr_name] == params[confirmation] raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: build_message ) end private def build_message if options_key?(:message) @option[:message] else translate(:same_as, parameter: @option) end end end end end end ================================================ FILE: lib/grape/validations/validators/values_validator.rb ================================================ # frozen_string_literal: true module Grape module Validations module Validators class ValuesValidator < Base def initialize(attrs, options, required, scope, opts) @values = options.is_a?(Hash) ? options[:value] : options super end def validate_param!(attr_name, params) return unless params.is_a?(Hash) val = params[attr_name] return if val.nil? && !required_for_root_scope? val = val.scrub if val.respond_to?(:valid_encoding?) && !val.valid_encoding? # don't forget that +false.blank?+ is true return if val != false && val.blank? && @allow_blank return if check_values?(val, attr_name) raise Grape::Exceptions::Validation.new( params: [@scope.full_name(attr_name)], message: message(:values) ) end private def check_values?(val, attr_name) values = @values.is_a?(Proc) && @values.arity.zero? ? @values.call : @values return true if values.nil? param_array = val.nil? ? [nil] : Array.wrap(val) return param_array.all? { |param| values.include?(param) } unless values.is_a?(Proc) begin param_array.all? { |param| values.call(param) } rescue StandardError => e warn "Error '#{e}' raised while validating attribute '#{attr_name}'" false end end def required_for_root_scope? return false unless @required scope = @scope scope = scope.parent while scope.lateral? scope.root? end end end end end ================================================ FILE: lib/grape/validations.rb ================================================ # frozen_string_literal: true module Grape module Validations extend Grape::Util::Registry module_function def require_validator(short_name) raise Grape::Exceptions::UnknownValidator, short_name unless registry.key?(short_name) registry[short_name] end def build_short_name(klass) return if klass.name.blank? klass.name.demodulize.underscore.delete_suffix('_validator') end end end ================================================ FILE: lib/grape/version.rb ================================================ # frozen_string_literal: true module Grape # The current version of Grape. VERSION = '3.2.0' end ================================================ FILE: lib/grape/xml.rb ================================================ # frozen_string_literal: true module Grape if defined?(::MultiXml) Xml = ::MultiXml else Xml = ::ActiveSupport::XmlMini Xml::ParseError = StandardError end end ================================================ FILE: lib/grape.rb ================================================ # frozen_string_literal: true require 'logger' require 'active_support' require 'active_support/version' require 'active_support/isolated_execution_state' require 'active_support/core_ext/array/conversions' # to_xml require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/conversions' # to_xml require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/deep_transform_values' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/module/delegation' # delegate_missing_to require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/deep_dup' require 'active_support/core_ext/object/duplicable' require 'active_support/deprecation' require 'active_support/inflector' require 'active_support/ordered_options' require 'active_support/notifications' require 'English' require 'bigdecimal' require 'date' require 'dry-types' require 'dry-configurable' require 'forwardable' require 'json' require 'mustermann/grape' require 'pathname' require 'rack' require 'rack/auth/basic' require 'rack/builder' require 'rack/head' require 'singleton' require 'zeitwerk' loader = Zeitwerk::Loader.for_gem loader.inflector.inflect( 'api' => 'API', 'dsl' => 'DSL' ) railtie = "#{__dir__}/grape/railtie.rb" loader.do_not_eager_load(railtie) loader.setup I18n.load_path << File.expand_path('grape/locale/en.yml', __dir__) module Grape extend Dry::Configurable setting :param_builder, default: :hash_with_indifferent_access setting :lint, default: false HTTP_SUPPORTED_METHODS = [ Rack::GET, Rack::POST, Rack::PUT, Rack::PATCH, Rack::DELETE, Rack::HEAD, Rack::OPTIONS ].freeze def self.deprecator @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'Grape') end end # https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html # adding Grape.deprecator to Rails App if any require 'grape/railtie' if defined?(Rails::Railtie) loader.eager_load ================================================ FILE: spec/grape/api/custom_validations_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations do describe 'using a custom length validator' do subject do Class.new(Grape::API) do params do requires :text, default_length: 140 end get do 'bacon' end end end let(:default_length_validator) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) @option = params[:max].to_i if params.key?(:max) return if params[attr_name].length <= @option raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") end end end let(:app) { subject } before do stub_const('DefaultLengthValidator', default_length_validator) described_class.register(DefaultLengthValidator) end after do described_class.deregister(:default_length) end it 'under 140 characters' do get '/', text: 'abc' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'over 140 characters' do get '/', text: 'a' * 141 expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text must be at the most 140 characters long' end it 'specified in the query string' do get '/', text: 'a' * 141, max: 141 expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end end describe 'using a custom body-only validator' do subject do Class.new(Grape::API) do params do requires :text, in_body: true end get do 'bacon' end end end let(:in_body_validator) do Class.new(Grape::Validations::Validators::PresenceValidator) do def validate(request) validate!(request.env[Grape::Env::API_REQUEST_BODY]) end end end let(:app) { subject } before do stub_const('InBodyValidator', in_body_validator) described_class.register(InBodyValidator) end after do described_class.deregister(:in_body) end it 'allows field in body' do get '/', text: 'abc' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'ignores field in query' do get '/', nil, text: 'abc' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text is missing' end end describe 'using a custom validator with message_key' do subject do Class.new(Grape::API) do params do requires :text, with_message_key: true end get do 'bacon' end end end let(:message_key_validator) do Class.new(Grape::Validations::Validators::PresenceValidator) do def validate_param!(attr_name, _params) raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: :presence) end end end let(:app) { subject } before do stub_const('WithMessageKeyValidator', message_key_validator) described_class.register(WithMessageKeyValidator) end after do described_class.deregister(:with_message_key) end it 'fails with message' do get '/', text: 'foobar' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'text is missing' end end describe 'using a custom request/param validator' do subject do Class.new(Grape::API) do params do optional :admin_field, type: String, admin: true optional :non_admin_field, type: String optional :admin_false_field, type: String, admin: false end get do 'bacon' end end end let(:admin_validator) do Class.new(Grape::Validations::Validators::Base) do def validate(request) # return if the param we are checking was not in request # @attrs is a list containing the attribute we are currently validating return unless request.params.key? @attrs.first # check if admin flag is set to true return unless @option # check if user is admin or not # as an example get a token from request and check if it's admin or not raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers[access_header] == 'admin' end def access_header 'x-access-token' end end end let(:app) { subject } let(:x_access_token_header) { 'x-access-token' } before do stub_const('AdminValidator', admin_validator) described_class.register(AdminValidator) end after do described_class.deregister(:admin) end it 'fail when non-admin user sets an admin field' do get '/', admin_field: 'tester', non_admin_field: 'toaster' expect(last_response.status).to eq 400 expect(last_response.body).to include 'Can not set Admin only field.' end it 'does not fail when we send non-admin fields only' do get '/', non_admin_field: 'toaster' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'does not fail when we send non-admin and admin=false fields only' do get '/', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'does not fail when we send admin fields and we are admin' do header x_access_token_header, 'admin' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'bacon' end it 'fails when we send admin fields and we are not admin' do header x_access_token_header, 'user' get '/', admin_field: 'tester', non_admin_field: 'toaster', admin_false_field: 'test' expect(last_response.status).to eq 400 expect(last_response.body).to include 'Can not set Admin only field.' end end describe 'using a custom validator with instance variable' do let(:validator_type) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(_attr_name, _params) if @instance_variable raise Grape::Exceptions::Validation.new(params: ['params'], message: 'This should never happen') end @instance_variable = true end end end let(:app) do Class.new(Grape::API) do params do optional :param_to_validate, instance_validator: true optional :another_param_to_validate, instance_validator: true end get do 'noop' end end end before do stub_const('InstanceValidatorValidator', validator_type) described_class.register(InstanceValidatorValidator) end after do described_class.deregister(:instance_validator) end it 'passes validation every time' do expect(validator_type).to receive(:new).twice.and_call_original get '/', param_to_validate: 'value', another_param_to_validate: 'value' expect(last_response.status).to eq 200 end end end ================================================ FILE: spec/grape/api/deeply_included_options_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do let(:app) do main_api = api Class.new(Grape::API) do mount main_api end end let(:api) do deeply_included_options = options Class.new(Grape::API) do include deeply_included_options resource :users do get do status 200 end end end end let(:options) do deep_included_options_default = default Module.new do extend ActiveSupport::Concern include deep_included_options_default end end let(:default) do Module.new do extend ActiveSupport::Concern included do format :json end end end it 'works for unspecified format' do get '/users' expect(last_response.status).to be 200 expect(last_response.content_type).to eql 'application/json' end it 'works for specified format' do get '/users.json' expect(last_response.status).to be 200 expect(last_response.content_type).to eql 'application/json' end it "doesn't work for format different than specified" do get '/users.txt' expect(last_response.status).to be 404 end end ================================================ FILE: spec/grape/api/defines_boolean_in_params_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Instance do describe 'boolean constant' do let(:app) do Class.new(Grape::API) do params do requires :message, type: Grape::API::Boolean end post :echo do { class: params[:message].class.name, value: params[:message] } end end end let(:expected_body) do { class: 'TrueClass', value: true }.to_s end it 'sets Boolean as a type' do post '/echo?message=true' expect(last_response.status).to eq(201) expect(last_response.body).to eq expected_body end end end ================================================ FILE: spec/grape/api/documentation_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do subject { Class.new(described_class) } let(:app) { subject } context 'an endpoint with documentation' do it 'documents parameters' do subject.params do requires 'price', type: Float, desc: 'Sales price' end subject.get '/' expect(subject.routes.first.params['price']).to eq(required: true, type: 'Float', desc: 'Sales price') end it 'allows documentation with a hash' do documentation = { example: 'Joe' } subject.params do requires 'first_name', documentation: documentation end subject.get '/' expect(subject.routes.first.params['first_name'][:documentation]).to eq(documentation) end end context 'an endpoint without documentation' do before do subject.do_not_document! subject.params do requires :city, type: String, desc: 'Should be ignored' optional :postal_code, type: Integer end subject.post '/' do declared(params).to_json end end it 'does not document parameters for the endpoint' do expect(subject.routes.first.params).to eq({}) end it 'still declares params internally' do data = { city: 'Berlin', postal_code: 10_115 } post '/', data expect(last_response.body).to eq(data.to_json) end end end ================================================ FILE: spec/grape/api/inherited_helpers_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Helpers do let(:user) { 'Miguel Caneo' } let(:id) { '42' } let(:api_super_class) do Class.new(Grape::API) do helpers do params(:superclass_params) { requires :id, type: String } def current_user params[:user] end end end end let(:api_overridden_sub_class) do Class.new(api_super_class) do params { use :superclass_params } helpers do def current_user "#{params[:user]} with id" end end get 'resource' do "#{current_user}: #{params['id']}" end end end let(:api_sub_class) do Class.new(api_super_class) do params { use :superclass_params } get 'resource' do "#{current_user}: #{params['id']}" end end end let(:api_example) do Class.new(api_sub_class) do params { use :superclass_params } get 'resource' do "#{current_user}: #{params['id']}" end end end context 'non overriding subclass' do subject { api_sub_class } def app subject end context 'given expected params' do it 'inherits helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user}: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end context 'overriding subclass' do def app api_overridden_sub_class end context 'given expected params' do it 'overrides helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user} with id: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end context 'example subclass' do def app api_example end context 'given expected params' do it 'inherits helpers from a superclass' do get '/resource', id: id, user: user expect(last_response.body).to eq("#{user}: #{id}") end end context 'with lack of expected params' do it 'returns missing error' do get '/resource' expect(last_response.body).to eq('id is missing') end end end end ================================================ FILE: spec/grape/api/instance_spec.rb ================================================ # frozen_string_literal: true require 'shared/versioning_examples' describe Grape::API::Instance do subject(:an_instance) do Class.new(Grape::API::Instance) do namespace :some_namespace do get 'some_endpoint' do 'success' end end end end let(:root_api) do to_mount = an_instance Class.new(Grape::API) do mount to_mount end end def app root_api end context 'when an instance is mounted on the root' do it 'can call the instance endpoint' do get '/some_namespace/some_endpoint' expect(last_response.body).to eq 'success' end end context 'when an instance is the root' do let(:root_api) do to_mount = an_instance Class.new(Grape::API::Instance) do mount to_mount end end it 'can call the instance endpoint' do get '/some_namespace/some_endpoint' expect(last_response.body).to eq 'success' end end context 'with multiple moutes' do let(:first) do Class.new(Grape::API::Instance) do namespace(:some_namespace) do route :any, '*path' do error!('Not found! (1)', 404) end end end end let(:second) do Class.new(Grape::API::Instance) do namespace(:another_namespace) do route :any, '*path' do error!('Not found! (2)', 404) end end end end let(:root_api) do first_instance = first second_instance = second Class.new(Grape::API) do mount first_instance mount first_instance mount second_instance end end it 'does not raise a FrozenError on first instance' do expect { patch '/some_namespace/anything' }.not_to \ raise_error end it 'responds the correct body at the first instance' do patch '/some_namespace/anything' expect(last_response.body).to eq 'Not found! (1)' end it 'does not raise a FrozenError on second instance' do expect { get '/another_namespace/other' }.not_to \ raise_error end it 'responds the correct body at the second instance' do get '/another_namespace/foobar' expect(last_response.body).to eq 'Not found! (2)' end end end ================================================ FILE: spec/grape/api/invalid_format_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace do format :json content_type :json, 'application/json' params do requires :id, desc: 'Identifier.' end get ':id' do { id: params[:id], format: params[:format] } end end end context 'get' do it 'no format' do get '/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: nil)) end it 'json format' do get '/foo.json' expect(last_response.status).to eq 200 expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'json')) end it 'invalid format' do get '/foo.invalid' expect(last_response.status).to eq 200 expect(last_response.body).to eq(Grape::Json.dump(id: 'foo', format: 'invalid')) end end end ================================================ FILE: spec/grape/api/mount_and_helpers_order_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do describe 'rescue_from' do context 'when the API is mounted AFTER defining the class rescue_from handler' do let(:api_rescue_from) do Class.new(Grape::API) do rescue_from :all do error!({ type: 'all' }, 404) end get do { count: 1 / 0 } end end end let(:main_rescue_from_after) do context = self Class.new(Grape::API) do rescue_from ZeroDivisionError do error!({ type: 'zero' }, 500) end mount context.api_rescue_from end end def app main_rescue_from_after end it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do get '/' expect(last_response.status).to eq(500) expect(last_response.body).to eq({ type: 'zero' }.to_json) end end context 'when the API is mounted BEFORE defining the class rescue_from handler' do let(:api_rescue_from) do Class.new(Grape::API) do rescue_from :all do error!({ type: 'all' }, 404) end get do { count: 1 / 0 } end end end let(:main_rescue_from_before) do context = self Class.new(Grape::API) do mount context.api_rescue_from rescue_from ZeroDivisionError do error!({ type: 'zero' }, 500) end end end def app main_rescue_from_before end it 'is rescued by the rescue_from ZeroDivisionError handler from Main class' do get '/' expect(last_response.status).to eq(500) expect(last_response.body).to eq({ type: 'zero' }.to_json) end end end describe 'before' do context 'when the API is mounted AFTER defining the before helper' do let(:api_before_handler) do Class.new(Grape::API) do get do { count: @count }.to_json end end end let(:main_before_handler_after) do context = self Class.new(Grape::API) do before do @count = 1 end mount context.api_before_handler end end def app main_before_handler_after end it 'is able to access the variables defined in the before helper' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ count: 1 }.to_json) end end context 'when the API is mounted BEFORE defining the before helper' do let(:api_before_handler) do Class.new(Grape::API) do get do { count: @count }.to_json end end end let(:main_before_handler_before) do context = self Class.new(Grape::API) do mount context.api_before_handler before do @count = 1 end end end def app main_before_handler_before end it 'is able to access the variables defined in the before helper' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ count: 1 }.to_json) end end end describe 'after' do context 'when the API is mounted AFTER defining the after handler' do let(:api_after_handler) do Class.new(Grape::API) do get do { count: 1 }.to_json end end end let(:main_after_handler_after) do context = self Class.new(Grape::API) do after do error!({ type: 'after' }, 500) end mount context.api_after_handler end end def app main_after_handler_after end it 'is able to access the variables defined in the after helper' do get '/' expect(last_response.status).to eq(500) expect(last_response.body).to eq({ type: 'after' }.to_json) end end context 'when the API is mounted BEFORE defining the after helper' do let(:api_after_handler) do Class.new(Grape::API) do get do { count: 1 }.to_json end end end let(:main_after_handler_before) do context = self Class.new(Grape::API) do mount context.api_after_handler after do error!({ type: 'after' }, 500) end end end def app main_after_handler_before end it 'is able to access the variables defined in the after helper' do get '/' expect(last_response.status).to eq(500) expect(last_response.body).to eq({ type: 'after' }.to_json) end end end end ================================================ FILE: spec/grape/api/mount_and_rescue_from_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do context 'when multiple classes defines the same rescue_from' do let(:an_api) do Class.new(Grape::API) do rescue_from ZeroDivisionError do error!({ type: 'an-api-zero' }, 404) end get '/an-api' do { count: 1 / 0 } end end end let(:another_api) do Class.new(Grape::API) do rescue_from ZeroDivisionError do error!({ type: 'another-api-zero' }, 322) end get '/another-api' do { count: 1 / 0 } end end end let(:other_main) do context = self Class.new(Grape::API) do mount context.an_api mount context.another_api end end def app other_main end it 'is rescued by the rescue_from ZeroDivisionError handler defined inside each of the classes' do get '/an-api' expect(last_response.status).to eq(404) expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json) get '/another-api' expect(last_response.status).to eq(322) expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json) end context 'when some class does not define a rescue_from but it was defined in a previous mounted endpoint' do let(:an_api_without_defined_rescue_from) do Class.new(Grape::API) do get '/another-api-without-defined-rescue-from' do { count: 1 / 0 } end end end let(:other_main_with_not_defined_rescue_from) do context = self Class.new(Grape::API) do mount context.an_api mount context.another_api mount context.an_api_without_defined_rescue_from end end def app other_main_with_not_defined_rescue_from end it 'is not rescued by any of the previous defined rescue_from ZeroDivisionError handlers' do get '/an-api' expect(last_response.status).to eq(404) expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json) get '/another-api' expect(last_response.status).to eq(322) expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json) expect do get '/another-api-without-defined-rescue-from' end.to raise_error(ZeroDivisionError) end end end end ================================================ FILE: spec/grape/api/mounted_helpers_inheritance_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do context 'when mounting a child API that inherits helpers from parent API' do let(:child_api) do Class.new(Grape::API) do get '/test' do parent_helper end end end let(:parent_api) do context = self Class.new(Grape::API) do helpers do def parent_helper 'parent helper value' end end mount context.child_api end end def app parent_api end it 'inherits helpers from parent API to mounted child API' do get '/test' expect(last_response.status).to eq(200) expect(last_response.body).to eq('parent helper value') end end end ================================================ FILE: spec/grape/api/namespace_parameters_in_route_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :me do namespace :pending do get '/' do 'banana' end end put ':id' do params[:id] end end end context 'get' do it 'responds without ext' do get '/me/pending' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'banana' end end context 'put' do it 'responds' do put '/me/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end ================================================ FILE: spec/grape/api/nested_helpers_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Helpers do let(:helper_methods) do Module.new do extend Grape::API::Helpers def current_user @current_user ||= params[:current_user] end end end let(:nested) do context = self Class.new(Grape::API) do resource :level1 do helpers context.helper_methods get do current_user end resource :level2 do get do current_user end end end end end let(:main) do context = self Class.new(Grape::API) do mount context.nested end end def app main end it 'can access helpers from a mounted resource' do get '/level1', current_user: 'hello' expect(last_response.body).to eq('hello') end it 'can access helpers from a mounted resource in a nested resource' do get '/level1/level2', current_user: 'world' expect(last_response.body).to eq('world') end end ================================================ FILE: spec/grape/api/optional_parameters_in_route_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :api do get ':id(/:ext)' do [params[:id], params[:ext]].compact.join('/') end put ':id' do params[:id] end end end context 'get' do it 'responds without ext' do get '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end it 'responds with ext' do get '/api/foo/bar' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo/bar' end end context 'put' do it 'responds' do put '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end ================================================ FILE: spec/grape/api/parameters_modification_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :test do params do optional :foo, default: +'-abcdef' end get do params[:foo].slice!(0) params[:foo] end end end context 'when route modifies param value' do it 'param default should not change' do get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' get '/test?foo=-123456' expect(last_response.status).to eq 200 expect(last_response.body).to eq '123456' get '/test' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'abcdef' end end end ================================================ FILE: spec/grape/api/patch_method_helpers_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Helpers do let(:patch_public) do Class.new(Grape::API) do format :json version 'public-v1', using: :header, vendor: 'grape' get do { ok: 'public' } end end end let(:auth_methods) do Module.new do def authenticate!; end end end let(:patch_private) do context = self Class.new(Grape::API) do format :json version 'private-v1', using: :header, vendor: 'grape' helpers context.auth_methods before do authenticate! end get do { ok: 'private' } end end end let(:main) do context = self Class.new(Grape::API) do mount context.patch_public mount context.patch_private end end def app main end context 'patch' do it 'public' do patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 405 end it 'private' do patch '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 405 end it 'default' do patch '/' expect(last_response.status).to eq 405 end end context 'default' do it 'public' do get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-public-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end it 'private' do get '/', {}, 'HTTP_ACCEPT' => 'application/vnd.grape-private-v1+json' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'private' }.to_json) end it 'default' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ ok: 'public' }.to_json) end end end ================================================ FILE: spec/grape/api/recognize_path_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do describe '.recognize_path' do subject { Class.new(described_class) } it 'fetches endpoint by given path' do subject.get('/foo/:id') {} subject.get('/bar/:id') {} subject.get('/baz/:id') {} actual = subject.recognize_path('/bar/1234').routes[0].origin expect(actual).to eq('/bar/:id') end it 'returns nil if given path does not match with registered routes' do subject.get {} expect(subject.recognize_path('/bar/1234')).to be_nil end context 'when parametrized route with type specified together with a static route' do subject do Class.new(described_class) do resource :books do route_param :id, type: Integer do get do end resource :loans do route_param :loan_id, type: Integer do get do end end resource :print do post do end end end end resource :share do post do end end end end end it 'recognizes the static route when the parameter does not match with the specified type' do actual = subject.recognize_path('/books/share').routes[0].origin expect(actual).to eq('/books/share') end it 'does not recognize any endpoint when there is not other endpoint that matches with the requested path' do actual = subject.recognize_path('/books/other') expect(actual).to be_nil end it 'recognizes the parametrized route when the parameter matches with the specified type' do actual = subject.recognize_path('/books/1').routes[0].origin expect(actual).to eq('/books/:id') end it 'recognizes the static nested route when the parameter does not match with the specified type' do actual = subject.recognize_path('/books/1/loans/print').routes[0].origin expect(actual).to eq('/books/:id/loans/print') end it 'recognizes the nested parametrized route when the parameter matches with the specified type' do actual = subject.recognize_path('/books/1/loans/33').routes[0].origin expect(actual).to eq('/books/:id/loans/:loan_id') end end end end ================================================ FILE: spec/grape/api/required_parameters_in_route_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace :api do get ':id' do [params[:id], params[:ext]].compact.join('/') end put ':something_id' do params[:something_id] end end end context 'get' do it 'responds' do get '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end context 'put' do it 'responds' do put '/api/foo' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'foo' end end end ================================================ FILE: spec/grape/api/required_parameters_with_invalid_method_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end before do subject.namespace do params do requires :id, desc: 'Identifier.' end get ':id' do end end end context 'post' do it '405' do post '/something' expect(last_response.status).to eq 405 end end end ================================================ FILE: spec/grape/api/routes_with_requirements_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end context 'get' do it 'routes to a namespace param with dots' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do get '/' do params[:ns_with_dots] end end get '/test.id.with.dots' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id.with.dots' end it 'routes to a path with multiple params with dots' do subject.get ':id_with_dots/:another_id_with_dots', requirements: { id_with_dots: %r{[^/]+}, another_id_with_dots: %r{[^/]+} } do "#{params[:id_with_dots]}/#{params[:another_id_with_dots]}" end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end it 'routes to namespace and path params with dots, with overridden requirements' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do get ':another_id_with_dots', requirements: { ns_with_dots: %r{[^/]+}, another_id_with_dots: %r{[^/]+} } do "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" end end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end it 'routes to namespace and path params with dots, with merged requirements' do subject.namespace ':ns_with_dots', requirements: { ns_with_dots: %r{[^/]+} } do get ':another_id_with_dots', requirements: { another_id_with_dots: %r{[^/]+} } do "#{params[:ns_with_dots]}/#{params[:another_id_with_dots]}" end end get '/test.id/test2.id' expect(last_response.status).to eq 200 expect(last_response.body).to eq 'test.id/test2.id' end end end ================================================ FILE: spec/grape/api/shared_helpers_exactly_one_of_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Helpers do let(:app) do Class.new(Grape::API) do helpers Module.new do extend Grape::API::Helpers params :drink do optional :beer optional :wine exactly_one_of :beer, :wine end end format :json params do requires :orderType, type: String, values: %w[food drink] given orderType: ->(val) { val == 'food' } do optional :pasta optional :pizza exactly_one_of :pasta, :pizza end given orderType: ->(val) { val == 'drink' } do use :drink end end get do declared(params, include_missing: true) end end end it 'defines parameters' do get '/', orderType: 'food', pizza: 'mista' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ orderType: 'food', pasta: nil, pizza: 'mista', beer: nil, wine: nil }.to_json) end end ================================================ FILE: spec/grape/api/shared_helpers_spec.rb ================================================ # frozen_string_literal: true describe Grape::API::Helpers do subject do shared_params = Module.new do extend Grape::API::Helpers params :pagination do optional :page, type: Integer optional :size, type: Integer end end Class.new(Grape::API) do helpers shared_params format :json params do use :pagination end get do declared(params, include_missing: true) end end end def app subject end it 'defines parameters' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ page: nil, size: nil }.to_json) end end ================================================ FILE: spec/grape/api_remount_spec.rb ================================================ # frozen_string_literal: true require 'shared/versioning_examples' describe Grape::API do subject(:a_remounted_api) { Class.new(described_class) } let(:root_api) { Class.new(described_class) } let(:app) { root_api } describe 'remounting an API' do context 'with a defined route' do before do a_remounted_api.get '/votes' do '10 votes' end end context 'when mounting one instance' do before do root_api.mount a_remounted_api end it 'can access the endpoint' do get '/votes' expect(last_response.body).to eql '10 votes' end end context 'when mounting twice' do before do root_api.mount a_remounted_api => '/posts' root_api.mount a_remounted_api => '/comments' end it 'can access the votes in both places' do get '/posts/votes' expect(last_response.body).to eql '10 votes' get '/comments/votes' expect(last_response.body).to eql '10 votes' end end context 'when mounting on namespace' do before do stub_const('StaticRefToAPI', a_remounted_api) root_api.namespace 'posts' do mount StaticRefToAPI end root_api.namespace 'comments' do mount StaticRefToAPI end end it 'can access the votes in both places' do get '/posts/votes' expect(last_response.body).to eql '10 votes' get '/comments/votes' expect(last_response.body).to eql '10 votes' end end end describe 'with dynamic configuration' do context 'when mounting an endpoint conditional on a configuration' do subject(:a_remounted_api) do Class.new(described_class) do get 'always' do 'success' end given configuration[:mount_sometimes] do get 'sometimes' do 'sometimes' end end end end it 'mounts the endpoints only when configured to do so' do root_api.mount({ a_remounted_api => 'with_conditional' }, with: { mount_sometimes: true }) root_api.mount({ a_remounted_api => 'without_conditional' }, with: { mount_sometimes: false }) get '/with_conditional/always' expect(last_response.body).to eq 'success' get '/with_conditional/sometimes' expect(last_response.body).to eq 'sometimes' get '/without_conditional/always' expect(last_response.body).to eq 'success' get '/without_conditional/sometimes' expect(last_response).to be_not_found end end context 'when using an expression derived from a configuration' do subject(:a_remounted_api) do Class.new(described_class) do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end end before do root_api.mount a_remounted_api, with: { api_name: 'a_name' } end it 'mounts the endpoint with the name' do get 'api_name_a_name' expect(last_response.body).to eq 'success' end it 'does not mount the endpoint with a null name' do get 'api_name_' expect(last_response.body).not_to eq 'success' end context 'when the expression lives in a namespace' do subject(:a_remounted_api) do Class.new(described_class) do namespace :base do get(mounted { "api_name_#{configuration[:api_name]}" }) do 'success' end end end end it 'mounts the endpoint with the name' do get 'base/api_name_a_name' expect(last_response.body).to eq 'success' end it 'does not mount the endpoint with a null name' do get 'base/api_name_' expect(last_response.body).not_to eq 'success' end end end context 'when the params are configured via a configuration' do subject(:a_remounted_api) do Class.new(described_class) do params do requires configuration[:required_attr_name], type: String end get(mounted { configuration[:endpoint] }) do status 200 end end end context 'when the configured param is my_attr' do it 'requires the configured params' do root_api.mount a_remounted_api, with: { required_attr_name: 'my_attr', endpoint: 'test' } get 'test?another_attr=1' expect(last_response).to be_bad_request get 'test?my_attr=1' expect(last_response).to be_successful root_api.mount a_remounted_api, with: { required_attr_name: 'another_attr', endpoint: 'test_b' } get 'test_b?another_attr=1' expect(last_response).to be_successful get 'test_b?my_attr=1' expect(last_response).to be_bad_request end end end context 'when executing a standard block within a `mounted` block with all dynamic params' do subject(:a_remounted_api) do Class.new(described_class) do mounted do desc configuration[:description] do headers configuration[:headers] end get configuration[:endpoint] do configuration[:response] end end end end let(:api_endpoint) { 'custom_endpoint' } let(:api_response) { 'custom response' } let(:endpoint_description) { 'this is a custom API' } let(:headers) do { 'XAuthToken' => { 'description' => 'Validates your identity', 'required' => true } } end it 'mounts the API and obtains the description and headers definition' do root_api.mount a_remounted_api, with: { description: endpoint_description, headers: headers, endpoint: api_endpoint, response: api_response } get api_endpoint expect(last_response.body).to eq api_response expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:description]) .to eq endpoint_description expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:headers]) .to eq headers end end context 'when executing a custom block on mount' do subject(:a_remounted_api) do Class.new(described_class) do get 'always' do 'success' end mounted do configuration[:endpoints].each do |endpoint_name, endpoint_response| get endpoint_name do endpoint_response end end end end end it 'mounts the endpoints only when configured to do so' do root_api.mount a_remounted_api, with: { endpoints: { 'api_name' => 'api_response' } } get 'api_name' expect(last_response.body).to eq 'api_response' end end context 'when the configuration is part of the arguments of a method' do subject(:a_remounted_api) do Class.new(described_class) do get configuration[:endpoint_name] do 'success' end end end it 'mounts the endpoint in the location it is configured' do root_api.mount a_remounted_api, with: { endpoint_name: 'some_location' } get '/some_location' expect(last_response.body).to eq 'success' get '/different_location' expect(last_response).to be_not_found root_api.mount a_remounted_api, with: { endpoint_name: 'new_location' } get '/new_location' expect(last_response.body).to eq 'success' end context 'when the configuration is the value in a key-arg pair' do subject(:a_remounted_api) do Class.new(described_class) do version 'v1', using: :param, parameter: configuration[:version_param] get 'endpoint' do 'version 1' end version 'v2', using: :param, parameter: configuration[:version_param] get 'endpoint' do 'version 2' end end end it 'takes the param from the configuration' do root_api.mount a_remounted_api, with: { version_param: 'param_name' } get '/endpoint?param_name=v1' expect(last_response.body).to eq 'version 1' get '/endpoint?param_name=v2' expect(last_response.body).to eq 'version 2' get '/endpoint?wrong_param_name=v2' expect(last_response.body).to eq 'version 1' end end end context 'on the DescSCope' do subject(:a_remounted_api) do Class.new(described_class) do desc 'The description of this' do tags ['not_configurable_tag', configuration[:a_configurable_tag]] end get 'location' do route.tags end end end it 'mounts the endpoint with the appropiate tags' do root_api.mount({ a_remounted_api => 'integer' }, with: { a_configurable_tag: 'a configured tag' }) get '/integer/location', param_key: 'a' expect(JSON.parse(last_response.body)).to eq ['not_configurable_tag', 'a configured tag'] end end context 'on the ParamScope' do subject(:a_remounted_api) do Class.new(described_class) do params do requires configuration[:required_param], type: configuration[:required_type] end get 'location' do 'success' end end end it 'mounts the endpoint in the location it is configured' do root_api.mount({ a_remounted_api => 'string' }, with: { required_param: 'param_key', required_type: String }) root_api.mount({ a_remounted_api => 'integer' }, with: { required_param: 'param_integer', required_type: Integer }) get '/string/location', param_key: 'a' expect(last_response.body).to eq 'success' get '/string/location', param_integer: 1 expect(last_response).to be_bad_request get '/integer/location', param_integer: 1 expect(last_response.body).to eq 'success' get '/integer/location', param_integer: 'a' expect(last_response).to be_bad_request end context 'on dynamic checks' do subject(:a_remounted_api) do Class.new(described_class) do params do optional :restricted_values, values: -> { [configuration[:allowed_value], 'always'] } end get 'location' do 'success' end end end it 'can read the configuration on lambdas' do root_api.mount a_remounted_api, with: { allowed_value: 'sometimes' } get '/location', restricted_values: 'always' expect(last_response.body).to eq 'success' get '/location', restricted_values: 'sometimes' expect(last_response.body).to eq 'success' get '/location', restricted_values: 'never' expect(last_response).to be_bad_request end end end context 'when the configuration is read within a namespace' do before do a_remounted_api.namespace 'api' do params do requires configuration[:required_param] end get "/#{configuration[:path]}" do '10 votes' end end root_api.mount a_remounted_api, with: { path: 'votes', required_param: 'param_key' } root_api.mount a_remounted_api, with: { path: 'scores', required_param: 'param_key' } end it 'uses the dynamic configuration on all routes' do get 'api/votes', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/scores', param_key: 'a' expect(last_response.body).to eql '10 votes' get 'api/votes' expect(last_response).to be_bad_request end end context 'a very complex configuration example' do before do top_level_api = Class.new(described_class) do remounted_api = Class.new(Grape::API) do get configuration[:endpoint_name] do configuration[:response] end end expression_namespace = mounted { configuration[:namespace].to_s * 2 } given(mounted { configuration[:should_mount_expressed] != false }) do namespace expression_namespace do mount remounted_api, with: { endpoint_name: configuration[:endpoint_name], response: configuration[:endpoint_response] } end end end root_api.mount top_level_api, with: configuration_options end context 'when the namespace should be mounted' do let(:configuration_options) do { should_mount_expressed: true, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).to eq 'bond' end end context 'when should be mounted is nil' do let(:configuration_options) do { should_mount_expressed: nil, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).to eq 'bond' end end context 'when it should not be mounted' do let(:configuration_options) do { should_mount_expressed: false, namespace: 'bang', endpoint_name: 'james', endpoint_response: 'bond' } end it 'gets a response' do get 'bangbang/james' expect(last_response.body).not_to eq 'bond' end end end context 'when the configuration is read in a helper' do subject(:a_remounted_api) do Class.new(described_class) do helpers do def printed_response configuration[:some_value] end end get 'location' do printed_response end end end it 'uses the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' expect(last_response.body).to eq 'response value' end end context 'when the configuration is read within the response block' do subject(:a_remounted_api) do Class.new(described_class) do get 'location' do configuration[:some_value] end end end it 'uses the dynamic configuration on all routes' do root_api.mount(a_remounted_api, with: { some_value: 'response value' }) get '/location' expect(last_response.body).to eq 'response value' end end end context 'with route settings' do before do a_remounted_api.desc 'Identical description' a_remounted_api.route_setting :custom, key: 'value' a_remounted_api.route_setting :custom_diff, key: 'foo' a_remounted_api.get '/api1' do status 200 end a_remounted_api.desc 'Identical description' a_remounted_api.route_setting :custom, key: 'value' a_remounted_api.route_setting :custom_diff, key: 'bar' a_remounted_api.get '/api2' do status 200 end end it 'has all the settings for both routes' do expect(a_remounted_api.routes.count).to be(2) expect(a_remounted_api.routes[0].settings).to include( { description: { description: 'Identical description' }, custom: { key: 'value' }, custom_diff: { key: 'foo' } } ) expect(a_remounted_api.routes[1].settings).to include( { description: { description: 'Identical description' }, custom: { key: 'value' }, custom_diff: { key: 'bar' } } ) end context 'when mounting it' do before do root_api.mount a_remounted_api end it 'still has all the settings for both routes' do expect(root_api.routes.count).to be(2) expect(root_api.routes[0].settings).to include( { description: { description: 'Identical description' }, custom: { key: 'value' }, custom_diff: { key: 'foo' } } ) expect(root_api.routes[1].settings).to include( { description: { description: 'Identical description' }, custom: { key: 'value' }, custom_diff: { key: 'bar' } } ) end end end end end ================================================ FILE: spec/grape/api_spec.rb ================================================ # frozen_string_literal: true require 'shared/versioning_examples' describe Grape::API do subject { Class.new(described_class) } let(:app) { subject } describe '.prefix' do it 'routes root through with the prefix' do subject.prefix 'awesome/sauce' subject.get do 'Hello there.' end get 'awesome/sauce/' expect(last_response).to be_successful expect(last_response.body).to eql 'Hello there.' end it 'routes through with the prefix' do subject.prefix 'awesome/sauce' subject.get :hello do 'Hello there.' end get 'awesome/sauce/hello' expect(last_response.body).to eql 'Hello there.' get '/hello' expect(last_response).to be_not_found end it 'supports OPTIONS' do subject.prefix 'awesome/sauce' subject.get do 'Hello there.' end options 'awesome/sauce' expect(last_response).to be_no_content expect(last_response.body).to be_blank end it 'disallows POST' do subject.prefix 'awesome/sauce' subject.get post 'awesome/sauce' expect(last_response).to be_method_not_allowed end end describe '.version' do context 'when defined' do it 'returns version value' do subject.version 'v1' expect(subject.version).to eq('v1') end end context 'when not defined' do it 'returns nil' do expect(subject.version).to be_nil end end end describe '.version using path' do it_behaves_like 'versioning' do let(:macro_options) do { using: :path } end end end describe '.version using param' do it_behaves_like 'versioning' do let(:macro_options) do { using: :param, parameter: 'apiver' } end end end describe '.version using header' do it_behaves_like 'versioning' do let(:macro_options) do { using: :header, vendor: 'mycompany', format: 'json' } end end end describe '.version using accept_version_header' do it_behaves_like 'versioning' do let(:macro_options) do { using: :accept_version_header } end end end describe '.represent' do it 'requires a :with option' do expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent) end it 'adds the association to the :representations setting' do dummy_presenter_klass = Class.new represent_object = Class.new subject.represent represent_object, with: dummy_presenter_klass expect(subject.inheritable_setting.namespace_stackable[:representations]).to eq([represent_object => dummy_presenter_klass]) end end describe '.namespace' do it 'is retrievable and converted to a path' do internal_namespace = nil subject.namespace :awesome do internal_namespace = namespace end expect(internal_namespace).to eql('/awesome') end it 'comes after the prefix and version' do subject.prefix :rad subject.version 'v1', using: :path subject.namespace :awesome do get('/hello') { 'worked' } end get '/rad/v1/awesome/hello' expect(last_response.body).to eq('worked') end it 'cancels itself after the block is over' do internal_namespace = nil subject.namespace :awesome do internal_namespace = namespace end expect(subject.namespace).to eql('/') end it 'is stackable' do internal_namespace = nil internal_second_namespace = nil subject.namespace :awesome do internal_namespace = namespace namespace :rad do internal_second_namespace = namespace end end expect(internal_namespace).to eq('/awesome') expect(internal_second_namespace).to eq('/awesome/rad') end it 'accepts path segments correctly' do inner_namespace = nil subject.namespace :members do namespace '/:member_id' do inner_namespace = namespace get '/' do params[:member_id] end end end get '/members/23' expect(last_response.body).to eq('23') expect(inner_namespace).to eq('/members/:member_id') end it 'is callable with nil just to push onto the stack' do subject.namespace do version 'v2', using: :path get('/hello') { 'inner' } end subject.get('/hello') { 'outer' } get '/v2/hello' expect(last_response.body).to eq('inner') get '/hello' expect(last_response.body).to eq('outer') end %w[group resource resources segment].each do |als| it "`.#{als}` is an alias" do inner_namespace = nil subject.__send__(als, :awesome) do inner_namespace = namespace end expect(inner_namespace).to eq '/awesome' end end end describe '.call' do context 'it does not add to the app setup' do it 'calls the app' do expect(subject).not_to receive(:add_setup) subject.call({}) end end end describe '.route_param' do it 'adds a parameterized route segment namespace' do subject.namespace :users do route_param :id do get do params[:id] end end end get '/users/23' expect(last_response.body).to eq('23') end it 'defines requirements with a single hash' do subject.namespace :users do route_param :id, requirements: /[0-9]+/ do get do params[:id] end end end get '/users/michael' expect(last_response).to be_not_found get '/users/23' expect(last_response).to be_successful end context 'with param type definitions' do it 'is used by passing to options' do subject.namespace :route_param do route_param :foo, type: Integer do get { params.to_json } end end get '/route_param/1234' expect(last_response.body).to eq('{"foo":1234}') end end end describe '.route' do it 'allows for no path' do subject.namespace :votes do get do 'Votes' end post do 'Created a Vote' end end get '/votes' expect(last_response.body).to eql 'Votes' post '/votes' expect(last_response.body).to eql 'Created a Vote' end it 'handles empty calls' do subject.get '/' get '/' expect(last_response.body).to eql '' end describe 'root routes should work with' do before do subject.format :txt subject.content_type :json, 'application/json' subject.formatter :json, ->(object, _env) { object } def subject.enable_root_route! get('/') { 'root' } end end shared_examples_for 'a root route' do it 'returns root' do expect(last_response.body).to eql 'root' end end describe 'path versioned APIs' do before do subject.version version, using: :path subject.enable_root_route! end context 'when a single version provided' do let(:version) { 'v1' } context 'without a format' do before do versioned_get '/', 'v1', using: :path end it_behaves_like 'a root route' end context 'with a format' do before do get '/v1/.json' end it_behaves_like 'a root route' end end context 'when array of versions provided' do let(:version) { %w[v1 v2] } context 'when v1' do before do versioned_get '/', 'v1', using: :path end it_behaves_like 'a root route' end context 'when v2' do before do versioned_get '/', 'v2', using: :path end it_behaves_like 'a root route' end end end context 'when header versioned APIs' do before do subject.version 'v1', using: :header, vendor: 'test' subject.enable_root_route! versioned_get '/', 'v1', using: :header, vendor: 'test' end it_behaves_like 'a root route' end context 'when header versioned APIs with multiple headers' do before do subject.version %w[v1 v2], using: :header, vendor: 'test' subject.enable_root_route! end context 'when v1' do before do versioned_get '/', 'v1', using: :header, vendor: 'test' end it_behaves_like 'a root route' end context 'when v2' do before do versioned_get '/', 'v2', using: :header, vendor: 'test' end it_behaves_like 'a root route' end end context 'param versioned APIs' do before do subject.version 'v1', using: :param subject.enable_root_route! versioned_get '/', 'v1', using: :param end it_behaves_like 'a root route' end context 'when Accept-Version header versioned APIs' do before do subject.version 'v1', using: :accept_version_header subject.enable_root_route! versioned_get '/', 'v1', using: :accept_version_header end it_behaves_like 'a root route' end context 'unversioned APIss' do before do subject.enable_root_route! get '/' end it_behaves_like 'a root route' end end it 'allows for multiple paths' do subject.get(['/abc', '/def']) do 'foo' end get '/abc' expect(last_response.body).to eql 'foo' get '/def' expect(last_response.body).to eql 'foo' end context 'format' do before do dummy_class = Class.new do def to_json(*_rest) 'abc' end def to_txt 'def' end end subject.get('/abc') do dummy_class.new end end it 'allows .json' do get '/abc.json' expect(last_response).to be_successful expect(last_response.body).to eql 'abc' # json-encoded symbol end it 'allows .txt' do get '/abc.txt' expect(last_response).to be_successful expect(last_response.body).to eql 'def' # raw text end end it 'allows for format without corrupting a param' do subject.get('/:id') do { 'id' => params[:id] } end get '/awesome.json' expect(last_response.body).to eql '{"id":"awesome"}' end it 'allows for format in namespace with no path' do subject.namespace :abc do get do ['json'] end end get '/abc.json' expect(last_response.body).to eql '["json"]' end it 'allows for multiple verbs' do subject.route(%i[get post], '/abc') do 'hiya' end subject.endpoints.first.routes.each do |route| expect(route.path).to eql '/abc(.:format)' end get '/abc' expect(last_response.body).to eql 'hiya' post '/abc' expect(last_response.body).to eql 'hiya' end objects = ['string', :symbol, 1, -1.1, {}, [], true, false, nil].freeze %i[put post].each do |verb| context verb.to_s do objects.each do |object| it "allows a(n) #{object.class} json object in params" do subject.format :json subject.__send__(verb) do env[Grape::Env::API_REQUEST_BODY] end __send__ verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql Grape::Json.dump(object) expect(last_request.params).to eql({}) end it 'stores input in api.request.input' do subject.format :json subject.__send__(verb) do env[Grape::Env::API_REQUEST_INPUT] end __send__ verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql Grape::Json.dump(object).to_json end context 'chunked transfer encoding' do it 'stores input in api.request.input' do subject.format :json subject.__send__(verb) do env[Grape::Env::API_REQUEST_INPUT] end __send__ verb, '/', Grape::Json.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked' expect(last_response.status).to eq(verb == :post ? 201 : 200) expect(last_response.body).to eql Grape::Json.dump(object).to_json end end end end end it 'allows for multipart paths' do subject.route(%i[get post], '/:id/first') do 'first' end subject.route(%i[get post], '/:id') do 'ola' end subject.route(%i[get post], '/:id/first/second') do 'second' end get '/1' expect(last_response.body).to eql 'ola' post '/1' expect(last_response.body).to eql 'ola' get '/1/first' expect(last_response.body).to eql 'first' post '/1/first' expect(last_response.body).to eql 'first' get '/1/first/second' expect(last_response.body).to eql 'second' end it 'allows for :any as a verb' do subject.route(:any, '/abc') do 'lol' end %w[get post put delete options patch].each do |m| __send__(m, '/abc') expect(last_response.body).to eql 'lol' end end it 'allows for catch-all in a namespace' do subject.namespace :nested do get do 'root' end get 'something' do 'something' end route :any, '*path' do 'catch-all' end end get 'nested' expect(last_response.body).to eql 'root' get 'nested/something' expect(last_response.body).to eql 'something' get 'nested/missing' expect(last_response.body).to eql 'catch-all' post 'nested' expect(last_response.body).to eql 'catch-all' post 'nested/something' expect(last_response.body).to eql 'catch-all' end verbs = %w[post get head delete put options patch] verbs.each do |verb| it "allows and properly constrain a #{verb.upcase} method" do subject.__send__(verb, '/example') do verb end __send__(verb, '/example') expect(last_response.body).to eql verb == 'head' ? '' : verb # Call it with all methods other than the properly constrained one. (verbs - [verb]).each do |other_verb| __send__(other_verb, '/example') expected_rc = if other_verb == 'options' then 204 elsif other_verb == 'head' && verb == 'get' then 200 else 405 end expect(last_response.status).to eql expected_rc end end end it 'returns a 201 response code for POST by default' do subject.post('example') do 'Created' end post '/example' expect(last_response).to be_created expect(last_response.body).to eql 'Created' end it 'returns a 405 for an unsupported method with an X-Custom-Header' do subject.before { header 'X-Custom-Header', 'foo' } subject.get 'example' do 'example' end put '/example' expect(last_response).to be_method_not_allowed expect(last_response.body).to eql '405 Not Allowed' expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs only the before filter on 405 bad method' do subject.namespace :example do before { header 'X-Custom-Header', 'foo' } before_validation { raise 'before_validation filter should not run' } after_validation { raise 'after_validation filter should not run' } after { raise 'after filter should not run' } params { requires :only_for_get } get end post '/example' expect(last_response).to be_method_not_allowed expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs before filter exactly once on 405 bad method' do already_run = false subject.namespace :example do before do raise 'before filter ran twice' if already_run already_run = true header 'X-Custom-Header', 'foo' end get end post '/example' expect(last_response).to be_method_not_allowed expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'runs all filters and body with a custom OPTIONS method' do subject.namespace :example do before { header 'X-Custom-Header-1', 'foo' } before_validation { header 'X-Custom-Header-2', 'foo' } after_validation { header 'X-Custom-Header-3', 'foo' } after { header 'X-Custom-Header-4', 'foo' } options { 'yup' } get end options '/example' expect(last_response).to be_successful expect(last_response.body).to eql 'yup' expect(last_response.headers['Allow']).to be_nil expect(last_response.headers['X-Custom-Header-1']).to eql 'foo' expect(last_response.headers['X-Custom-Header-2']).to eql 'foo' expect(last_response.headers['X-Custom-Header-3']).to eql 'foo' expect(last_response.headers['X-Custom-Header-4']).to eql 'foo' end context 'when format is xml' do it 'returns a 405 for an unsupported method' do subject.format :xml subject.get 'example' do 'example' end put '/example' expect(last_response).to be_method_not_allowed expect(last_response.body).to eq <<~XML 405 Not Allowed XML end end context 'when accessing env' do it 'returns a 405 for an unsupported method' do subject.before do _customheader1 = headers['X-Custom-Header'] _customheader2 = env['HTTP_X_CUSTOM_HEADER'] end subject.get 'example' do 'example' end put '/example' expect(last_response).to be_method_not_allowed expect(last_response.body).to eql '405 Not Allowed' end end specify '405 responses includes an Allow header specifying supported methods' do subject.get 'example' do 'example' end subject.post 'example' do 'example' end put '/example' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, POST, HEAD' end specify '405 responses includes an Content-Type header' do subject.get 'example' do 'example' end subject.post 'example' do 'example' end put '/example' expect(last_response.content_type).to eql 'text/plain' end describe 'adds an OPTIONS route that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.before_validation { header 'X-Custom-Header-2', 'bar' } subject.after_validation { header 'X-Custom-Header-3', 'baz' } subject.after { header 'X-Custom-Header-4', 'bing' } subject.params { requires :only_for_get } subject.get 'example' do 'example' end subject.route :any, '*path' do error! :not_found, 404 end options '/example' end it 'returns a 204' do expect(last_response).to be_no_content end it 'has an empty body' do expect(last_response.body).to be_blank end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD' end it 'calls before hook' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'does not call before_validation hook' do expect(last_response.headers.key?('X-Custom-Header-2')).to be false end it 'does not call after_validation hook' do expect(last_response.headers.key?('X-Custom-Header-3')).to be false end it 'calls after hook' do expect(last_response.headers['X-Custom-Header-4']).to eq 'bing' end it 'has no Content-Type' do expect(last_response.content_type).to be_nil end it 'has no Content-Length' do expect(last_response.content_length).to be_nil end end describe 'when a resource routes by POST, GET, PATCH, PUT, and DELETE' do before do subject.namespace :example do get do 'example' end patch do 'example' end post do 'example' end delete do 'example' end put do 'example' end end options '/example' end describe 'it adds an OPTIONS route for namespaced endpoints that' do it 'returns a 204' do expect(last_response).to be_no_content end it 'has an empty body' do expect(last_response.body).to be_blank end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, PATCH, POST, DELETE, PUT, HEAD' end end end describe 'adds an OPTIONS route for namespaced endpoints that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.namespace :example do before { header 'X-Custom-Header-2', 'foo' } get :inner do 'example/inner' end end options '/example/inner' end it 'returns a 204' do expect(last_response).to be_no_content end it 'has an empty body' do expect(last_response.body).to be_blank end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD' end it 'calls the outer before filter' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end it 'calls the inner before filter' do expect(last_response.headers['X-Custom-Header-2']).to eql 'foo' end it 'has no Content-Type' do expect(last_response.content_type).to be_nil end it 'has no Content-Length' do expect(last_response.content_length).to be_nil end end describe 'adds a 405 Not Allowed route that' do before do subject.before { header 'X-Custom-Header', 'foo' } subject.post :example do 'example' end get '/example' end it 'returns a 405' do expect(last_response).to be_method_not_allowed end it 'contains error message in body' do expect(last_response.body).to eq '405 Not Allowed' end it 'has an Allow header' do expect(last_response.headers['Allow']).to eql 'OPTIONS, POST' end it 'has a X-Custom-Header' do expect(last_response.headers['X-Custom-Header']).to eql 'foo' end end describe 'when hook behaviour is controlled by attributes on the route' do before do subject.before do error!('Access Denied', 401) unless route.options[:secret] == params[:secret] end subject.namespace 'example' do before do error!('Access Denied', 401) unless route.options[:namespace_secret] == params[:namespace_secret] end desc 'it gets with secret', secret: 'password' get { status(params[:id] == '504' ? 200 : 404) } desc 'it post with secret', secret: 'password', namespace_secret: 'namespace_password' post {} end end context 'when HTTP method is not defined' do let(:response) { delete('/example') } it 'responds with a 405 status' do expect(response).to be_method_not_allowed end end context 'when HTTP method is defined with attribute' do let(:response) { post('/example?secret=incorrect_password') } it 'responds with the defined error in the before hook' do expect(response).to be_unauthorized end end context 'when HTTP method is defined and the underlying before hook expectation is not met' do let(:response) { post('/example?secret=password&namespace_secret=wrong_namespace_password') } it 'ends up in the endpoint' do expect(response).to be_unauthorized end end context 'when HTTP method is defined and everything is like the before hooks expect' do let(:response) { post('/example?secret=password&namespace_secret=namespace_password') } it 'ends up in the endpoint' do expect(response).to be_created end end context 'when HEAD is called for the defined GET' do let(:response) { head('/example?id=504') } it 'responds with 401 because before expectations in before hooks are not met' do expect(response).to be_unauthorized end end context 'when HEAD is called for the defined GET' do let(:response) { head('/example?id=504&secret=password') } it 'responds with 200 because before hooks are not called' do expect(response).to be_successful end end end context 'allows HEAD on a GET request that' do before do subject.get 'example' do 'example' end subject.route :any, '*path' do error! :not_found, 404 end head '/example' end it 'returns a 200' do expect(last_response).to be_successful end it 'has an empty body' do expect(last_response.body).to eql '' end end it 'overwrites the default HEAD request' do subject.head 'example' do error! 'nothing to see here', 400 end subject.get 'example' do 'example' end head '/example' expect(last_response).to be_bad_request end end context 'do_not_route_head!' do before do subject.do_not_route_head! subject.get 'example' do 'example' end end it 'options does not contain HEAD' do options '/example' expect(last_response).to be_no_content expect(last_response.body).to eql '' expect(last_response.headers['Allow']).to eql 'OPTIONS, GET' end it 'does not allow HEAD on a GET request' do head '/example' expect(last_response).to be_method_not_allowed end end context 'do_not_route_options!' do before do subject.do_not_route_options! subject.get 'example' do 'example' end end it 'does not create an OPTIONS route' do options '/example' expect(last_response).to be_method_not_allowed end it 'does not include OPTIONS in Allow header' do options '/example' expect(last_response).to be_method_not_allowed expect(last_response.headers['Allow']).to eql 'GET, HEAD' end end describe '.compile!' do let(:base_instance) { app.base_instance } before do allow(base_instance).to receive(:compile!).and_return(:compiled!) end it 'returns compiled!' do expect(app.__send__(:compile!)).to eq(:compiled!) end end describe 'filters' do it 'adds a before filter' do subject.before { @foo = 'first' } subject.before { @bar = 'second' } subject.get '/' do "#{@foo} #{@bar}" end get '/' expect(last_response.body).to eql 'first second' end it 'adds a before filter to current and child namespaces only' do subject.get '/' do "root - #{@foo if @foo}" end subject.namespace :blah do before { @foo = 'foo' } get '/' do "blah - #{@foo}" end namespace :bar do get '/' do "blah - bar - #{@foo}" end end end get '/' expect(last_response.body).to eql 'root - ' get '/blah' expect(last_response.body).to eql 'blah - foo' get '/blah/bar' expect(last_response.body).to eql 'blah - bar - foo' end it 'adds a after_validation filter' do subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" } subject.after_validation { @bar = 'second' } subject.params do requires :id, type: Integer end subject.get '/' do "#{@foo} #{@bar}" end get '/', id: '32' expect(last_response.body).to eql "first 32:#{integer_class_name} second" end it 'adds a after filter' do m = double('after mock') subject.after { m.do_something! } subject.after { m.do_something! } subject.get '/' do @var ||= 'default' end expect(m).to receive(:do_something!).twice get '/' expect(last_response.body).to eql 'default' end it 'calls all filters when validation passes' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end expect(a).to receive(:do_something!).once expect(b).to receive(:do_something!).once expect(c).to receive(:do_something!).once expect(d).to receive(:do_something!).once get '/123' expect(last_response).to be_successful expect(last_response.body).to eql 'got it' end it 'calls only before filters when validation fails' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer, values: [1, 2, 3] end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end expect(a).to receive(:do_something!).once expect(b).to receive(:do_something!).once expect(c).to receive(:do_something!).exactly(0).times expect(d).to receive(:do_something!).exactly(0).times get '/4' expect(last_response).to be_bad_request expect(last_response.body).to eql 'id does not have a valid value' end it 'calls filters in the correct order' do i = 0 a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.here(i += 1) } before_validation { b.here(i += 1) } after_validation { c.here(i += 1) } after { d.here(i += 1) } get do 'got it' end end expect(a).to receive(:here).with(1).once expect(b).to receive(:here).with(2).once expect(c).to receive(:here).with(3).once expect(d).to receive(:here).with(4).once get '/123' expect(last_response).to be_successful expect(last_response.body).to eql 'got it' end end context 'format' do before do subject.get('/foo') { 'bar' } end it 'sets content type for txt format' do get '/foo' expect(last_response.content_type).to eq('text/plain') end it 'does not set Cache-Control' do get '/foo' expect(last_response.headers[Rack::CACHE_CONTROL]).to be_nil end it 'sets content type for xml' do get '/foo.xml' expect(last_response.content_type).to eq('application/xml') end it 'sets content type for json' do get '/foo.json' expect(last_response.content_type).to eq('application/json') end it 'sets content type for serializable hash format' do get '/foo.serializable_hash' expect(last_response.content_type).to eq('application/json') end it 'sets content type for binary format' do get '/foo.binary' expect(last_response.content_type).to eq('application/octet-stream') end it 'returns raw data when content type binary' do image_filename = 'grape.png' file = File.binread(image_filename) subject.format :binary subject.get('/binary_file') { File.binread(image_filename) } get '/binary_file' expect(last_response.content_type).to eq('application/octet-stream') expect(last_response.body).to eq(file) end it 'returns the content of the file with file' do file_content = 'This is some file content' test_file = Tempfile.new('test') test_file.write file_content test_file.rewind subject.get('/file') { stream test_file } get '/file' expect(last_response.content_length).to eq(25) expect(last_response.content_type).to eq('text/plain') expect(last_response.body).to eq(file_content) end it 'streams the content of the file with stream' do test_stream = Enumerator.new do |blk| blk.yield 'This is some' blk.yield ' file content' end subject.use Gem::Version.new(Rack.release) < Gem::Version.new('3') ? Rack::Chunked : ChunkedResponse subject.get('/stream') { stream test_stream } get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', Rack::SERVER_PROTOCOL => 'HTTP/1.1' expect(last_response.content_type).to eq('text/plain') expect(last_response.content_length).to be_nil expect(last_response.headers[Rack::CACHE_CONTROL]).to eq('no-cache') expect(last_response.headers['Transfer-Encoding']).to eq('chunked') expect(last_response.body).to eq("c\r\nThis is some\r\nd\r\n file content\r\n0\r\n\r\n") end it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' expect(last_response.content_type).to eql 'text/plain' end it 'sets content type for json error' do subject.format :json subject.get('/error') { error!('error in json', 500) } get '/error.json' expect(last_response).to be_server_error expect(last_response.content_type).to eql 'application/json' end it 'sets content type for xml error' do subject.format :xml subject.get('/error') { error!('error in xml', 500) } get '/error' expect(last_response).to be_server_error expect(last_response.content_type).to eql 'application/xml' end it 'includes extension in format' do subject.get(':id') { params[:format] } get '/baz.bar' expect(last_response).to be_successful expect(last_response.body).to eq 'bar' end it 'does not include extension in id' do subject.format :json subject.get(':id') { params } get '/baz.bar' expect(last_response).to be_not_found end context 'with a custom content_type' do before do subject.content_type :custom, 'application/custom' subject.formatter :custom, ->(_object, _env) { 'custom' } subject.get('/custom') { 'bar' } subject.get('/error') { error!('error in custom', 500) } end it 'sets content type' do get '/custom.custom' expect(last_response.content_type).to eql 'application/custom' end it 'sets content type for error' do get '/error.custom' expect(last_response.content_type).to eql 'application/custom' end end context 'env["api.format"]' do before do ct = content_type subject.post 'attachment' do filename = params[:file][:filename] content_type ct env[Grape::Env::API_FORMAT] = :binary # there's no formatter for :binary, data will be returned "as is" header 'Content-Disposition', "attachment; filename*=UTF-8''#{CGI.escape(filename)}" params[:file][:tempfile].read end end context 'when image/png' do let(:content_type) { 'image/png' } %w[/attachment.png attachment].each do |url| it "uploads and downloads a PNG file via #{url}" do image_filename = 'grape.png' post url, file: Rack::Test::UploadedFile.new(image_filename, content_type, true) expect(last_response).to be_created expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''grape.png") File.open(image_filename, 'rb') do |io| expect(last_response.body).to eq io.read end end end end context 'when ruby file' do let(:content_type) { 'application/x-ruby' } it 'uploads and downloads a Ruby file' do filename = __FILE__ post '/attachment.rb', file: Rack::Test::UploadedFile.new(filename, content_type, true) expect(last_response).to be_created expect(last_response.content_type).to eq(content_type) expect(last_response.headers['Content-Disposition']).to eq("attachment; filename*=UTF-8''api_spec.rb") File.open(filename, 'rb') do |io| expect(last_response.body).to eq io.read end end end end end context 'custom middleware' do let(:phony_middleware) do Class.new do def initialize(app, *args) @args = args @app = app @block = block_given? || nil end def call(env) env['phony.args'] ||= [] env['phony.args'] << @args env['phony.block'] = true if @block @app.call(env) end end end describe '.middleware' do it 'includes middleware arguments from settings' do subject.use phony_middleware, 'abc', 123 expect(subject.middleware).to eql [[:use, phony_middleware, 'abc', 123]] end it 'includes all middleware from stacked settings' do subject.use phony_middleware, 123 subject.use phony_middleware, 'abc' subject.use phony_middleware, 'foo' expect(subject.middleware).to eql [ [:use, phony_middleware, 123], [:use, phony_middleware, 'abc'], [:use, phony_middleware, 'foo'] ] end end describe '.use' do it 'adds middleware' do subject.use phony_middleware, 123 expect(subject.middleware).to eql [[:use, phony_middleware, 123]] end it 'does not show up outside the namespace' do example = self inner_middleware = nil subject.use phony_middleware, 123 subject.namespace :awesome do use example.phony_middleware, 'abc' inner_middleware = middleware end expect(subject.middleware).to eql [[:use, phony_middleware, 123]] expect(inner_middleware).to eql [[:use, phony_middleware, 123], [:use, phony_middleware, 'abc']] end it 'calls the middleware' do subject.use phony_middleware, 'hello' subject.get '/' do env['phony.args'].first.first end get '/' expect(last_response.body).to eql 'hello' end it 'adds a block if one is given' do block = -> {} subject.use phony_middleware, &block expect(subject.middleware).to eql [[:use, phony_middleware, block]] end it 'uses a block if one is given' do block = -> {} subject.use phony_middleware, &block subject.get '/' do env['phony.block'].inspect end get '/' expect(last_response.body).to eq('true') end it 'does not destroy the middleware settings on multiple runs' do block = -> {} subject.use phony_middleware, &block subject.get '/' do env['phony.block'].inspect end 2.times do get '/' expect(last_response.body).to eq('true') end end it 'mounts behind error middleware' do m = Class.new(Grape::Middleware::Base) do def before throw :error, message: 'Caught in the Net', status: 400 end end subject.use m subject.get '/' do end get '/' expect(last_response).to be_bad_request expect(last_response.body).to eq('Caught in the Net') end context 'when middleware initialize as keywords' do let(:middleware_with_keywords) do Class.new do def initialize(app, keyword:) @app = app @keyword = keyword end def call(env) env['middleware_with_keywords'] = @keyword @app.call(env) end end end before do subject.use middleware_with_keywords, keyword: 'hello' subject.get '/' do env['middleware_with_keywords'] end get '/' end it 'returns the middleware value' do expect(last_response.body).to eq('hello') end end end describe '.insert_before' do it 'runs before a given middleware' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use phony_middleware, 'hello' subject.insert_before phony_middleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'bye hello' end end describe '.insert_after' do it 'runs after a given middleware' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use phony_middleware, 'hello' subject.insert_after phony_middleware, m, message: 'bye' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'hello bye' end end describe '.insert' do it 'inserts middleware in a specific location in the stack' do m = Class.new(Grape::Middleware::Base) do def call(env) env['phony.args'] ||= [] env['phony.args'] << @options[:message] @app.call(env) end end subject.use phony_middleware, 'bye' subject.insert 0, m, message: 'good' subject.insert 0, m, message: 'hello' subject.get '/' do env['phony.args'].join(' ') end get '/' expect(last_response.body).to eql 'hello good bye' end end end describe '.http_basic' do it 'protects any resources on the same scope' do subject.http_basic do |u, _p| u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello' expect(last_response).to be_unauthorized get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response).to be_successful end it 'is scopable' do subject.get(:hello) { 'Hello, world.' } subject.namespace :admin do http_basic do |u, _p| u == 'allow' end get(:hello) { 'Hello, world.' } end get '/hello' expect(last_response).to be_successful get '/admin/hello' expect(last_response).to be_unauthorized end it 'is callable via .auth as well' do subject.auth :http_basic do |u, _p| u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello' expect(last_response).to be_unauthorized get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response).to be_successful end it 'has access to the current endpoint' do basic_auth_context = nil subject.http_basic do |u, _p| basic_auth_context = self u == 'allow' end subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(basic_auth_context).to be_a(Grape::Endpoint) end it 'has access to helper methods' do subject.helpers do def authorize?(user, password) user == 'allow' && password == 'whatever' end end subject.http_basic do |u, p| authorize?(u, p) end subject.get(:hello) { 'Hello, world.' } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response).to be_successful get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever') expect(last_response).to be_unauthorized end it 'can set instance variables accessible to routes' do subject.http_basic do |u, _p| @hello = 'Hello, world.' u == 'allow' end subject.get(:hello) { @hello } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') expect(last_response).to be_successful expect(last_response.body).to eql 'Hello, world.' end end describe '.logger' do it 'returns an instance of Logger class by default' do expect(subject.logger.class).to eql Logger end context 'with a custom logger' do subject do Class.new(described_class) do def self.io @io ||= StringIO.new end logger Logger.new(io) end end it 'exposes its interaface' do message = 'this will be logged' subject.logger.info message expect(subject.io.string).to include(message) end end end describe '.helpers' do it 'is accessible from the endpoint' do subject.helpers do def hello 'Hello, world.' end end subject.get '/howdy' do hello end get '/howdy' expect(last_response.body).to eql 'Hello, world.' end it 'is scopable' do subject.helpers do def generic 'always there' end end subject.namespace :admin do helpers do def secret 'only in admin' end end get '/secret' do [generic, secret].join ':' end end subject.get '/generic' do [generic, respond_to?(:secret)].join ':' end get '/generic' expect(last_response.body).to eql 'always there:false' get '/admin/secret' expect(last_response.body).to eql 'always there:only in admin' end it 'is reopenable' do subject.helpers do def one 1 end end subject.helpers do def two 2 end end subject.get 'howdy' do [one, two] end expect { get '/howdy' }.not_to raise_error end it 'allows for modules' do mod = Module.new do def hello 'Hello, world.' end end subject.helpers mod subject.get '/howdy' do hello end get '/howdy' expect(last_response.body).to eql 'Hello, world.' end it 'allows multiple calls with modules and blocks' do subject.helpers Module.new do def one 1 end end subject.helpers Module.new do def two 2 end end subject.helpers do def three 3 end end subject.get 'howdy' do [one, two, three] end expect { get '/howdy' }.not_to raise_error end end describe '.scope' do # TODO: refactor this to not be tied to versioning. How about a generic # .setting macro? it 'scopes the various settings' do subject.prefix 'new' subject.scope :legacy do prefix 'legacy' get '/abc' do 'abc' end end subject.get '/def' do 'def' end get '/new/abc' expect(last_response).to be_not_found get '/legacy/abc' expect(last_response).to be_successful get '/legacy/def' expect(last_response).to be_not_found get '/new/def' expect(last_response).to be_successful end end describe 'lifecycle' do let!(:lifecycle) { [] } let!(:standard_cycle) do %i[before before_validation after_validation api_call after finally] end let!(:validation_error) do %i[before before_validation finally] end let!(:errored_cycle) do %i[before before_validation after_validation api_call finally] end before do current_cycle = lifecycle subject.before do current_cycle << :before end subject.before_validation do current_cycle << :before_validation end subject.after_validation do current_cycle << :after_validation end subject.after do current_cycle << :after end subject.finally do current_cycle << :finally end end context 'when the api_call succeeds' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call end end it 'follows the standard life_cycle' do get '/api_call' expect(lifecycle).to eq standard_cycle end end context 'when the api_call has a controlled error' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call error!(:some_error) end end it 'follows the errored life_cycle (skips after)' do get '/api_call' expect(lifecycle).to eq errored_cycle end end context 'when the api_call has an exception' do before do current_cycle = lifecycle subject.get 'api_call' do current_cycle << :api_call raise StandardError end end it 'follows the errored life_cycle (skips after)' do expect { get '/api_call' }.to raise_error(StandardError) expect(lifecycle).to eq errored_cycle end end context 'when the api_call fails validation' do before do current_cycle = lifecycle subject.params do requires :some_param, type: String end subject.get 'api_call' do current_cycle << :api_call end end it 'follows the failed_validation cycle (skips after_validation, api_call & after)' do get '/api_call' expect(lifecycle).to eq validation_error end end end describe '.finally' do let!(:code) { { has_executed: false } } let(:block_to_run) do code_to_execute = code proc do code_to_execute[:has_executed] = true end end context 'when the ensure block has no exceptions' do before { subject.finally(&block_to_run) } context 'when no API call is made' do it 'has not executed the ensure code' do expect(code[:has_executed]).to be false end end context 'when no errors occurs' do before do subject.get '/no_exceptions' do 'success' end end it 'executes the ensure code' do get '/no_exceptions' expect(last_response.body).to eq 'success' expect(code[:has_executed]).to be true end context 'with a helper' do let(:block_to_run) do code_to_execute = code proc do code_to_execute[:value] = some_helper end end before do subject.helpers do def some_helper 'some_value' end end subject.get '/with_helpers' do 'success' end end it 'has access to the helper' do get '/with_helpers' expect(code[:value]).to eq 'some_value' end end end context 'when an unhandled occurs inside the API call' do before do subject.get '/unhandled_exception' do raise StandardError end end it 'executes the ensure code' do expect { get '/unhandled_exception' }.to raise_error StandardError expect(code[:has_executed]).to be true end end context 'when a handled error occurs inside the API call' do before do subject.rescue_from(StandardError) { error! 'handled' } subject.get '/handled_exception' do raise StandardError end end it 'executes the ensure code' do get '/handled_exception' expect(code[:has_executed]).to be true expect(last_response.body).to eq 'handled' end end end end describe '.rescue_from' do it 'does not rescue errors when rescue_from is not set' do subject.get '/exception' do raise 'rain!' end expect { get '/exception' }.to raise_error(RuntimeError, 'rain!') end it 'uses custom helpers defined by using #helpers method' do subject.helpers do def custom_error!(name) error! "hello #{name}" end end subject.rescue_from(ArgumentError) { custom_error! :bob } subject.get '/custom_error' do raise ArgumentError end get '/custom_error' expect(last_response.body).to eq 'hello bob' end context 'with multiple apis' do let(:a) do Class.new(described_class) do namespace :a do helpers do def foo error!('foo', 401) end end rescue_from(:all) { foo } get { raise 'boo' } end end end let(:b) do Class.new(described_class) do namespace :b do helpers do def foo error!('bar', 401) end end rescue_from(:all) { foo } get { raise 'boo' } end end end before do subject.mount a subject.mount b end it 'avoids polluting global namespace' do get '/a' expect(last_response.body).to eq('foo') get '/b' expect(last_response.body).to eq('bar') get '/a' expect(last_response.body).to eq('foo') end end it 'rescues all errors if rescue_from :all is called' do subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response).to be_server_error expect(last_response.body).to eq 'rain!' end it 'rescues all errors with a json formatter' do subject.format :json subject.default_format :json subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response).to be_server_error expect(last_response.body).to eq({ error: 'rain!' }.to_json) end it 'rescues only certain errors if rescue_from is called with specific errors' do subject.rescue_from ArgumentError subject.get('/rescued') { raise ArgumentError } subject.get('/unrescued') { raise 'beefcake' } get '/rescued' expect(last_response).to be_server_error expect { get '/unrescued' }.to raise_error(RuntimeError, 'beefcake') end it 'mimics default ruby "rescue" handler' do # The exception is matched to the rescue starting at the top, and matches only once subject.rescue_from ArgumentError do |e| error!(e, 402) end subject.rescue_from StandardError do |e| error!(e, 401) end subject.get('/child_of_standard_error') { raise ArgumentError } subject.get('/standard_error') { raise StandardError } get '/child_of_standard_error' expect(last_response.status).to be 402 get '/standard_error' expect(last_response).to be_unauthorized end context 'CustomError subclass of Grape::Exceptions::Base' do before do stub_const('ApiSpec::CustomError', Class.new(Grape::Exceptions::Base)) end it 'does not re-raise exceptions of type Grape::Exceptions::Base' do subject.get('/custom_exception') { raise ApiSpec::CustomError } expect { get '/custom_exception' }.not_to raise_error end it 'rescues custom grape exceptions' do subject.rescue_from ApiSpec::CustomError do |e| error!('New Error', e.status) end subject.get '/custom_error' do raise ApiSpec::CustomError.new(status: 400, message: 'Custom Error') end get '/custom_error' expect(last_response).to be_bad_request expect(last_response.body).to eq('New Error') end end it 'can rescue exceptions raised in the formatter' do formatter = double(:formatter) allow(formatter).to receive(:call) { raise StandardError } allow(Grape::Formatter).to receive(:formatter_for) { formatter } subject.rescue_from :all do |_e| error!('Formatter Error', 500) end subject.get('/formatter_exception') { 'Hello world' } get '/formatter_exception' expect(last_response).to be_server_error expect(last_response.body).to eq('Formatter Error') end context 'when rescue_from block returns an invalid response' do it 'returns a formatted response' do subject.rescue_from(:all) { 'error' } subject.get('/') { raise } get '/' expect(last_response).to be_server_error expect(last_response.body).to eql 'Invalid response' end end end describe '.rescue_from klass, block' do it 'rescues Exception' do subject.rescue_from RuntimeError do |e| error!("rescued from #{e.message}", 202) end subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response).to be_accepted expect(last_response.body).to eq('rescued from rain!') end context 'custom errors' do before do stub_const('ConnectionError', Class.new(RuntimeError)) stub_const('DatabaseError', Class.new(RuntimeError)) stub_const('CommunicationError', Class.new(StandardError)) end it 'rescues an error via rescue_from :all' do subject.rescue_from :all do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues a specific error' do subject.rescue_from ConnectionError do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues a subclass of an error by default' do subject.rescue_from RuntimeError do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') end it 'rescues multiple specific errors' do subject.rescue_from ConnectionError do |e| error!("rescued from #{e.class.name}", 500) end subject.rescue_from DatabaseError do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/connection' do raise ConnectionError end subject.get '/database' do raise DatabaseError end get '/connection' expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from ConnectionError') get '/database' expect(last_response).to be_server_error expect(last_response.body).to eq('rescued from DatabaseError') end it 'does not rescue a different error' do subject.rescue_from RuntimeError do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise CommunicationError end expect { get '/uncaught' }.to raise_error(CommunicationError) end end end describe '.rescue_from klass, lambda' do it 'rescues an error with the lambda' do subject.rescue_from ArgumentError, lambda { error!('rescued with a lambda', 400) } subject.get('/rescue_lambda') { raise ArgumentError } get '/rescue_lambda' expect(last_response).to be_bad_request expect(last_response.body).to eq('rescued with a lambda') end it 'can execute the lambda with an argument' do subject.rescue_from ArgumentError, lambda { |e| error!(e.message, 400) } subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' } get '/rescue_lambda' expect(last_response).to be_bad_request expect(last_response.body).to eq('lambda takes an argument') end end describe '.rescue_from klass, with: :method_name' do it 'rescues an error with the specified method name' do subject.helpers do def rescue_arg_error error!('500 ArgumentError', 500) end def rescue_no_method_error error!('500 NoMethodError', 500) end end subject.rescue_from ArgumentError, with: :rescue_arg_error subject.rescue_from NoMethodError, with: :rescue_no_method_error subject.get('/rescue_arg_error') { raise ArgumentError } subject.get('/rescue_no_method_error') { raise NoMethodError } get '/rescue_arg_error' expect(last_response).to be_server_error expect(last_response.body).to eq('500 ArgumentError') get '/rescue_no_method_error' expect(last_response).to be_server_error expect(last_response.body).to eq('500 NoMethodError') end it 'aborts if the specified method name does not exist' do subject.rescue_from :all, with: :not_exist_method subject.get('/rescue_method') { raise StandardError } expect { get '/rescue_method' }.to raise_error(NameError, /^undefined method [`']not_exist_method'/) end it 'correctly chooses exception handler if :all handler is specified' do subject.helpers do def rescue_arg_error error!('500 ArgumentError', 500) end def rescue_all_errors error!('500 AnotherError', 500) end end subject.rescue_from ArgumentError, with: :rescue_arg_error subject.rescue_from :all, with: :rescue_all_errors subject.get('/argument_error') { raise ArgumentError } subject.get('/another_error') { raise NoMethodError } get '/argument_error' expect(last_response).to be_server_error expect(last_response.body).to eq('500 ArgumentError') get '/another_error' expect(last_response).to be_server_error expect(last_response.body).to eq('500 AnotherError') end end describe '.rescue_from klass, rescue_subclasses: boolean' do before do parent_error = Class.new(StandardError) stub_const('ApiSpec::APIErrors::ParentError', parent_error) stub_const('ApiSpec::APIErrors::ChildError', Class.new(parent_error)) end it 'rescues error as well as subclass errors with rescue_subclasses option set' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: true do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError end subject.get '/caught_parent' do raise ApiSpec::APIErrors::ParentError end subject.get '/uncaught_parent' do raise StandardError end get '/caught_child' expect(last_response).to be_server_error get '/caught_parent' expect(last_response).to be_server_error expect { get '/uncaught_parent' }.to raise_error(StandardError) end it 'sets rescue_subclasses to true by default' do subject.rescue_from ApiSpec::APIErrors::ParentError do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise ApiSpec::APIErrors::ChildError end get '/caught_child' expect(last_response).to be_server_error end it 'does not rescue child errors if rescue_subclasses is false' do subject.rescue_from ApiSpec::APIErrors::ParentError, rescue_subclasses: false do |e| error!("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise ApiSpec::APIErrors::ChildError end expect { get '/uncaught' }.to raise_error(ApiSpec::APIErrors::ChildError) end end describe '.rescue_from :grape_exceptions' do before do subject.rescue_from :grape_exceptions end let(:grape_exception) do Grape::Exceptions::Base.new(status: 400, message: 'Grape Error') end it 'rescues grape exceptions' do exception = grape_exception subject.get('/grape_exception') { raise exception } get '/grape_exception' expect(last_response.status).to eq(exception.status) expect(last_response.body).to eq(exception.message) end it 'rescues grape exceptions with a user-defined handler' do subject.rescue_from grape_exception.class do |_error| error!('Redefined Error', 403) end exception = grape_exception subject.get('/grape_exception') { raise exception } get '/grape_exception' expect(last_response).to be_forbidden expect(last_response.body).to eq('Redefined Error') end end describe '.error_format' do it 'rescues all errors and return :txt' do subject.rescue_from :all subject.format :txt subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eql 'rain!' end it 'rescues all errors and return :txt with backtrace' do subject.rescue_from :all, backtrace: true subject.format :txt subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body.start_with?("rain!\r\n")).to be true end it 'rescues all errors with a default formatter' do subject.default_format :foo subject.content_type :foo, 'text/foo' subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception.foo' expect(last_response.body).to start_with 'rain!' end it 'defaults the error formatter to format' do subject.format :json subject.rescue_from :all subject.content_type :json, 'application/json' subject.content_type :foo, 'text/foo' subject.get '/exception' do raise 'rain!' end get '/exception.json' expect(last_response.body).to eq('{"error":"rain!"}') get '/exception.foo' expect(last_response.body).to eq('{"error":"rain!"}') end context 'class' do let(:custom_error_formatter) do Class.new do def self.call(message, _backtrace, _options, _env, _original_exception) "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, custom_error_formatter subject.get('/exception') { raise 'rain!' } get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end it 'returns a custom error format (using keyword :with)' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, with: custom_error_formatter subject.get('/exception') { raise 'rain!' } get '/exception' expect(last_response.body).to eq('message: rain! @backtrace') end it 'returns a modified error with a custom error format' do subject.rescue_from :all, backtrace: true do |e| error!('raining dogs and cats', 418, {}, e.backtrace, e) end subject.error_formatter :txt, with: custom_error_formatter subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eq('message: raining dogs and cats @backtrace') end end it 'rescues all errors and return :json' do subject.rescue_from :all subject.format :json subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response.body).to eql '{"error":"rain!"}' end it 'rescues all errors and return :json with backtrace' do subject.rescue_from :all, backtrace: true subject.format :json subject.get '/exception' do raise 'rain!' end get '/exception' json = Grape::Json.load(last_response.body) expect(json['error']).to eql 'rain!' expect(json['backtrace'].length).to be > 0 end it 'rescues error! and return txt' do subject.format :txt subject.get '/error' do error!('Access Denied', 401) end get '/error' expect(last_response.body).to eql 'Access Denied' end context 'with json format' do shared_examples_for 'a json format api' do |error_message| subject { JSON.parse(last_response.body) } before { get '/error' } let(:app) do Class.new(Grape::API) do format :json get('/error') { error!(error_message, 401) } end end context "when error! called with #{error_message.class.name}" do it { is_expected.to eq('error' => 'failure') } end end it_behaves_like 'a json format api', 'failure' it_behaves_like 'a json format api', :failure it_behaves_like 'a json format api', { error: :failure } end context 'when rescue_from enables backtrace without original exception' do let(:app) do response_type = response_format Class.new(Grape::API) do format response_type rescue_from :all, backtrace: true, original_exception: false do |e| error!('raining dogs and cats!', 418, {}, e.backtrace, e) end get '/exception' do raise 'rain!' end end end before do get '/exception' end context 'with json response type format' do subject { JSON.parse(last_response.body) } let(:response_format) { :json } it { is_expected.to include('error' => a_kind_of(String), 'backtrace' => a_kind_of(Array)) } it { is_expected.not_to include('original_exception') } end context 'with txt response type format' do subject { last_response.body } let(:response_format) { :txt } it { is_expected.to include('backtrace') } it { is_expected.not_to include('original_exception') } end context 'with xml response type format' do subject { Grape::Xml.parse(last_response.body)['error'] } let(:response_format) { :xml } it { is_expected.to have_key('backtrace') } it { is_expected.not_to have_key('original-exception') } end end context 'when rescue_from enables original exception without backtrace' do let(:app) do response_type = response_format Class.new(Grape::API) do format response_type rescue_from :all, backtrace: false, original_exception: true do |e| error!('raining dogs and cats!', 418, {}, e.backtrace, e) end get '/exception' do raise 'rain!' end end end before do get '/exception' end context 'with json response type format' do subject { JSON.parse(last_response.body) } let(:response_format) { :json } it { is_expected.to include('error' => a_kind_of(String), 'original_exception' => a_kind_of(String)) } it { is_expected.not_to include('backtrace') } end context 'with txt response type format' do subject { last_response.body } let(:response_format) { :txt } it { is_expected.to include('original exception') } it { is_expected.not_to include('backtrace') } end context 'with xml response type format' do subject { Grape::Xml.parse(last_response.body)['error'] } let(:response_format) { :xml } it { is_expected.to have_key('original-exception') } it { is_expected.not_to have_key('backtrace') } end end context 'when rescue_from include backtrace and original exception' do let(:app) do response_type = response_format Class.new(Grape::API) do format response_type rescue_from :all, backtrace: true, original_exception: true do |e| error!('raining dogs and cats!', 418, {}, e.backtrace, e) end get '/exception' do raise 'rain!' end end end before do get '/exception' end context 'with json response type format' do subject { JSON.parse(last_response.body) } let(:response_format) { :json } it { is_expected.to include('error' => a_kind_of(String), 'backtrace' => a_kind_of(Array), 'original_exception' => a_kind_of(String)) } end context 'with txt response type format' do subject { last_response.body } let(:response_format) { :txt } it { is_expected.to include('backtrace', 'original exception') } end context 'with xml response type format' do subject { Grape::Xml.parse(last_response.body)['error'] } let(:response_format) { :xml } it { is_expected.to have_key('backtrace') & have_key('original-exception') } end end context 'when rescue validation errors include backtrace and original exception' do let(:app) do response_type = response_format Class.new(Grape::API) do format response_type rescue_from Grape::Exceptions::ValidationErrors, backtrace: true, original_exception: true do |e| error!(e, 418, {}, e.backtrace, e) end params do requires :weather end get '/forecast' do 'sunny' end end end before do get '/forecast' end context 'with json response type format' do subject { JSON.parse(last_response.body) } let(:response_format) { :json } it 'does not include backtrace or original exception' do expect(subject).to match([{ 'messages' => ['is missing'], 'params' => ['weather'] }]) end end context 'with txt response type format' do subject { last_response.body } let(:response_format) { :txt } it { is_expected.to include('backtrace', 'original exception') } end context 'with xml response type format' do subject { Grape::Xml.parse(last_response.body)['error'] } let(:response_format) { :xml } it { is_expected.to have_key('backtrace') & have_key('original-exception') } end end end describe '.content_type' do it 'sets additional content-type' do subject.content_type :xls, 'application/vnd.ms-excel' subject.get :excel do 'some binary content' end get '/excel.xls' expect(last_response.content_type).to eq('application/vnd.ms-excel') end it 'allows to override content-type' do subject.get :content do content_type 'text/javascript' 'var x = 1;' end get '/content' expect(last_response.content_type).to eq('text/javascript') end it 'removes existing content types' do subject.content_type :xls, 'application/vnd.ms-excel' subject.get :excel do 'some binary content' end get '/excel.json' expect(last_response.status).to eq(406) expect(last_response.body).to eq(Rack::Utils.escape_html("The requested format 'txt' is not supported.")) end end describe '.formatter' do context 'multiple formatters' do before do subject.formatter :json, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } subject.formatter :txt, ->(object, _env) { "custom_formatter: #{object[:some]}" } subject.get :simple do { some: 'hash' } end end it 'sets one formatter' do get '/simple.json' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end it 'sets another formatter' do get '/simple.txt' expect(last_response.body).to eql 'custom_formatter: hash' end end context 'custom formatter' do before do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, ->(object, _env) { "{\"custom_formatter\":\"#{object[:some]}\"}" } subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end context 'custom formatter class' do let(:custom_formatter) do Module.new do def self.call(object, _env) "{\"custom_formatter\":\"#{object[:some]}\"}" end end end before do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, custom_formatter subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' expect(last_response.body).to eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' expect(last_response.body).to eql '{"custom_formatter":"hash"}' end end end describe '.parser' do it 'parses data in format requested by content-type' do subject.format :json subject.post '/data' do { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(last_response.body).to eq('{"x":42}') end context 'lambda parser' do before do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, ->(object, _env) { { object.to_sym => object.to_s.reverse } } subject.put :simple do params[:simple] end end ['text/custom', 'text/custom; charset=UTF-8'].each do |content_type| it "uses parser for #{content_type}" do put '/simple', 'simple', 'CONTENT_TYPE' => content_type expect(last_response).to be_successful expect(last_response.body).to eql 'elpmis' end end end context 'custom parser class' do let(:custom_parser) do Module.new do def self.call(object, _env) { object.to_sym => object.to_s.reverse } end end end before do subject.content_type :txt, 'text/plain' subject.content_type :custom, 'text/custom' subject.parser :custom, custom_parser subject.put :simple do params[:simple] end end it 'uses custom parser' do put '/simple', 'simple', 'CONTENT_TYPE' => 'text/custom' expect(last_response).to be_successful expect(last_response.body).to eql 'elpmis' end end if Object.const_defined? :MultiXml context 'multi_xml' do it "doesn't parse yaml" do subject.put :yaml do params[:tag] end put '/yaml', 'a123', 'CONTENT_TYPE' => 'application/xml' expect(last_response).to be_bad_request expect(last_response.body).to eql 'Disallowed type attribute: "symbol"' end end else context 'default xml parser' do it 'parses symbols' do subject.put :yaml do params[:tag] end body = 'a123' put '/yaml', body, 'CONTENT_TYPE' => 'application/xml' expect(last_response).to be_successful expect(last_response.body).to eq(Grape::Xml.parse(body)['tag'].to_s) end end end context 'none parser class' do before do subject.parser :json, nil subject.put 'data' do "body: #{env[Grape::Env::API_REQUEST_BODY]}" end end it 'does not parse data' do put '/data', 'not valid json', 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_successful expect(last_response.body).to eq('body: not valid json') end end end describe '.default_format' do before do subject.format :json subject.default_format :json end it 'returns data in default format' do subject.get '/data' do { x: 42 } end get '/data' expect(last_response).to be_successful expect(last_response.body).to eq('{"x":42}') end it 'parses data in default format' do subject.post '/data' do { x: params[:x] } end post '/data', '{"x":42}', 'CONTENT_TYPE' => '' expect(last_response).to be_created expect(last_response.body).to eq('{"x":42}') end end describe '.default_error_status' do it 'allows setting default_error_status' do subject.rescue_from :all subject.default_error_status 200 subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response).to be_successful end it 'has a default error status' do subject.rescue_from :all subject.get '/exception' do raise 'rain!' end get '/exception' expect(last_response).to be_server_error end it 'uses the default error status in error!' do subject.rescue_from :all subject.default_error_status 400 subject.get '/exception' do error! 'rain!' end get '/exception' expect(last_response).to be_bad_request end end context 'routes' do describe 'empty api structure' do it 'returns an empty array of routes' do expect(subject.routes).to eq([]) end end describe 'single method api structure' do before do subject.get :ping do 'pong' end end it 'returns one route' do expect(subject.routes.size).to eq(1) route = subject.routes[0] expect(route.version).to be_nil expect(route.path).to eq('/ping(.:format)') expect(route.request_method).to eq(Rack::GET) end end describe 'api structure with two versions and a namespace' do before do subject.version 'v1', using: :path subject.get 'version' do api.version end # version v2 subject.version 'v2', using: :path subject.prefix 'p' subject.namespace 'n1' do namespace 'n2' do get 'version' do api.version end end end end it 'returns the latest version set' do expect(subject.version).to eq('v2') end it 'returns versions' do expect(subject.versions).to eq(%w[v1 v2]) end it 'sets route paths' do expect(subject.routes.size).to be >= 2 expect(subject.routes[0].path).to eq('/:version/version(.:format)') expect(subject.routes[1].path).to eq('/p/:version/n1/n2/version(.:format)') end it 'sets route versions' do expect(subject.routes[0].version).to eq('v1') expect(subject.routes[1].version).to eq('v2') end it 'sets a nested namespace' do expect(subject.routes[1].namespace).to eq('/n1/n2') end it 'sets prefix' do expect(subject.routes[1].prefix).to eq('p') end end describe 'api structure with additional parameters' do before do subject.params do requires :token, desc: 'a token' optional :limit, desc: 'the limit' end subject.get 'split/:string' do params[:string].split(params[:token], (params[:limit] || 0).to_i) end end it 'splits a string' do get '/split/a,b,c.json', token: ',' expect(last_response.body).to eq('["a","b","c"]') end it 'splits a string with limit' do get '/split/a,b,c.json', token: ',', limit: '2' expect(last_response.body).to eq('["a","b,c"]') end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'string' => '', 'token' => { required: true, desc: 'a token' }, 'limit' => { required: false, desc: 'the limit' } } } ] end end describe 'api structure with multiple apis' do before do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' end subject.get 'one' do end subject.params do requires :three, desc: 'a token' optional :four, desc: 'the limit' end subject.get 'two' do end end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'one' => { required: true, desc: 'a token' }, 'two' => { required: false, desc: 'the limit' } } }, { params: { 'three' => { required: true, desc: 'a token' }, 'four' => { required: false, desc: 'the limit' } } } ] end end describe 'api structure with an api without params' do before do subject.params do requires :one, desc: 'a token' optional :two, desc: 'the limit' end subject.get 'one' do end subject.get 'two' do end end it 'sets params' do expect(subject.routes.map do |route| { params: route.params } end).to eq [ { params: { 'one' => { required: true, desc: 'a token' }, 'two' => { required: false, desc: 'the limit' } } }, { params: {} } ] end end describe 'api with a custom route setting' do before do subject.route_setting :custom, key: 'value' subject.get 'one' end it 'exposed' do expect(subject.routes.count).to eq 1 route = subject.routes.first expect(route.settings[:custom]).to eq(key: 'value') end end describe 'status' do it 'can be set to arbitrary Integer value' do subject.get '/foo' do status 210 end get '/foo' expect(last_response.status).to eq 210 end it 'can be set with a status code symbol' do subject.get '/foo' do status :see_other end get '/foo' expect(last_response.status).to eq 303 end end end context 'desc' do it 'empty array of routes' do expect(subject.routes).to eq([]) end it 'empty array of routes' do subject.desc 'grape api' expect(subject.routes).to eq([]) end it 'describes a method' do subject.desc 'first method' subject.get :first expect(subject.routes.length).to eq(1) route = subject.routes.first expect(route.description).to eq('first method') expect(route.params).to eq({}) expect(route.options).to be_a(Hash) end it 'has params which does not include format and version as named captures' do subject.version :v1, using: :path subject.get :first param_keys = subject.routes.first.params.keys expect(param_keys).not_to include('format') expect(param_keys).not_to include('version') end it 'describes methods separately' do subject.desc 'first method' subject.get :first subject.desc 'second method' subject.get :second expect(subject.routes.count).to eq(2) expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'first method', params: {} }, { description: 'second method', params: {} } ] end it 'resets desc' do subject.desc 'first method' subject.get :first subject.get :second expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'first method', params: {} }, { description: nil, params: {} } ] end it 'namespaces and describe arbitrary parameters' do subject.namespace 'ns' do desc 'ns second', foo: 'bar' get 'second' end expect(subject.routes.map do |route| { description: route.description, foo: route.options[:foo], params: route.params } end).to eq [ { description: 'ns second', foo: 'bar', params: {} } ] end it 'includes detail' do subject.desc 'method', detail: 'method details' subject.get 'method' expect(subject.routes.map do |route| { description: route.description, detail: route.detail, params: route.params } end).to eq [ { description: 'method', detail: 'method details', params: {} } ] end it 'describes a method with parameters' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse' do params[:s].reverse end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } } ] end it 'does not inherit param descriptions in consequent namespaces' do subject.desc 'global description' subject.params do requires :param1 optional :param2 end subject.namespace 'ns1' do get {} end subject.params do optional :param2 end subject.namespace 'ns2' do get {} end routes_doc = subject.routes.map do |route| { description: route.description, params: route.params } end expect(routes_doc).to eq [ { description: 'global description', params: { 'param1' => { required: true }, 'param2' => { required: false } } }, { description: 'global description', params: { 'param2' => { required: false } } } ] end it 'merges the parameters of the namespace with the parameters of the method' do subject.desc 'namespace' subject.params do requires :ns_param, desc: 'namespace parameter' end subject.namespace 'ns' do desc 'method' params do optional :method_param, desc: 'method parameter' end get 'method' end routes_doc = subject.routes.map do |route| { description: route.description, params: route.params } end expect(routes_doc).to eq [ { description: 'method', params: { 'ns_param' => { required: true, desc: 'namespace parameter' }, 'method_param' => { required: false, desc: 'method parameter' } } } ] end it 'merges the parameters of nested namespaces' do subject.desc 'ns1' subject.params do optional :ns_param, desc: 'ns param 1' requires :ns1_param, desc: 'ns1 param' end subject.namespace 'ns1' do desc 'ns2' params do requires :ns_param, desc: 'ns param 2' requires :ns2_param, desc: 'ns2 param' end namespace 'ns2' do desc 'method' params do optional :method_param, desc: 'method param' end get 'method' end end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'method', params: { 'ns_param' => { required: true, desc: 'ns param 2' }, 'ns1_param' => { required: true, desc: 'ns1 param' }, 'ns2_param' => { required: true, desc: 'ns2 param' }, 'method_param' => { required: false, desc: 'method param' } } } ] end it 'groups nested params and prevents overwriting of params with same name in different groups' do subject.desc 'method' subject.params do group :group1, type: Array do optional :param1, desc: 'group1 param1 desc' requires :param2, desc: 'group1 param2 desc' end group :group2, type: Array do optional :param1, desc: 'group2 param1 desc' requires :param2, desc: 'group2 param2 desc' end end subject.get 'method' expect(subject.routes.map(&:params)).to eq [{ 'group1' => { required: true, type: 'Array' }, 'group1[param1]' => { required: false, desc: 'group1 param1 desc' }, 'group1[param2]' => { required: true, desc: 'group1 param2 desc' }, 'group2' => { required: true, type: 'Array' }, 'group2[param1]' => { required: false, desc: 'group2 param1 desc' }, 'group2[param2]' => { required: true, desc: 'group2 param2 desc' } }] end it 'uses full name of parameters in nested groups' do subject.desc 'nesting' subject.params do requires :root_param, desc: 'root param' group :nested, type: Array do requires :nested_param, desc: 'nested param' end end subject.get 'method' expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'nesting', params: { 'root_param' => { required: true, desc: 'root param' }, 'nested' => { required: true, type: 'Array' }, 'nested[nested_param]' => { required: true, desc: 'nested param' } } } ] end it 'allows to set the type attribute on :group element' do subject.params do group :foo, type: Array do optional :bar end end subject.get 'method' expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: nil, params: { 'foo' => { required: true, type: 'Array' }, 'foo[bar]' => { required: false } } } ] end it 'parses parameters when no description is given' do subject.params do requires :one_param, desc: 'one param' end subject.get 'method' expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: nil, params: { 'one_param' => { required: true, desc: 'one param' } } } ] end it 'does not symbolize params' do subject.desc 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } subject.get 'reverse/:s' do params[:s].reverse end expect(subject.routes.map do |route| { description: route.description, params: route.params } end).to eq [ { description: 'Reverses a string.', params: { 's' => { desc: 'string to reverse', type: 'string' } } } ] end end describe '.mount' do let(:mounted_app) { ->(_env) { [200, {}, ['MOUNTED']] } } context 'with a bare rack app' do before do subject.mount mounted_app => '/mounty' end it 'makes a bare Rack app available at the endpoint' do get '/mounty' expect(last_response.body).to eq('MOUNTED') end it 'anchors the routes, passing all subroutes to it' do get '/mounty/awesome' expect(last_response.body).to eq('MOUNTED') end it 'is able to cascade' do subject.mount lambda { |env| headers = {} headers['X-Cascade'] == 'pass' unless env[Rack::PATH_INFO].include?('boo') [200, headers, ['Farfegnugen']] } => '/' get '/boo' expect(last_response.body).to eq('Farfegnugen') get '/mounty' expect(last_response.body).to eq('MOUNTED') end end context 'without a hash' do it 'calls through setting the route to "/"' do subject.mount mounted_app get '/' expect(last_response.body).to eq('MOUNTED') end end context 'mounting an API' do it 'applies the settings of the mounting api' do subject.version 'v1', using: :path subject.namespace :cool do app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/awesome') do 'yo' end mount app end get '/v1/cool/awesome' expect(last_response.body).to eq('yo') end it 'applies the settings to nested mounted apis' do subject.version 'v1', using: :path subject.namespace :cool do inner_app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass inner_app.get('/awesome') do 'yo' end app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.mount inner_app mount app end get '/v1/cool/awesome' expect(last_response.body).to eq('yo') end context 'when some rescues are defined by mounted' do it 'inherits parent rescues' do subject.rescue_from :all do |e| error!("rescued from #{e.message}", 202) end app = Class.new(described_class) subject.namespace :mounted do app.rescue_from ArgumentError app.get('/fail') { raise 'doh!' } mount app end get '/mounted/fail' expect(last_response).to be_accepted expect(last_response.body).to eq('rescued from doh!') end it 'prefers rescues defined by mounted if they rescue similar error class' do subject.rescue_from StandardError do error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do error!('inner rescue') end app.get('/fail') { raise 'doh!' } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end it 'prefers rescues defined by mounted even if outer is more specific' do subject.rescue_from ArgumentError do error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from StandardError do error!('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end it 'prefers more specific rescues defined by mounted' do subject.rescue_from StandardError do error!('outer rescue') end app = Class.new(described_class) subject.namespace :mounted do rescue_from ArgumentError do error!('inner rescue') end app.get('/fail') { raise ArgumentError.new } mount app end get '/mounted/fail' expect(last_response.body).to eq('inner rescue') end end it 'collects the routes of the mounted api' do subject.namespace :cool do app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/awesome') {} app.post('/sauce') {} mount app end expect(subject.routes.size).to eq(2) expect(subject.routes.first.path).to match(%r{/cool/awesome}) expect(subject.routes.last.path).to match(%r{/cool/sauce}) end it 'mounts on a path' do subject.namespace :cool do app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get '/awesome' do 'sauce' end mount app => '/mounted' end get '/mounted/cool/awesome' expect(last_response).to be_successful expect(last_response.body).to eq('sauce') end it 'mounts on a nested path' do app1 = Class.new(described_class) app2 = Class.new(described_class) app2.get '/nice' do 'play' end # NOTE: that the reverse won't work, mount from outside-in app3 = subject app3.mount app1 => '/app1' app1.mount app2 => '/app2' get '/app1/app2/nice' expect(last_response).to be_successful expect(last_response.body).to eq('play') options '/app1/app2/nice' expect(last_response).to be_no_content end it 'responds to options' do app = Class.new(described_class) app.get '/colour' do 'red' end app.namespace :pears do get '/colour' do 'green' end end subject.namespace :apples do mount app end get '/apples/colour' expect(last_response).to be_successful expect(last_response.body).to eq('red') options '/apples/colour' expect(last_response).to be_no_content get '/apples/pears/colour' expect(last_response).to be_successful expect(last_response.body).to eq('green') options '/apples/pears/colour' expect(last_response).to be_no_content end it 'responds to options with path versioning' do subject.version 'v1', using: :path subject.namespace :apples do app = Class.new(Grape::API) # rubocop:disable RSpec/DescribedClass app.get('/colour') do 'red' end mount app end get '/v1/apples/colour' expect(last_response).to be_successful expect(last_response.body).to eq('red') options '/v1/apples/colour' expect(last_response).to be_no_content end it 'mounts a versioned API with nested resources' do api = Class.new(described_class) do version 'v1' resources :users do get :hello do 'hello users' end end end subject.mount api get '/v1/users/hello' expect(last_response.body).to eq('hello users') end it 'mounts a prefixed API with nested resources' do api = Class.new(described_class) do prefix 'api' resources :users do get :hello do 'hello users' end end end subject.mount api get '/api/users/hello' expect(last_response.body).to eq('hello users') end it 'applies format to a mounted API with nested resources' do api = Class.new(described_class) do format :json resources :users do get do { users: true } end end end subject.mount api get '/users' expect(last_response.body).to eq({ users: true }.to_json) end it 'applies auth to a mounted API with nested resources' do api = Class.new(described_class) do format :json http_basic do |username, password| username == 'username' && password == 'password' end resources :users do get do { users: true } end end end subject.mount api get '/users' expect(last_response).to be_unauthorized get '/users', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('username', 'password') expect(last_response.body).to eq({ users: true }.to_json) end it 'mounts multiple versioned APIs with nested resources' do api1 = Class.new(described_class) do version 'one', using: :header, vendor: 'test' resources :users do get :hello do 'one' end end end api2 = Class.new(described_class) do version 'two', using: :header, vendor: 'test' resources :users do get :hello do 'two' end end end subject.mount api1 subject.mount api2 versioned_get '/users/hello', 'one', using: :header, vendor: 'test' expect(last_response.body).to eq('one') versioned_get '/users/hello', 'two', using: :header, vendor: 'test' expect(last_response.body).to eq('two') end it 'recognizes potential versions with mounted path' do a = Class.new(described_class) do version :v1, using: :path get '/hello' do 'hello' end end b = Class.new(described_class) do version :v1, using: :path get '/world' do 'world' end end subject.mount a => '/one' subject.mount b => '/two' get '/one/v1/hello' expect(last_response).to be_successful get '/two/v1/world' expect(last_response).to be_successful end context 'when mounting class extends a subclass of Grape::API' do it 'mounts APIs with the same superclass' do base_api = Class.new(described_class) a = Class.new(base_api) b = Class.new(base_api) expect { a.mount b }.not_to raise_error end end context 'when including a module' do let(:included_module) do Module.new do def self.included(base) base.extend(ClassMethods) end end end before do stub_const( 'ClassMethods', Module.new do def my_method @test = true end end ) end it 'correctlies include module in nested mount' do module_to_include = included_module v1 = Class.new(described_class) do version :v1, using: :path include module_to_include my_method end v2 = Class.new(described_class) do version :v2, using: :path end segment_base = Class.new(described_class) do mount v1 mount v2 end Class.new(described_class) do mount segment_base end expect(v1.my_method).to be_truthy end end end end describe '.endpoints' do it 'adds one for each route created' do subject.get '/' subject.post '/' expect(subject.endpoints.size).to eq(2) end end describe '.change!' do it 'invalidates any compiled instance' do expect(Grape::API::Instance.singleton_class::LOCK).to receive(:synchronize).and_call_original.twice subject.compile! subject.change! subject.compile! end end describe '.endpoint' do before do subject.format :json subject.get '/endpoint/options' do { path: options[:path], source_location: source.source_location } end end it 'path' do get '/endpoint/options' options = Grape::Json.load(last_response.body) expect(options['path']).to eq(['/endpoint/options']) expect(options['source_location'][0]).to include 'api_spec.rb' expect(options['source_location'][1].to_i).to be > 0 end end describe '.route' do context 'plain' do before do subject.get '/' do route.path end subject.get '/path' do route.path end end it 'provides access to route info' do get '/' expect(last_response.body).to eq('/(.:format)') get '/path' expect(last_response.body).to eq('/path(.:format)') end end context 'with desc' do before do subject.desc 'returns description' subject.get '/description' do route.description end subject.desc 'returns parameters', params: { 'x' => 'y' } subject.get '/params/:id' do route.params[params[:id]] end end it 'returns route description' do get '/description' expect(last_response.body).to eq('returns description') end it 'returns route parameters' do get '/params/x' expect(last_response.body).to eq('y') end end end describe '.format' do context ':txt' do before do subject.format :txt subject.content_type :json, 'application/json' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'does not force txt with an extension' do get '/meaning_of_life.json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end context ':txt only' do before do subject.format :txt subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'accepts specified extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'does not accept extensions other than specified' do get '/meaning_of_life.json' expect(last_response).to be_not_found end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end end context ':json' do before do subject.format :json subject.content_type :txt, 'text/plain' subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces json without an extension' do get '/meaning_of_life' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'does not force json with an extension' do get '/meaning_of_life.txt' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'forces json from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_json) end it 'can be overwritten with an explicit api_format' do subject.get '/meaning_of_life_with_content_type' do api_format :txt { meaning_of_life: 42 }.to_s end get '/meaning_of_life_with_content_type' expect(last_response.body).to eq({ meaning_of_life: 42 }.to_s) end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before throw :error, message: 'Unauthorized', status: 500 end end subject.use middleware subject.get do end get '/' expect(last_response).to be_server_error expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json) end end context ':serializable_hash' do before do stub_const( 'SerializableHashExample', Class.new do def serializable_hash { abc: 'def' } end end ) subject.format :serializable_hash end it 'instance' do subject.get '/example' do SerializableHashExample.new end get '/example' expect(last_response.body).to eq('{"abc":"def"}') end it 'root' do subject.get '/example' do { 'root' => SerializableHashExample.new } end get '/example' expect(last_response.body).to eq('{"root":{"abc":"def"}}') end it 'array' do subject.get '/examples' do [SerializableHashExample.new, SerializableHashExample.new] end get '/examples' expect(last_response.body).to eq('[{"abc":"def"},{"abc":"def"}]') end end context ':xml' do before do subject.format :xml end it 'string' do subject.get '/example' do 'example' end get '/example' expect(last_response).to be_server_error expect(last_response.body).to eq <<~XML cannot convert String to xml XML end it 'hash' do subject.get '/example' do { example1: 'example1', example2: 'example2' } end get '/example' expect(last_response).to be_successful expect(last_response.body).to eq <<~XML example1 example2 XML end it 'array' do subject.get '/example' do %w[example1 example2] end get '/example' expect(last_response).to be_successful expect(last_response.body).to eq <<~XML example1 example2 XML end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before throw :error, message: 'Unauthorized', status: 500 end end subject.use middleware subject.get do end get '/' expect(last_response.status).to eq(500) expect(last_response.body).to eq <<~XML Unauthorized XML end end end describe '.configure' do context 'when given a block' do it 'returns self' do expect(subject.configure {}).to be subject end it 'calls the block passing the config' do call = [false, nil] subject.configure do |config| call = [true, config] end expect(call[0]).to be true expect(call[1]).not_to be_nil end end context 'when not given a block' do it 'returns a configuration object' do expect(subject.configure).to respond_to(:[], :[]=) end end it 'allows configuring the api' do subject.configure do |config| config[:hello] = 'hello' config[:bread] = 'bread' end subject.get '/hello-bread' do "#{configuration[:hello]} #{configuration[:bread]}" end get '/hello-bread' expect(last_response.body).to eq 'hello bread' end end context 'catch-all' do before do api1 = Class.new(described_class) api1.version 'v1', using: :path api1.get 'hello' do 'v1' end api2 = Class.new(described_class) api2.version 'v2', using: :path api2.get 'hello' do 'v2' end subject.mount api1 subject.mount api2 end [true, false].each do |anchor| it "anchor=#{anchor}" do subject.route :any, '*path', anchor: anchor do error!("Unrecognized request path: #{params[:path]} - #{env[Rack::PATH_INFO]}#{env[Rack::SCRIPT_NAME]}", 404) end get '/v1/hello' expect(last_response).to be_successful expect(last_response.body).to eq('v1') get '/v2/hello' expect(last_response).to be_successful expect(last_response.body).to eq('v2') options '/v2/hello' expect(last_response).to be_no_content expect(last_response.body).to be_blank head '/v2/hello' expect(last_response).to be_successful expect(last_response.body).to be_blank get '/foobar' expect(last_response).to be_not_found expect(last_response.body).to eq('Unrecognized request path: foobar - /foobar') end end end context 'cascading' do context 'via version' do it 'cascades' do subject.version 'v1', using: :path, cascade: true get '/v1/hello' expect(last_response).to be_not_found expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.version 'v2', using: :path, cascade: false get '/v2/hello' expect(last_response).to be_not_found expect(last_response.headers.keys).not_to include 'X-Cascade' end end context 'via endpoint' do it 'cascades' do subject.cascade true get '/hello' expect(last_response).to be_not_found expect(last_response.headers['X-Cascade']).to eq('pass') end it 'does not cascade' do subject.cascade false get '/hello' expect(last_response).to be_not_found expect(last_response.headers.keys).not_to include 'X-Cascade' end end end context 'with json default_error_formatter' do it 'returns json error' do subject.content_type :json, 'application/json' subject.default_error_formatter :json subject.get '/something' do 'foo' end get '/something' expect(last_response.status).to eq(406) expect(last_response.body).to eq(Rack::Utils.escape_html({ error: "The requested format 'txt' is not supported." }.to_json)) end end context 'with unsafe HTML format specified' do it 'escapes the HTML' do subject.content_type :json, 'application/json' subject.get '/something' do 'foo' end get '/something?format=' expect(last_response.status).to eq(406) expect(last_response.body).to eq(Rack::Utils.escape_html("The requested format '' is not supported.")) end end context 'with non-UTF-8 characters in specified format' do it 'converts the characters' do subject.format :json subject.content_type :json, 'application/json' subject.get '/something' do 'foo' end get '/something?format=%0A%0B%BF' expect(last_response.status).to eq(406) message = "The requested format '\n\u000b\357\277\275' is not supported." expect(last_response.body).to eq({ error: message }.to_json) end end context 'body' do context 'false' do before do subject.get '/blank' do body false end end it 'returns blank body' do get '/blank' expect(last_response).to be_no_content expect(last_response.body).to be_blank end end context 'plain text' do before do subject.get '/text' do content_type 'text/plain' body 'Hello World' 'ignored' end end it 'returns blank body' do get '/text' expect(last_response).to be_successful expect(last_response.body).to eq 'Hello World' end end end describe 'normal class methods' do subject(:grape_api) { Class.new(described_class) } before do stub_const('MyAPI', grape_api) end it 'can find the appropiate name' do expect(grape_api.name).to eq 'MyAPI' end it 'is equal to itself' do expect(grape_api.itself).to eq grape_api expect(grape_api).to eq MyAPI expect(grape_api.eql?(MyAPI)) end end describe '.inherited' do context 'overriding within class' do let(:root_api) do Class.new(described_class) do @bar = 'Hello, world' def self.inherited(child_api) super child_api.instance_variable_set(:@foo, @bar.dup) end end end let(:child_api) { Class.new(root_api) } it 'allows overriding the hook' do expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world') end end it 'does not override methods inherited from Class' do Class.define_method(:test_method) {} subclass = Class.new(described_class) expect(subclass).not_to receive(:add_setup) subclass.test_method ensure Class.remove_method(:test_method) end context 'overriding via composition' do let(:inherited) do Module.new do def inherited(api) super api.instance_variable_set(:@foo, @bar.dup) end end end let(:root_api) do context = self Class.new(described_class) do @bar = 'Hello, world' extend context.inherited end end let(:child_api) { Class.new(root_api) } it 'allows overriding the hook' do expect(child_api.instance_variable_get(:@foo)).to eq('Hello, world') end end end describe 'const_missing' do subject(:grape_api) { Class.new(described_class) } let(:mounted) do Class.new(described_class) do get '/missing' do SomeRandomConstant end end end before { subject.mount mounted => '/const' } it 'raises an error' do expect { get '/const/missing' }.to raise_error(NameError).with_message(/SomeRandomConstant/) end end describe 'custom route helpers on nested APIs' do subject(:grape_api) do Class.new(described_class) do version 'v1', using: :path end end let(:shared_api_module) do Module.new do # rubocop:disable Style/ExplicitBlockArgument -- because # this causes the underlying issue in this form def uniqe_id_route params do use :unique_id end route_param(:id) do yield end end # rubocop:enable Style/ExplicitBlockArgument end end let(:shared_api_definitions) do Module.new do extend ActiveSupport::Concern included do helpers do params :unique_id do requires :id, type: String, allow_blank: false, regexp: /\d+-\d+/ end end end end end let(:orders_root) do shared = shared_api_definitions find = orders_find_endpoint Class.new(described_class) do include shared namespace(:orders) do mount find end end end let(:orders_find_endpoint) do shared = shared_api_definitions Class.new(described_class) do include shared uniqe_id_route do desc 'Fetch a single order' do detail 'While specifying the order id on the route' end get { params[:id] } end end end before do Grape::API::Instance.extend(shared_api_module) subject.mount orders_root end it 'returns an error when the id is bad' do get '/v1/orders/abc' expect(last_response.body).to eq('id is invalid') end it 'returns the given id when it is valid' do get '/v1/orders/1-2' expect(last_response.body).to eq('1-2') end end context 'instance variables' do context 'when setting instance variables in a before validation' do it 'is accessible inside the endpoint' do expected_instance_variable_value = 'wadus' subject.before do @my_var = expected_instance_variable_value end subject.get('/') do { my_var: @my_var }.to_json end get '/' expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) end end context 'when setting instance variables inside the endpoint code' do it 'is accessible inside the rescue_from handler' do expected_instance_variable_value = 'wadus' subject.rescue_from(:all) do body = { my_var: @my_var } error!(body, 400) end subject.get('/') do @my_var = expected_instance_variable_value raise end get '/' expect(last_response).to be_bad_request expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) end it 'is NOT available in other endpoints of the same api' do expected_instance_variable_value = 'wadus' subject.get('/first') do @my_var = expected_instance_variable_value { my_var: @my_var }.to_json end subject.get('/second') do { my_var: @my_var }.to_json end get '/first' expect(last_response.body).to eq({ my_var: expected_instance_variable_value }.to_json) get '/second' expect(last_response.body).to eq({ my_var: nil }.to_json) end end context 'when set type to a route_param' do context 'and the param does not match' do it 'returns a 404 response' do subject.namespace :books do route_param :id, type: Integer do get do params[:id] end end end get '/books/other' expect(last_response).to be_not_found end end end end context 'rescue_from context' do subject { last_response } let(:api) do Class.new(described_class) do rescue_from :all do error!(context.env, 400) end get { raise ArgumentError, 'Oops!' } end end let(:app) { api } before { get '/' } it { is_expected.to be_bad_request } end describe '.build_with' do let(:app) do Class.new(described_class) do build_with :unknown params do requires :a_param, type: Integer end get end end before do get '/' end it 'raises an UnknownParamsBuilder error' do expect(last_response).to be_server_error expect(last_response.body).to eq('unknown params_builder: unknown') end end describe '.lint!' do let(:app) do Class.new(described_class) do lint! get '/' do status 42 end end end around do |example| @lint = Grape.config.lint Grape.config.lint = false example.run ensure Grape.config.lint = @lint end it 'raises a Rack::Lint error' do # Status must be an Integer >= 100 expect { get '/' }.to raise_error(Rack::Lint::LintError) end end describe '.cascade' do subject { api.cascade } let(:api) do Class.new(Grape::API) do cascade true end end it { is_expected.to be(true) } end end ================================================ FILE: spec/grape/content_types_spec.rb ================================================ # frozen_string_literal: true describe Grape::ContentTypes do describe 'DEFAULTS' do subject { described_class::DEFAULTS } let(:expected_value) do { xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', binary: 'application/octet-stream', txt: 'text/plain' }.freeze end it { is_expected.to eq(expected_value) } end describe 'MIME_TYPES' do subject { described_class::MIME_TYPES } let(:expected_value) do { 'application/xml' => :xml, 'application/json' => :json, 'application/octet-stream' => :binary, 'text/plain' => :txt }.freeze end it { is_expected.to eq(expected_value) } end describe '.content_types_for' do subject { described_class.content_types_for(from_settings) } context 'when from_settings is present' do let(:from_settings) { { a: :b } } it { is_expected.to eq(from_settings) } end context 'when from_settings is not present' do let(:from_settings) { nil } it { is_expected.to be(described_class::DEFAULTS) } end end describe '.mime_types_for' do subject { described_class.mime_types_for(from_settings) } context 'when from_settings is equal to Grape::ContentTypes::DEFAULTS' do let(:from_settings) do { xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', binary: 'application/octet-stream', txt: 'text/plain' }.freeze end it { is_expected.to be(described_class::MIME_TYPES) } end context 'when from_settings is not equal to Grape::ContentTypes::DEFAULTS' do let(:from_settings) do { xml: 'application/xml;charset=utf-8' } end it { is_expected.to eq('application/xml' => :xml) } end end end ================================================ FILE: spec/grape/dsl/callbacks_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Callbacks do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Settings extend Grape::DSL::Callbacks end end let(:proc) { -> {} } describe '.before' do it 'adds a block to "before"' do subject.before(&proc) expect(subject.inheritable_setting.namespace_stackable[:befores]).to eq([proc]) end end describe '.before_validation' do it 'adds a block to "before_validation"' do subject.before_validation(&proc) expect(subject.inheritable_setting.namespace_stackable[:before_validations]).to eq([proc]) end end describe '.after_validation' do it 'adds a block to "after_validation"' do subject.after_validation(&proc) expect(subject.inheritable_setting.namespace_stackable[:after_validations]).to eq([proc]) end end describe '.after' do it 'adds a block to "after"' do subject.after(&proc) expect(subject.inheritable_setting.namespace_stackable[:afters]).to eq([proc]) end end end ================================================ FILE: spec/grape/dsl/desc_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Desc do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Desc extend Grape::DSL::Settings end end describe '.desc' do it 'sets a description' do desc_text = 'The description' options = { message: 'none' } subject.desc desc_text, options expect(subject.namespace_setting(:description)).to eq(options.merge(description: desc_text)) expect(subject.route_setting(:description)).to eq(options.merge(description: desc_text)) end it 'can be set with a block' do expected_options = { summary: 'summary', description: 'The description', detail: 'more details', params: { first: :param }, entity: Object, default: { code: 400, message: 'Invalid' }, http_codes: [[401, 'Unauthorized', 'Entities::Error']], named: 'My named route', body_name: 'My body name', headers: [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ], hidden: false, deprecated: false, is_array: true, nickname: 'nickname', produces: %w[array of mime_types], consumes: %w[array of mime_types], tags: %w[tag1 tag2], security: %w[array of security schemes] } subject.desc 'The description' do summary 'summary' detail 'more details' params(first: :param) success Object default code: 400, message: 'Invalid' failure [[401, 'Unauthorized', 'Entities::Error']] named 'My named route' body_name 'My body name' headers [ XAuthToken: { description: 'Valdates your identity', required: true }, XOptionalHeader: { description: 'Not really needed', required: false } ] hidden false deprecated false is_array true nickname 'nickname' produces %w[array of mime_types] consumes %w[array of mime_types] tags %w[tag1 tag2] security %w[array of security schemes] end expect(subject.namespace_setting(:description)).to eq(expected_options) expect(subject.route_setting(:description)).to eq(expected_options) end end end ================================================ FILE: spec/grape/dsl/headers_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Headers do subject { dummy_class.new } let(:dummy_class) do Class.new do include Grape::DSL::Headers end end let(:header_data) do { 'first key' => 'First Value', 'second key' => 'Second Value' } end context 'when headers are set' do describe '#header' do before do header_data.each { |k, v| subject.header(k, v) } end describe 'get' do it 'returns a specifc value' do expect(subject.header['first key']).to eq 'First Value' expect(subject.header['second key']).to eq 'Second Value' end it 'returns all set headers' do expect(subject.header).to eq header_data expect(subject.headers).to eq header_data end end describe 'set' do it 'returns value' do expect(subject.header('third key', 'Third Value')) expect(subject.header['third key']).to eq 'Third Value' end end describe 'delete' do it 'deletes a header key-value pair' do expect(subject.header('first key')).to eq header_data['first key'] expect(subject.header).not_to have_key('first key') end end end end context 'when no headers are set' do describe '#header' do it 'returns nil' do expect(subject.header['first key']).to be_nil expect(subject.header('first key')).to be_nil end end end end ================================================ FILE: spec/grape/dsl/helpers_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Helpers do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Helpers extend Grape::DSL::Settings def self.mods inheritable_setting.namespace_stackable[:helpers] end def self.first_mod mods.first end end end let(:proc) do lambda do |*| def test :test end end end describe '.helpers' do it 'adds a module with the given block' do subject.helpers(&proc) expect(subject.first_mod.instance_methods).to include(:test) end it 'uses provided modules' do mod = Module.new subject.helpers(mod, &proc) expect(subject.first_mod).to eq mod end it 'uses many provided modules' do mod = Module.new mod2 = Module.new mod3 = Module.new subject.helpers(mod, mod2, mod3, &proc) expect(subject.mods).to include(mod, mod2, mod3) end context 'with an external file' do let(:boolean_helper) do Module.new do extend Grape::API::Helpers params :requires_toggle_prm do requires :toggle_prm, type: Boolean end end end it 'sets Boolean as a Grape::API::Boolean' do subject.helpers boolean_helper expect(subject.first_mod::Boolean).to eq Grape::API::Boolean end end context 'in child classes' do let(:base_class) do Class.new(Grape::API) do helpers do params :requires_toggle_prm do requires :toggle_prm, type: Integer end end end end let(:api_class) do Class.new(base_class) do params do use :requires_toggle_prm end end end it 'is available' do expect { api_class }.not_to raise_exception end end context 'public scope' do it 'returns helpers only' do expect(Class.new { extend Grape::DSL::Helpers }.singleton_methods - Class.methods).to contain_exactly(:helpers) end end end end ================================================ FILE: spec/grape/dsl/inside_route_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::InsideRoute do subject { dummy_class.new } let(:dummy_class) do Class.new do include Grape::DSL::InsideRoute include Grape::DSL::Settings attr_reader :env, :request, :new_settings def initialize @env = {} @header = {} @new_settings = { namespace_inheritable: inheritable_setting.namespace_inheritable, namespace_stackable: inheritable_setting.namespace_stackable } end def header(key = nil, val = nil) if key val ? header[key] = val : header.delete(key) else @header ||= Grape::Util::Header.new end end end end describe '#version' do it 'defaults to nil' do expect(subject.version).to be_nil end it 'returns env[api.version]' do subject.env[Grape::Env::API_VERSION] = 'dummy' expect(subject.version).to eq 'dummy' end end describe '#error!' do it 'throws :error' do expect { subject.error! 'Not Found', 404 }.to throw_symbol(:error) end describe 'thrown' do before do catch(:error) { subject.error! 'Not Found', 404 } end it 'sets status' do expect(subject.status).to eq 404 end end describe 'default_error_status' do before do subject.inheritable_setting.namespace_inheritable[:default_error_status] = 500 catch(:error) { subject.error! 'Unknown' } end it 'sets status to default_error_status' do expect(subject.status).to eq 500 end end # self.status(status || settings[:default_error_status]) # throw :error, message: message, status: self.status, headers: headers end describe '#redirect' do describe 'default' do before do subject.redirect '/' end it 'sets status to 302' do expect(subject.status).to eq 302 end it 'sets location header' do expect(subject.header['Location']).to eq '/' end end describe 'permanent' do before do subject.redirect '/', permanent: true end it 'sets status to 301' do expect(subject.status).to eq 301 end it 'sets location header' do expect(subject.header['Location']).to eq '/' end end end describe '#status' do %w[GET PUT OPTIONS].each do |method| it 'defaults to 200 on GET' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: method)) expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 200 end end it 'defaults to 201 on POST' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::POST)) expect(subject).to receive(:request).and_return(request) expect(subject.status).to eq 201 end it 'defaults to 204 on DELETE' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::DELETE)) expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 204 end it 'defaults to 200 on DELETE with a body present' do request = Grape::Request.new(Rack::MockRequest.env_for('/', method: Rack::DELETE)) subject.body 'content here' expect(subject).to receive(:request).and_return(request).twice expect(subject.status).to eq 200 end it 'returns status set' do subject.status 501 expect(subject.status).to eq 501 end it 'accepts symbol for status' do subject.status :see_other expect(subject.status).to eq 303 end it 'raises error if unknow symbol is passed' do expect { subject.status :foo_bar } .to raise_error(ArgumentError, 'Status code :foo_bar is invalid.') end it 'accepts unknown Integer status codes' do expect { subject.status 210 }.not_to raise_error end it 'raises error if status is not a integer or symbol' do expect { subject.status Object.new } .to raise_error(ArgumentError, 'Status code must be Integer or Symbol.') end end describe '#return_no_content' do it 'sets the status code and body' do subject.return_no_content expect(subject.status).to eq 204 expect(subject.body).to eq '' end end describe '#content_type' do describe 'set' do before do subject.content_type 'text/plain' end it 'returns value' do expect(subject.content_type).to eq 'text/plain' end end it 'returns default' do expect(subject.content_type).to be_nil end end describe '#body' do describe 'set' do before do subject.body 'body' end it 'returns value' do expect(subject.body).to eq 'body' end end describe 'false' do before do subject.body false end it 'sets status to 204' do expect(subject.body).to eq '' expect(subject.status).to eq 204 end end it 'returns default' do expect(subject.body).to be_nil end end describe '#sendfile' do describe 'set' do context 'as file path' do let(:file_path) { '/some/file/path' } let(:file_response) do file_body = Grape::ServeStream::FileBody.new(file_path) Grape::ServeStream::StreamResponse.new(file_body) end before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header 'Transfer-Encoding', 'base64' subject.sendfile file_path end it 'returns value wrapped in StreamResponse' do expect(subject.sendfile).to eq file_response end it 'set the correct headers' do expect(subject.header).to match( Rack::CACHE_CONTROL => 'cache', Rack::CONTENT_LENGTH => 123, 'Transfer-Encoding' => 'base64' ) end end context 'as object' do let(:file_object) { double('StreamerObject', each: nil) } it 'raises an error that only a file path is supported' do expect { subject.sendfile file_object }.to raise_error(ArgumentError, /Argument must be a file path/) end end end it 'returns default' do expect(subject.sendfile).to be_nil end end describe '#stream' do describe 'set' do context 'as a file path' do let(:file_path) { '/some/file/path' } let(:file_response) do file_body = Grape::ServeStream::FileBody.new(file_path) Grape::ServeStream::StreamResponse.new(file_body) end before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header 'Transfer-Encoding', 'base64' subject.stream file_path end it 'returns file body wrapped in StreamResponse' do expect(subject.stream).to eq file_response end it 'sets only the cache-control header' do expect(subject.header).to match(Rack::CACHE_CONTROL => 'no-cache') end end context 'as a stream object' do let(:stream_object) { double('StreamerObject', each: nil) } let(:stream_response) do Grape::ServeStream::StreamResponse.new(stream_object) end before do subject.header Rack::CACHE_CONTROL, 'cache' subject.header Rack::CONTENT_LENGTH, 123 subject.header 'Transfer-Encoding', 'base64' subject.stream stream_object end it 'returns value wrapped in StreamResponse' do expect(subject.stream).to eq stream_response end it 'set only the cache-control header' do expect(subject.header).to match(Rack::CACHE_CONTROL => 'no-cache') end end context 'as a non-stream object' do let(:non_stream_object) { double('NonStreamerObject') } it 'raises an error that the object must implement :each' do expect { subject.stream non_stream_object }.to raise_error(ArgumentError, /:each/) end end end it 'returns default' do expect(subject.stream).to be_nil expect(subject.header).to be_empty end end describe '#route' do before do subject.env[Grape::Env::GRAPE_ROUTING_ARGS] = {} subject.env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] = 'dummy' end it 'returns route_info' do expect(subject.route).to eq 'dummy' end end describe '#present' do # see entity_spec.rb for entity representation spec coverage describe 'dummy' do before do subject.present 'dummy' end it 'presents dummy object' do expect(subject.body).to eq 'dummy' end end describe 'with' do describe 'entity' do let(:entity_mock) do entity_mock = Object.new allow(entity_mock).to receive(:represent).and_return('dummy') entity_mock end describe 'instance' do before do subject.present 'dummy', with: entity_mock end it 'presents dummy object' do expect(subject.body).to eq 'dummy' end end end end describe 'multiple entities' do let(:entity_mock_one) do entity_mock_one = Object.new allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock_one end let(:entity_mock_two) do entity_mock_two = Object.new allow(entity_mock_two).to receive(:represent).and_return(dummy2: 'dummy2') entity_mock_two end describe 'instance' do before do subject.present 'dummy1', with: entity_mock_one subject.present 'dummy2', with: entity_mock_two end it 'presents both dummy objects' do expect(subject.body[:dummy1]).to eq 'dummy1' expect(subject.body[:dummy2]).to eq 'dummy2' end end end describe 'non mergeable entity' do let(:entity_mock_one) do entity_mock_one = Object.new allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1') entity_mock_one end let(:entity_mock_two) do entity_mock_two = Object.new allow(entity_mock_two).to receive(:represent).and_return('not a hash') entity_mock_two end describe 'instance' do it 'fails' do subject.present 'dummy1', with: entity_mock_one expect do subject.present 'dummy2', with: entity_mock_two end.to raise_error ArgumentError, 'Representation of type String cannot be merged.' end end end end describe '#declared' do let(:dummy_class) do Class.new do include Grape::DSL::Declared attr_reader :before_filter_passed def initialize @before_filter_passed = false end end end it 'is not available by default' do expect { subject.declared({}) }.to raise_error( Grape::DSL::InsideRoute::MethodNotYetAvailable ) end end end ================================================ FILE: spec/grape/dsl/logger_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Logger do let(:dummy_logger) do Class.new do extend Grape::DSL::Logger extend Grape::DSL::Settings end end describe '.logger' do context 'when setting a logger' do subject { dummy_logger.logger :my_logger } it { is_expected.to eq(:my_logger) } end context 'when retrieving logger' do context 'when never been set' do subject { dummy_logger.logger } before { allow(Logger).to receive(:new).with($stdout).and_return(:stdout_logger) } it { is_expected.to eq(:stdout_logger) } end context 'when already set' do subject { dummy_logger.logger } before { dummy_logger.logger :my_logger } it { is_expected.to eq(:my_logger) } end end end end ================================================ FILE: spec/grape/dsl/middleware_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Middleware do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Middleware extend Grape::DSL::Settings end end let(:proc) { -> {} } let(:foo_middleware) { Class.new } let(:bar_middleware) { Class.new } describe '.use' do it 'adds a middleware with the right operation' do subject.use foo_middleware, :arg1, &proc expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:use, foo_middleware, :arg1, proc]]) end end describe '.insert' do it 'adds a middleware with the right operation' do subject.insert 0, :arg1, &proc expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert, 0, :arg1, proc]]) end end describe '.insert_before' do it 'adds a middleware with the right operation' do subject.insert_before foo_middleware, :arg1, &proc expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert_before, foo_middleware, :arg1, proc]]) end end describe '.insert_after' do it 'adds a middleware with the right operation' do subject.insert_after foo_middleware, :arg1, &proc expect(subject.inheritable_setting.namespace_stackable[:middleware]).to eq([[:insert_after, foo_middleware, :arg1, proc]]) end end describe '.middleware' do it 'returns the middleware stack' do subject.use foo_middleware, :arg1, &proc subject.insert_before bar_middleware, :arg1, :arg2 expect(subject.middleware).to eq [[:use, foo_middleware, :arg1, proc], [:insert_before, bar_middleware, :arg1, :arg2]] end end end ================================================ FILE: spec/grape/dsl/parameters_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Parameters do subject { dummy_class.new } let(:dummy_class) do Class.new do include Grape::DSL::Parameters attr_accessor :api, :element, :parent def initialize @validate_attributes = [] end def validate_attributes(*args) @validate_attributes.push(*args) end def validate_attributes_reader @validate_attributes end def push_declared_params(args, _opts) @push_declared_params = args end def push_declared_params_reader @push_declared_params end def validates(*args) @validates = *args end def validates_reader @validates end def new_scope(element, **_opts, &block) nested_scope = self.class.new nested_scope.new_group_scope(element, &block) nested_scope end def new_group_scope(group) prev_group = @group @group = group yield @group = prev_group end def extract_message_option(attrs) return nil unless attrs.is_a?(Array) opts = attrs.last.is_a?(Hash) ? attrs.pop : {} opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil end end end describe '#use' do before do allow_message_expectations_on_nil allow(subject.api).to receive(:namespace_stackable).with(:named_params) end let(:options) { { option: 'value' } } let(:named_params) { { params_group: proc {} } } it 'calls processes associated with named params' do subject.api = Class.new { include Grape::DSL::Settings }.new subject.api.inheritable_setting.namespace_stackable[:named_params] = named_params expect(subject).to receive(:instance_exec).with(options).and_yield subject.use :params_group, **options end it 'raises error when non-existent named param is called' do subject.api = Class.new { include Grape::DSL::Settings }.new expect { subject.use :params_group }.to raise_error('Params :params_group not found!') end end describe '#use_scope' do it 'is alias to #use' do expect(subject.method(:use_scope)).to eq subject.method(:use) end end describe '#includes' do it 'is alias to #use' do expect(subject.method(:includes)).to eq subject.method(:use) end end describe '#requires' do it 'adds a required parameter' do subject.requires :id, type: Integer, desc: 'Identity.' expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: { value: true, message: nil } }]) expect(subject.push_declared_params_reader).to eq([:id]) end end describe '#optional' do it 'adds an optional parameter' do subject.optional :id, type: Integer, desc: 'Identity.' expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) expect(subject.push_declared_params_reader).to eq([:id]) end end describe '#with' do it 'creates a scope with group attributes' do subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' } expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }]) expect(subject.push_declared_params_reader).to eq([:id]) end it 'merges the group attributes' do subject.with(documentation: { in: 'body' }) { subject.optional :vault, documentation: { default: 33 } } expect(subject.validate_attributes_reader).to eq([[:vault], { documentation: { in: 'body', default: 33 } }]) expect(subject.push_declared_params_reader).to eq([:vault]) end it 'overrides the group attribute when values not mergable' do subject.with(type: Integer, documentation: { in: 'body', default: 33 }) do subject.optional :vault subject.optional :allowed_vaults, type: [Integer], documentation: { default: [31, 32, 33], is_array: true } end expect(subject.validate_attributes_reader).to eq( [ [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, [:allowed_vaults], { type: [Integer], documentation: { in: 'body', default: [31, 32, 33], is_array: true } } ] ) end it 'allows a primitive type attribite to overwrite a complex type group attribute' do subject.with(documentation: { x: { nullable: true } }) do subject.optional :vault, type: Integer, documentation: { x: nil } end expect(subject.validate_attributes_reader).to eq( [ [:vault], { type: Integer, documentation: { x: nil } } ] ) end it 'does not nest primitives inside existing complex types erroneously' do subject.with(type: Hash, documentation: { default: { vault: '33' } }) do subject.optional :info subject.optional :role, type: String, documentation: { default: 'resident' } end expect(subject.validate_attributes_reader).to eq( [ [:info], { type: Hash, documentation: { default: { vault: '33' } } }, [:role], { type: String, documentation: { default: 'resident' } } ] ) end it 'merges deeply nested attributes' do subject.with(documentation: { details: { in: 'body', hidden: false } }) do subject.optional :vault, documentation: { details: { desc: 'The vault number' } } end expect(subject.validate_attributes_reader).to eq( [ [:vault], { documentation: { details: { in: 'body', hidden: false, desc: 'The vault number' } } } ] ) end it "supports nested 'with' calls" do subject.with(type: Integer, documentation: { in: 'body' }) do subject.optional :pipboy_id subject.with(documentation: { default: 33 }) do subject.optional :vault subject.with(type: String) do subject.with(documentation: { default: 'resident' }) do subject.optional :role end end subject.optional :age, documentation: { default: 42 } end end expect(subject.validate_attributes_reader).to eq( [ [:pipboy_id], { type: Integer, documentation: { in: 'body' } }, [:vault], { type: Integer, documentation: { in: 'body', default: 33 } }, [:role], { type: String, documentation: { in: 'body', default: 'resident' } }, [:age], { type: Integer, documentation: { in: 'body', default: 42 } } ] ) end it "supports Hash parameter inside the 'with' calls" do subject.with(documentation: { in: 'body' }) do subject.optional :info, type: Hash, documentation: { x: { nullable: true }, desc: 'The info' } do subject.optional :vault, type: Integer, documentation: { default: 33, desc: 'The vault number' } end end expect(subject.validate_attributes_reader).to eq( [ [:info], { type: Hash, documentation: { in: 'body', desc: 'The info', x: { nullable: true } } }, [:vault], { type: Integer, documentation: { in: 'body', default: 33, desc: 'The vault number' } } ] ) end end describe '#mutually_exclusive' do it 'adds an mutally exclusive parameter validation' do subject.mutually_exclusive :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { mutually_exclusive: { value: true, message: nil } }]) end end describe '#exactly_one_of' do it 'adds an exactly of one parameter validation' do subject.exactly_one_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { exactly_one_of: { value: true, message: nil } }]) end end describe '#at_least_one_of' do it 'adds an at least one of parameter validation' do subject.at_least_one_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { at_least_one_of: { value: true, message: nil } }]) end end describe '#all_or_none_of' do it 'adds an all or none of parameter validation' do subject.all_or_none_of :media, :audio expect(subject.validates_reader).to eq([%i[media audio], { all_or_none_of: { value: true, message: nil } }]) end end describe '#group' do it 'is alias to #requires' do expect(subject.method(:group)).to eq subject.method(:requires) end end describe '#params' do it 'inherits params from parent' do parent_params = { foo: 'bar' } subject.parent = Object.new allow(subject.parent).to receive_messages(params: parent_params, qualifying_params: nil) expect(subject.params({})).to eq parent_params end describe 'when params argument is an array of hashes' do it 'returns values of each hash for @element key' do subject.element = :foo expect(subject.params([{ foo: 'bar' }, { foo: 'baz' }])).to eq(%w[bar baz]) end end describe 'when params argument is a hash' do it 'returns value for @element key' do subject.element = :foo expect(subject.params(foo: 'bar')).to eq('bar') end end describe 'when params argument is not a array or a hash' do it 'returns empty hash' do subject.element = Object.new expect(subject.params(Object.new)).to eq({}) end end end end ================================================ FILE: spec/grape/dsl/request_response_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::RequestResponse do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::RequestResponse extend Grape::DSL::Settings end end let(:c_type) { 'application/json' } let(:format) { 'txt' } describe '.default_format' do it 'sets the default format' do subject.default_format :format expect(subject.inheritable_setting.namespace_inheritable[:default_format]).to eq(:format) end it 'returns the format without paramter' do subject.default_format :format expect(subject.default_format).to eq :format end end describe '.format' do it 'sets a new format' do subject.format format expect(subject.inheritable_setting.namespace_inheritable[:format]).to eq(format.to_sym) expect(subject.inheritable_setting.namespace_inheritable[:default_error_formatter]).to eq(Grape::ErrorFormatter::Txt) end end describe '.formatter' do it 'sets the formatter for a content type' do subject.formatter c_type, :formatter expect(subject.inheritable_setting.namespace_stackable[:formatters]).to eq([c_type.to_sym => :formatter]) end end describe '.parser' do it 'sets a parser for a content type' do subject.parser c_type, :parser expect(subject.inheritable_setting.namespace_stackable[:parsers]).to eq([c_type.to_sym => :parser]) end end describe '.default_error_formatter' do it 'sets a new error formatter' do subject.default_error_formatter :json expect(subject.inheritable_setting.namespace_inheritable[:default_error_formatter]).to eq(Grape::ErrorFormatter::Json) end end describe '.error_formatter' do it 'sets a error_formatter' do format = 'txt' subject.error_formatter format, :error_formatter expect(subject.inheritable_setting.namespace_stackable[:error_formatters]).to eq([{ format.to_sym => :error_formatter }]) end it 'understands syntactic sugar' do subject.error_formatter format, with: :error_formatter expect(subject.inheritable_setting.namespace_stackable[:error_formatters]).to eq([{ format.to_sym => :error_formatter }]) end end describe '.content_type' do it 'sets a content type for a format' do subject.content_type format, c_type expect(subject.inheritable_setting.namespace_stackable[:content_types]).to eq([format.to_sym => c_type]) end end describe '.content_types' do it 'returns all content types' do expect(subject.content_types).to eq(xml: 'application/xml', serializable_hash: 'application/json', json: 'application/json', txt: 'text/plain', binary: 'application/octet-stream') end end describe '.default_error_status' do it 'sets a default error status' do subject.default_error_status 500 expect(subject.inheritable_setting.namespace_inheritable[:default_error_status]).to eq(500) end end describe '.rescue_from' do describe ':all' do it 'sets rescue all to true' do subject.rescue_from :all expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, all_rescue_handler: nil } ) end it 'sets given proc as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :all, rescue_handler_proc expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, all_rescue_handler: rescue_handler_proc } ) end it 'sets given block as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :all, &rescue_handler_proc expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, all_rescue_handler: rescue_handler_proc } ) end it 'sets a rescue handler declared through :with option' do with_block = -> { 'hello' } subject.rescue_from :all, with: with_block expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, all_rescue_handler: with_block } ) end it 'abort if :with option value is not Symbol, String or Proc' do expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc") end it 'abort if both :with option and block are passed' do expect do subject.rescue_from :all, with: -> { 'hello' } do error!('bye') end end.to raise_error(ArgumentError, 'both :with option and block cannot be passed') end end describe ':grape_exceptions' do it 'sets rescue all to true' do subject.rescue_from :grape_exceptions expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, rescue_grape_exceptions: true, grape_exceptions_rescue_handler: nil } ) end it 'sets given proc as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :grape_exceptions, rescue_handler_proc expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, rescue_grape_exceptions: true, grape_exceptions_rescue_handler: rescue_handler_proc } ) end it 'sets given block as rescue handler' do rescue_handler_proc = proc {} subject.rescue_from :grape_exceptions, &rescue_handler_proc expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, rescue_grape_exceptions: true, grape_exceptions_rescue_handler: rescue_handler_proc } ) end it 'sets a rescue handler declared through :with option' do with_block = -> { 'hello' } subject.rescue_from :grape_exceptions, with: with_block expect(subject.inheritable_setting.namespace_inheritable.to_hash).to eq( { rescue_all: true, rescue_grape_exceptions: true, grape_exceptions_rescue_handler: with_block } ) end end describe 'list of exceptions is passed' do it 'sets hash of exceptions as rescue handlers' do subject.rescue_from StandardError expect(subject.inheritable_setting.namespace_reverse_stackable[:rescue_handlers]).to eq([StandardError => nil]) expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq([{}]) end it 'rescues only base handlers if rescue_subclasses: false option is passed' do subject.rescue_from StandardError, rescue_subclasses: false expect(subject.inheritable_setting.namespace_reverse_stackable[:base_only_rescue_handlers]).to eq([StandardError => nil]) expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq([rescue_subclasses: false]) end it 'sets given proc as rescue handler for each key in hash' do rescue_handler_proc = proc {} subject.rescue_from StandardError, rescue_handler_proc expect(subject.inheritable_setting.namespace_reverse_stackable[:rescue_handlers]).to eq([StandardError => rescue_handler_proc]) expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq([{}]) end it 'sets given block as rescue handler for each key in hash' do rescue_handler_proc = proc {} subject.rescue_from StandardError, &rescue_handler_proc expect(subject.inheritable_setting.namespace_reverse_stackable[:rescue_handlers]).to eq([StandardError => rescue_handler_proc]) expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq([{}]) end it 'sets a rescue handler declared through :with option for each key in hash' do with_block = -> { 'hello' } subject.rescue_from StandardError, with: with_block expect(subject.inheritable_setting.namespace_reverse_stackable[:rescue_handlers]).to eq([StandardError => with_block]) expect(subject.inheritable_setting.namespace_stackable[:rescue_options]).to eq([{}]) end end end describe '.represent' do it 'sets a presenter for a class' do presenter = Class.new subject.represent :ThisClass, with: presenter expect(subject.inheritable_setting.namespace_stackable[:representations]).to eq([ThisClass: presenter]) end end end ================================================ FILE: spec/grape/dsl/routing_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Routing do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Routing extend Grape::DSL::Settings extend Grape::DSL::Validations class << self attr_reader :instance, :base attr_accessor :configuration end end end let(:proc) { -> {} } let(:options) { { a: :b } } let(:path) { '/dummy' } describe '.version' do it 'sets a version for route' do version = 'v1' expect(subject.version(version)).to eq(version) expect(subject.inheritable_setting.namespace_inheritable[:version]).to eq([version]) expect(subject.inheritable_setting.namespace_inheritable[:version_options]).to eq(using: :path) end end describe '.prefix' do it 'sets a prefix for route' do prefix = '/api' subject.prefix prefix expect(subject.inheritable_setting.namespace_inheritable[:root_prefix]).to eq(prefix) end end describe '.scope' do let(:root_app) do Class.new(Grape::API) do scope :my_scope do get :my_endpoint do return_no_content end end end end it 'create a scope without affecting the URL' do env = Rack::MockRequest.env_for('/my_endpoint', method: Rack::GET) response = Rack::MockResponse[*root_app.call(env)] expect(response).to be_no_content end end describe '.do_not_route_head!' do it 'sets do not route head option' do subject.do_not_route_head! expect(subject.inheritable_setting.namespace_inheritable[:do_not_route_head]).to be(true) end end describe '.do_not_route_options!' do it 'sets do not route options option' do subject.do_not_route_options! expect(subject.inheritable_setting.namespace_inheritable[:do_not_route_options]).to be(true) end end describe '.mount' do it 'mounts on a nested path' do subject = Class.new(Grape::API) app1 = Class.new(Grape::API) do get '/' do return_no_content end end app2 = Class.new(Grape::API) do get '/' do return_no_content end end subject.mount app1 => '/app1' app1.mount app2 => '/app2' env = Rack::MockRequest.env_for('/app1', method: Rack::GET) response = Rack::MockResponse[*subject.call(env)] expect(response).to be_no_content env = Rack::MockRequest.env_for('/app1/app2', method: Rack::GET) response = Rack::MockResponse[*subject.call(env)] expect(response).to be_no_content end it 'mounts multiple routes at once' do base_app = Class.new(Grape::API) app1 = Class.new(Grape::API) do get '/' do return_no_content end end app2 = Class.new(Grape::API) do get '/' do return_no_content end end base_app.mount(app1 => '/app1', app2 => '/app2') env = Rack::MockRequest.env_for('/app1', method: Rack::GET) response = Rack::MockResponse[*base_app.call(env)] expect(response).to be_no_content env = Rack::MockRequest.env_for('/app2', method: Rack::GET) response = Rack::MockResponse[*base_app.call(env)] expect(response).to be_no_content end end describe '.route' do before do allow(subject).to receive(:endpoints).and_return([]) allow(subject.inheritable_setting).to receive(:route_end) allow(subject).to receive(:reset_validations!) end it 'marks end of the route' do expect(subject.inheritable_setting).to receive(:route_end) subject.route(:any) end it 'resets validations' do expect(subject).to receive(:reset_validations!) subject.route(:any) end it 'defines a new endpoint' do expect { subject.route(:any) } .to change { subject.endpoints.count }.from(0).to(1) end it 'does not duplicate identical endpoints' do subject.route(:any) expect { subject.route(:any) } .not_to change(subject.endpoints, :count) end it 'generates correct endpoint options' do subject.inheritable_setting.route[:description] = { fiz: 'baz' } subject.inheritable_setting.namespace_stackable[:params] = { nuz: 'naz' } expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options| expect(endpoint_options[:method]).to eq :get expect(endpoint_options[:path]).to eq '/foo' expect(endpoint_options[:for]).to eq subject expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' }) end.and_yield subject.route(:get, '/foo', { foo: 'bar' }, &proc {}) end end describe '.get' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::GET, path, options) subject.get path, **options, &proc end end describe '.post' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::POST, path, options) subject.post path, **options, &proc end end describe '.put' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::PUT, path, options) subject.put path, **options, &proc end end describe '.head' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::HEAD, path, options) subject.head path, **options, &proc end end describe '.delete' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::DELETE, path, options) subject.delete path, **options, &proc end end describe '.options' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::OPTIONS, path, options) subject.options path, **options, &proc end end describe '.patch' do it 'delegates to .route' do expect(subject).to receive(:route).with(Rack::PATCH, path, options) subject.patch path, **options, &proc end end describe '.namespace' do it 'creates a new namespace with given name and options' do subject.namespace(:foo, foo: 'bar') {} expect(subject.namespace(:foo, foo: 'bar')).to eq(Grape::Namespace.new(:foo, foo: 'bar')) end it 'calls #joined_space_path on Namespace' do inside_namespace = nil subject.namespace(:foo, foo: 'bar') do inside_namespace = namespace end expect(inside_namespace).to eq('/foo') end end describe '.group' do it 'is alias to #namespace' do expect(subject.method(:group)).to eq subject.method(:namespace) end end describe '.resource' do it 'is alias to #namespace' do expect(subject.method(:resource)).to eq subject.method(:namespace) end end describe '.resources' do it 'is alias to #namespace' do expect(subject.method(:resources)).to eq subject.method(:namespace) end end describe '.segment' do it 'is alias to #namespace' do expect(subject.method(:segment)).to eq subject.method(:namespace) end end describe '.routes' do let(:main_app) { Class.new(Grape::API) } let(:first_app) { Class.new(Grape::API) } let(:second_app) { Class.new(Grape::API) } before do main_app.mount(first_app => '/first_app', second_app => '/second_app') end it 'returns flatten endpoints routes' do expect(main_app.endpoints).not_to be_empty expect(main_app.routes).to eq(main_app.endpoints.map(&:routes).flatten) end context 'when #routes was already called once' do it 'memoizes' do object_id = main_app.routes.object_id expect(main_app.routes.object_id).to eq(object_id) end end end describe '.route_param' do let!(:options) { { requirements: regex } } let(:regex) { /(.*)/ } it 'calls #namespace with given params' do expect(subject).to receive(:namespace).with(':foo', requirements: nil).and_yield subject.route_param('foo', &proc {}) end it 'nests requirements option under param name' do expect(subject).to receive(:namespace) do |_param, options| expect(options[:requirements][:foo]).to eq regex end subject.route_param('foo', **options, &proc {}) end it 'does not modify options parameter' do allow(subject).to receive(:namespace) expect { subject.route_param('foo', **options, &proc {}) } .not_to(change { options }) end end describe '.versions' do it 'returns last defined version' do subject.version 'v1' subject.version 'v2' expect(subject.version).to eq('v2') end end end ================================================ FILE: spec/grape/dsl/settings_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Settings do subject { dummy_class.new } let(:dummy_class) do Class.new do include Grape::DSL::Settings def with_namespace(&block) within_namespace(&block) end def reset_validations!; end end end describe '#global_setting' do it 'sets a value globally' do subject.global_setting :some_thing, :foo_bar expect(subject.global_setting(:some_thing)).to eq :foo_bar subject.with_namespace do subject.global_setting :some_thing, :foo_bar_baz expect(subject.global_setting(:some_thing)).to eq :foo_bar_baz end expect(subject.global_setting(:some_thing)).to eq(:foo_bar_baz) end end describe '#route_setting' do it 'sets a value until the end of a namespace' do subject.with_namespace do subject.route_setting :some_thing, :foo_bar expect(subject.route_setting(:some_thing)).to eq :foo_bar end expect(subject.route_setting(:some_thing)).to be_nil end end describe '#namespace_setting' do it 'sets a value until the end of a namespace' do subject.with_namespace do subject.namespace_setting :some_thing, :foo_bar expect(subject.namespace_setting(:some_thing)).to eq :foo_bar end expect(subject.namespace_setting(:some_thing)).to be_nil end it 'resets values after leaving nested namespaces' do subject.with_namespace do subject.namespace_setting :some_thing, :foo_bar expect(subject.namespace_setting(:some_thing)).to eq :foo_bar subject.with_namespace do expect(subject.namespace_setting(:some_thing)).to be_nil end expect(subject.namespace_setting(:some_thing)).to eq :foo_bar end expect(subject.namespace_setting(:some_thing)).to be_nil end end describe '#namespace_inheritable' do it 'inherits values from surrounding namespace' do subject.with_namespace do subject.inheritable_setting.namespace_inheritable[:some_thing] = :foo_bar expect(subject.inheritable_setting.namespace_inheritable[:some_thing]).to eq :foo_bar subject.with_namespace do expect(subject.inheritable_setting.namespace_inheritable[:some_thing]).to eq :foo_bar subject.inheritable_setting.namespace_inheritable[:some_thing] = :foo_bar_2 expect(subject.inheritable_setting.namespace_inheritable[:some_thing]).to eq :foo_bar_2 end expect(subject.inheritable_setting.namespace_inheritable[:some_thing]).to eq :foo_bar end end end describe '#namespace_stackable' do it 'stacks values from surrounding namespace' do subject.with_namespace do subject.inheritable_setting.namespace_stackable[:some_thing] = :foo_bar expect(subject.inheritable_setting.namespace_stackable[:some_thing]).to eq [:foo_bar] subject.with_namespace do subject.inheritable_setting.namespace_stackable[:some_thing] = :foo_bar_2 expect(subject.inheritable_setting.namespace_stackable[:some_thing]).to eq %i[foo_bar foo_bar_2] end expect(subject.inheritable_setting.namespace_stackable[:some_thing]).to eq [:foo_bar] end end end describe 'complex scenario' do it 'plays well' do obj1 = dummy_class.new obj2 = dummy_class.new obj3 = dummy_class.new obj1_copy = nil obj2_copy = nil obj3_copy = nil obj1.with_namespace do obj1.inheritable_setting.namespace_stackable[:some_thing] = :obj1 expect(obj1.inheritable_setting.namespace_stackable[:some_thing]).to eq [:obj1] obj1_copy = obj1.inheritable_setting.point_in_time_copy end expect(obj1.inheritable_setting.namespace_stackable[:some_thing]).to eq [] expect(obj1_copy.namespace_stackable[:some_thing]).to eq [:obj1] obj2.with_namespace do obj2.inheritable_setting.namespace_stackable[:some_thing] = :obj2 expect(obj2.inheritable_setting.namespace_stackable[:some_thing]).to eq [:obj2] obj2_copy = obj2.inheritable_setting.point_in_time_copy end expect(obj2.inheritable_setting.namespace_stackable[:some_thing]).to eq [] expect(obj2_copy.namespace_stackable[:some_thing]).to eq [:obj2] obj3.with_namespace do obj3.inheritable_setting.namespace_stackable[:some_thing] = :obj3 expect(obj3.inheritable_setting.namespace_stackable[:some_thing]).to eq [:obj3] obj3_copy = obj3.inheritable_setting.point_in_time_copy end expect(obj3.inheritable_setting.namespace_stackable[:some_thing]).to eq [] expect(obj3_copy.namespace_stackable[:some_thing]).to eq [:obj3] # obj1.top_level_setting.inherit_from obj2_copy.point_in_time_copy # obj2.top_level_setting.inherit_from obj3_copy.point_in_time_copy # expect(obj1_copy.namespace_stackable[:some_thing]).to eq %i[obj3 obj2 obj1] end end end ================================================ FILE: spec/grape/dsl/validations_spec.rb ================================================ # frozen_string_literal: true describe Grape::DSL::Validations do subject { dummy_class } let(:dummy_class) do Class.new do extend Grape::DSL::Settings extend Grape::DSL::Validations end end describe '.params' do subject { dummy_class.params { :my_block } } it 'creates a proper Grape::Validations::ParamsScope' do expect(Grape::Validations::ParamsScope).to receive(:new).with(api: dummy_class, type: Hash) do |_func, &block| expect(block.call).to eq(:my_block) end.and_return(:param_scope) expect(subject).to eq(:param_scope) end end describe '.contract' do context 'when contract is nil and blockless' do it 'raises an ArgumentError' do expect { dummy_class.contract }.to raise_error(ArgumentError, 'Either contract or block must be provided') end end context 'when contract is nil and but a block is provided' do it 'returns a proper rape::Validations::ContractScope' do expect(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, nil) do |_func, &block| expect(block.call).to eq(:my_block) end.and_return(:my_contract_scope) expect(dummy_class.contract { :my_block }).to eq(:my_contract_scope) end end context 'when contract is present and blockless' do subject { dummy_class.contract(:my_contract) } before do allow(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, :my_contract).and_return(:my_contract_scope) end it { is_expected.to eq(:my_contract_scope) } end context 'when contract and block are provided' do context 'when contract does not respond to schema' do let(:my_contract) { Class.new } it 'returns a proper rape::Validations::ContractScope' do expect(Grape::Validations::ContractScope).to receive(:new).with(dummy_class, my_contract) do |_func, &block| expect(block.call).to eq(:my_block) end.and_return(:my_contract_scope) expect(dummy_class.contract(my_contract.new) { :my_block }).to eq(:my_contract_scope) end end context 'when contract responds to schema' do let(:my_contract) do Class.new do def schema; end end end it 'raises an ArgumentError' do expect { dummy_class.contract(my_contract.new) { :my_block } }.to raise_error(ArgumentError, 'Cannot inherit from contract, only schema') end end end end end ================================================ FILE: spec/grape/endpoint/declared_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } let(:app) { subject } describe '#declared' do before do subject.format :json subject.params do requires :first optional :second optional :third, default: 'third-default' optional :multiple_types, types: [Integer, String] optional :nested, type: Hash do optional :fourth optional :fifth optional :nested_two, type: Hash do optional :sixth optional :nested_three, type: Hash do optional :seventh end end optional :nested_arr, type: Array do optional :eighth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end optional :arr, type: Array do optional :nineth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_hash_two, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end end context 'when params are not built with default class' do it 'returns an object that corresponds with the params class - hash with indifferent access' do subject.params do build_with :hash_with_indifferent_access end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('ActiveSupport::HashWithIndifferentAccess') end it 'returns an object that corresponds with the params class - hash' do subject.params do build_with :hash end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('Hash') end end it 'shows nil for nested params if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end get '/declared?first=present' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil end it 'shows nil for multiple allowed types if include_missing is true' do subject.get '/declared' do declared(params, include_missing: true) end get '/declared?first=present' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['multiple_types']).to be_nil end it 'does not work in a before filter' do subject.before do declared(params) end subject.get('/declared') { declared(params) } expect { get('/declared') }.to raise_error( Grape::DSL::InsideRoute::MethodNotYetAvailable ) end it 'has as many keys as there are declared params' do subject.get '/declared' do declared(params) end get '/declared?first=present' expect(last_response).to be_successful expect(JSON.parse(last_response.body).keys.size).to eq(12) end it 'has a optional param with default value all the time' do subject.get '/declared' do declared(params) end get '/declared?first=one' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['third']).to eql('third-default') end it 'builds nested params' do subject.get '/declared' do declared(params) end get '/declared?first=present&nested[fourth]=1' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9 end it 'builds arrays correctly' do subject.params do requires :first optional :second, type: Array end subject.post('/declared') { declared(params) } post '/declared', first: 'present', second: ['present'] expect(last_response).to be_created body = JSON.parse(last_response.body) expect(body['second']).to eq(['present']) end it 'builds nested params when given array' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third, default: 'third-default' optional :nested, type: Array do optional :fourth end end subject.get '/declared' do declared(params) end get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested'].size).to eq 2 end context 'when the param is missing and include_missing=false' do before do subject.get('/declared') { declared(params, include_missing: false) } end it 'sets nested objects to be nil' do get '/declared?first=present' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['nested']).to be_nil end end context 'when the param is missing and include_missing=true' do before do subject.get('/declared') { declared(params, include_missing: true) } end it 'sets objects with type=Hash to be a hash' do get '/declared?first=present' expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['empty_hash']).to eq({}) expect(body['empty_hash_two']).to eq({}) expect(body['nested']).to be_a(Hash) expect(body['nested']['empty_hash']).to eq({}) expect(body['nested']['nested_two']).to be_a(Hash) end it 'sets objects with type=Set to be a set' do get '/declared?first=present' expect(last_response).to be_successful json_empty_set = JSON.parse(Set.new.to_json) body = JSON.parse(last_response.body) expect(body['empty_set']).to eq(json_empty_set) expect(body['empty_typed_set']).to eq(json_empty_set) expect(body['nested']['empty_set']).to eq(json_empty_set) expect(body['nested']['empty_typed_set']).to eq(json_empty_set) end it 'sets objects with type=Array to be an array' do get '/declared?first=present' expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['empty_arr']).to eq([]) expect(body['empty_typed_arr']).to eq([]) expect(body['arr']).to eq([]) expect(body['nested']['empty_arr']).to eq([]) expect(body['nested']['empty_typed_arr']).to eq([]) expect(body['nested']['nested_arr']).to eq([]) end it 'includes all declared children when type=Hash' do get '/declared?first=present' expect(last_response).to be_successful body = JSON.parse(last_response.body) expect(body['nested'].keys).to eq(%w[fourth fifth nested_two nested_arr empty_arr empty_typed_arr empty_hash empty_set empty_typed_set]) expect(body['nested']['nested_two'].keys).to eq(%w[sixth nested_three]) expect(body['nested']['nested_two']['nested_three'].keys).to eq(%w[seventh]) end end it 'filters out any additional params that are given' do subject.get '/declared' do declared(params) end get '/declared?first=one&other=two' expect(last_response).to be_successful expect(JSON.parse(last_response.body).key?(:other)).to be false end it 'stringifies if that option is passed' do subject.get '/declared' do declared(params, stringify: true) end get '/declared?first=one&other=two' expect(last_response).to be_successful expect(JSON.parse(last_response.body)['first']).to eq 'one' end it 'does not include missing attributes if that option is passed' do subject.get '/declared' do error! 'expected nil', 400 if declared(params, include_missing: false).key?(:second) '' end get '/declared?first=one&other=two' expect(last_response).to be_successful end it 'does not include renamed missing attributes if that option is passed' do subject.params do optional :renamed_original, as: :renamed end subject.get '/declared' do error! 'expected nil', 400 if declared(params, include_missing: false).key?(:renamed) '' end get '/declared?first=one&other=two' expect(last_response).to be_successful end it 'includes attributes with value that evaluates to false' do subject.params do requires :first optional :boolean end subject.post '/declared' do error!('expected false', 400) if declared(params, include_missing: false)[:boolean] != false '' end post '/declared', Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created end it 'includes attributes with value that evaluates to nil' do subject.params do requires :first optional :second end subject.post '/declared' do error!('expected nil', 400) unless declared(params, include_missing: false)[:second].nil? '' end post '/declared', Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created end it 'includes missing attributes with defaults when there are nested hashes' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third, default: nil optional :nested, type: Hash do optional :fourth, default: nil optional :fifth, default: nil requires :nested_nested, type: Hash do optional :sixth, default: 'sixth-default' optional :seven, default: nil end end end subject.get '/declared' do declared(params, include_missing: false) end get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth' json = JSON.parse(last_response.body) expect(last_response).to be_successful expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth fifth nested_nested] expect(json['nested']['fourth']).to eq '' expect(json['nested']['nested_nested'].keys).to eq %w[sixth seven] expect(json['nested']['nested_nested']['sixth']).to eq 'sixth' end it 'does not include missing attributes when there are nested hashes' do subject.get '/dummy' do end subject.params do requires :first optional :second optional :third optional :nested, type: Hash do optional :fourth optional :fifth end end subject.get '/declared' do declared(params, include_missing: false) end get '/declared?first=present&nested[fourth]=4' json = JSON.parse(last_response.body) expect(last_response).to be_successful expect(json['first']).to eq 'present' expect(json['nested'].keys).to eq %w[fourth] expect(json['nested']['fourth']).to eq '4' end end describe '#declared; call from child namespace' do before do subject.format :json subject.namespace :parent do params do requires :parent_name, type: String end namespace ':parent_name' do params do requires :child_name, type: String requires :child_age, type: Integer end namespace ':child_name' do params do requires :grandchild_name, type: String end get ':grandchild_name' do { 'params' => params, 'without_parent_namespaces' => declared(params, include_parent_namespaces: false), 'with_parent_namespaces' => declared(params, include_parent_namespaces: true) } end end end end get '/parent/foo/bar/baz', child_age: 5, extra: 'hello' end let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) } it { expect(last_response).to be_successful } context 'with include_parent_namespaces: false' do it 'returns declared parameters only from current namespace' do expect(parsed_response[:without_parent_namespaces]).to eq( grandchild_name: 'baz' ) end end context 'with include_parent_namespaces: true' do it 'returns declared parameters from every parent namespace' do expect(parsed_response[:with_parent_namespaces]).to eq( parent_name: 'foo', child_name: 'bar', grandchild_name: 'baz', child_age: 5 ) end end context 'without declaration' do it 'returns all requested parameters' do expect(parsed_response[:params]).to eq( parent_name: 'foo', child_name: 'bar', grandchild_name: 'baz', child_age: 5, extra: 'hello' ) end end end describe '#declared; from a nested mounted endpoint' do before do doubly_mounted = Class.new(Grape::API) doubly_mounted.namespace :more do params do requires :y, type: Integer end route_param :y do get do { params: params, declared_params: declared(params) } end end end mounted = Class.new(Grape::API) mounted.namespace :another do params do requires :mount_space, type: Integer end route_param :mount_space do mount doubly_mounted end end subject.format :json subject.namespace :something do params do requires :id, type: Integer end resource ':id' do mount mounted end end end it 'can access parent attributes' do get '/something/123/another/456/more/789' expect(last_response).to be_successful json = JSON.parse(last_response.body, symbolize_names: true) # test all three levels of params expect(json[:declared_params][:y]).to eq 789 expect(json[:declared_params][:mount_space]).to eq 456 expect(json[:declared_params][:id]).to eq 123 end end describe '#declared; mixed nesting' do before do subject.format :json subject.resource :users do route_param :id, type: Integer, desc: 'ID desc' do # Adding this causes route_setting(:declared_params) to be nil for the # get block in namespace 'foo' below get do end namespace 'foo' do get do { params: params, declared_params: declared(params), declared_params_no_parent: declared(params, include_parent_namespaces: false) } end end end end end it 'can access parent route_param' do get '/users/123/foo', bar: 'bar' expect(last_response).to be_successful json = JSON.parse(last_response.body, symbolize_names: true) expect(json[:declared_params][:id]).to eq 123 expect(json[:declared_params_no_parent][:id]).to be_nil end end describe '#declared; with multiple route_param' do before do mounted = Class.new(Grape::API) mounted.namespace :albums do get do declared(params) end end subject.format :json subject.namespace :artists do route_param :id, type: Integer do get do declared(params) end params do requires :filter, type: String end get :some_route do declared(params) end end route_param :artist_id, type: Integer do namespace :compositions do get do declared(params) end end end route_param :compositor_id, type: Integer do mount mounted end end end it 'return only :id without :artist_id' do get '/artists/1' json = JSON.parse(last_response.body, symbolize_names: true) expect(json).to be_key(:id) expect(json).not_to be_key(:artist_id) end it 'return only :artist_id without :id' do get '/artists/1/compositions' json = JSON.parse(last_response.body, symbolize_names: true) expect(json).to be_key(:artist_id) expect(json).not_to be_key(:id) end it 'return :filter and :id parameters in declared for second enpoint inside route_param' do get '/artists/1/some_route', filter: 'some_filter' json = JSON.parse(last_response.body, symbolize_names: true) expect(json).to be_key(:filter) expect(json).to be_key(:id) expect(json).not_to be_key(:artist_id) end it 'return :compositor_id for mounter in route_param' do get '/artists/1/albums' json = JSON.parse(last_response.body, symbolize_names: true) expect(json).to be_key(:compositor_id) expect(json).not_to be_key(:id) expect(json).not_to be_key(:artist_id) end end describe 'parameter renaming' do context 'with a deeply nested parameter structure' do let(:params) do { i_a: 'a', i_b: { i_c: 'c', i_d: { i_e: { i_f: 'f', i_g: 'g', i_h: [ { i_ha: 'ha1', i_hb: { i_hc: 'c' } }, { i_ha: 'ha2', i_hb: { i_hc: 'c' } } ] } } } } end let(:declared) do { o_a: 'a', o_b: { o_c: 'c', o_d: { o_e: { o_f: 'f', o_g: 'g', o_h: [ { o_ha: 'ha1', o_hb: { o_hc: 'c' } }, { o_ha: 'ha2', o_hb: { o_hc: 'c' } } ] } } } } end let(:params_keys) do [ 'i_a', 'i_b', 'i_b[i_c]', 'i_b[i_d]', 'i_b[i_d][i_e]', 'i_b[i_d][i_e][i_f]', 'i_b[i_d][i_e][i_g]', 'i_b[i_d][i_e][i_h]', 'i_b[i_d][i_e][i_h][i_ha]', 'i_b[i_d][i_e][i_h][i_hb]', 'i_b[i_d][i_e][i_h][i_hb][i_hc]' ] end before do subject.format :json subject.params do optional :i_a, type: String, as: :o_a optional :i_b, type: Hash, as: :o_b do optional :i_c, type: String, as: :o_c optional :i_d, type: Hash, as: :o_d do optional :i_e, type: Hash, as: :o_e do optional :i_f, type: String, as: :o_f optional :i_g, type: String, as: :o_g optional :i_h, type: Array, as: :o_h do optional :i_ha, type: String, as: :o_ha optional :i_hb, type: Hash, as: :o_hb do optional :i_hc, type: String, as: :o_hc end end end end end end subject.post '/test' do declared(params, include_missing: false) end subject.post '/test/no-mod' do before = params.to_h declared(params, include_missing: false) after = params.to_h { before: before, after: after } end end it 'generates the correct parameter names for documentation' do expect(subject.routes.first.params.keys).to match(params_keys) end it 'maps the renamed parameter correctly' do post '/test', **params expect(JSON.parse(last_response.body, symbolize_names: true)).to \ match(declared) end it 'maps no parameters when none are given' do post '/test' expect(JSON.parse(last_response.body)).to match({}) end it 'does not modify the request params' do post '/test/no-mod', **params result = JSON.parse(last_response.body, symbolize_names: true) expect(result[:before]).to match(result[:after]) end end context 'with a renamed root parameter' do before do subject.format :json subject.params do optional :email_address, type: String, regexp: /.+@.+/, as: :email end subject.post '/test' do declared(params, include_missing: false) end end it 'generates the correct parameter names for documentation' do expect(subject.routes.first.params.keys).to match(%w[email_address]) end it 'maps the renamed parameter correctly (original name)' do post '/test', email_address: 'test@example.com' expect(JSON.parse(last_response.body)).to \ match('email' => 'test@example.com') end it 'validates the renamed parameter correctly (original name)' do post '/test', email_address: 'bad[at]example.com' expect(JSON.parse(last_response.body)).to \ match('error' => 'email_address is invalid') end it 'ignores the renamed parameter (as name)' do post '/test', email: 'test@example.com' expect(JSON.parse(last_response.body)).to match({}) end end context 'with a renamed hash with nested parameters' do before do subject.format :json subject.params do optional :address, type: Hash, as: :address_attributes do optional :street, type: String, values: ['Street 1', 'Street 2'], default: 'Street 1' optional :city, type: String end end subject.post '/test' do declared(params, include_missing: false) end end it 'generates the correct parameter names for documentation' do expect(subject.routes.first.params.keys).to \ match(%w[address address[street] address[city]]) end it 'maps the renamed parameter correctly (original name)' do post '/test', address: { city: 'Berlin', street: 'Street 2', t: 't' } expect(JSON.parse(last_response.body)).to \ match('address_attributes' => { 'city' => 'Berlin', 'street' => 'Street 2' }) end it 'validates the renamed parameter correctly (original name)' do post '/test', address: { street: 'unknown' } expect(JSON.parse(last_response.body)).to \ match('error' => 'address[street] does not have a valid value') end it 'ignores the renamed parameter (as name)' do post '/test', address_attributes: { city: 'Berlin', unknown: '1' } expect(JSON.parse(last_response.body)).to match({}) end end context 'with a renamed hash with nested renamed parameter' do before do subject.format :json subject.params do optional :user, type: Hash, as: :user_attributes do optional :email_address, type: String, regexp: /.+@.+/, as: :email end end subject.post '/test' do declared(params, include_missing: false) end end it 'generates the correct parameter names for documentation' do expect(subject.routes.first.params.keys).to \ match(%w[user user[email_address]]) end it 'maps the renamed parameter correctly (original name)' do post '/test', user: { email_address: 'test@example.com' } expect(JSON.parse(last_response.body)).to \ match('user_attributes' => { 'email' => 'test@example.com' }) end it 'validates the renamed parameter correctly (original name)' do post '/test', user: { email_address: 'bad[at]example.com' } expect(JSON.parse(last_response.body)).to \ match('error' => 'user[email_address] is invalid') end it 'ignores the renamed parameter (as name, 1)' do post '/test', user: { email: 'test@example.com' } expect(JSON.parse(last_response.body)).to \ match({ 'user_attributes' => {} }) end it 'ignores the renamed parameter (as name, 2)' do post '/test', user_attributes: { email_address: 'test@example.com' } expect(JSON.parse(last_response.body)).to match({}) end it 'ignores the renamed parameter (as name, 3)' do post '/test', user_attributes: { email: 'test@example.com' } expect(JSON.parse(last_response.body)).to match({}) end end context 'with a renamed field inside `given` block nested in hashes' do before do subject.format :json subject.params do requires :a, type: Hash do optional :c, type: String given :c do requires :b, type: Hash do requires :input_field, as: :output_field end end end end subject.post '/test' do declared(params) end end it 'renames parameter input_field to output_field' do post '/test', { a: { b: { input_field: 'value' }, c: 'value2' } } expect(JSON.parse(last_response.body)).to \ match('a' => { 'b' => { 'output_field' => 'value' }, 'c' => 'value2' }) end end end describe 'optional_array' do subject { last_response } let(:app) do Class.new(Grape::API) do params do requires :z, type: Array do optional :a, type: Integer end end post do declared(params, include_missing: false) end end end before { post '/', { z: [] } } it { is_expected.to be_successful } end end ================================================ FILE: spec/grape/endpoint_spec.rb ================================================ # frozen_string_literal: true describe Grape::Endpoint do subject { Class.new(Grape::API) } def app subject end describe '.before_each' do after { described_class.before_each.clear } it 'is settable via block' do block = ->(_endpoint) { 'noop' } described_class.before_each(&block) expect(described_class.before_each.first).to eq(block) end it 'is settable via reference' do block = ->(_endpoint) { 'noop' } described_class.before_each block expect(described_class.before_each.first).to eq(block) end it 'is able to override a helper' do subject.get('/') { current_user } expect { get '/' }.to raise_error(NameError, /undefined local variable or method [`']current_user'/) described_class.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end get '/' expect(last_response.body).to eq('Bob') described_class.before_each(nil) expect { get '/' }.to raise_error(NameError, /undefined local variable or method [`']current_user'/) end it 'is able to stack helper' do subject.get('/') do authenticate_user! current_user end expect { get '/' }.to raise_error(NoMethodError, /undefined method [`']authenticate_user!' for/) described_class.before_each do |endpoint| allow(endpoint).to receive(:current_user).and_return('Bob') end described_class.before_each do |endpoint| allow(endpoint).to receive(:authenticate_user!).and_return(true) end get '/' expect(last_response.body).to eq('Bob') described_class.before_each(nil) expect { get '/' }.to raise_error(NoMethodError, /undefined method [`']authenticate_user!' for/) end end it 'sets itself in the env upon call' do subject.get('/') { 'Hello world.' } get '/' expect(last_request.env[Grape::Env::API_ENDPOINT]).to be_a(described_class) end describe '#status' do it 'is callable from within a block' do subject.get('/home') do status 206 'Hello' end get '/home' expect(last_response.status).to eq(206) expect(last_response.body).to eq('Hello') end it 'is set as default to 200 for get' do memoized_status = nil subject.get('/home') do memoized_status = status 'Hello' end get '/home' expect(last_response.status).to eq(200) expect(memoized_status).to eq(200) expect(last_response.body).to eq('Hello') end it 'is set as default to 201 for post' do memoized_status = nil subject.post('/home') do memoized_status = status 'Hello' end post '/home' expect(last_response.status).to eq(201) expect(memoized_status).to eq(201) expect(last_response.body).to eq('Hello') end context 'when rescue_from' do subject { last_request.env[Grape::Env::API_ENDPOINT].status } before do post '/' end context 'when :all blockless' do context 'when default_error_status is not set' do let(:app) do Class.new(Grape::API) do rescue_from :all post { raise StandardError } end end it { is_expected.to eq(last_response.status) } end context 'when default_error_status is set' do let(:app) do Class.new(Grape::API) do default_error_status 418 rescue_from :all post { raise StandardError } end end it { is_expected.to eq(last_response.status) } end end context 'when :with' do let(:app) do Class.new(Grape::API) do helpers do def handle_argument_error error!("I'm a teapot!", 418) end end rescue_from ArgumentError, with: :handle_argument_error post { raise ArgumentError } end end it { is_expected.to eq(last_response.status) } end end end describe '#header' do it 'is callable from within a block' do subject.get('/hey') do header 'X-Awesome', 'true' 'Awesome' end get '/hey' expect(last_response.headers['X-Awesome']).to eq('true') end end describe '#headers' do before do subject.get('/headers') do headers.to_json end end let(:headers) do Grape::Util::Header.new.tap do |h| h['Cookie'] = '' h['Host'] = 'example.org' end end it 'includes request headers' do get '/headers' expect(JSON.parse(last_response.body)).to include(headers.to_h) end it 'includes additional request headers' do get '/headers', nil, 'HTTP_X_GRAPE_CLIENT' => '1' x_grape_client_header = 'x-grape-client' expect(JSON.parse(last_response.body)[x_grape_client_header]).to eq('1') end end describe '#cookies' do it 'is callable from within a block' do subject.get('/get/cookies') do cookies['my-awesome-cookie1'] = 'is cool' cookies['my-awesome-cookie2'] = { value: 'is cool too', domain: 'my.example.com', path: '/', secure: true } cookies[:cookie3] = 'symbol' cookies['cookie4'] = 'secret code here' end get('/get/cookies') expect(last_response.cookie_jar).to contain_exactly( { 'name' => 'cookie3', 'value' => 'symbol' }, { 'name' => 'cookie4', 'value' => 'secret code here' }, { 'name' => 'my-awesome-cookie1', 'value' => 'is cool' }, { 'name' => 'my-awesome-cookie2', 'value' => 'is cool too', 'domain' => 'my.example.com', 'path' => '/', 'secure' => true } ) end it 'sets browser cookies and does not set response cookies' do set_cookie %w[username=mrplum sandbox=true] subject.get('/username') do cookies[:username] end get '/username' expect(last_response.body).to eq('mrplum') expect(last_response.cookie_jar).to be_empty end it 'sets and update browser cookies' do set_cookie %w[username=user sandbox=false] subject.get('/username') do cookies[:sandbox] = true if cookies[:sandbox] == 'false' cookies[:username] += '_test' end get '/username' expect(last_response.body).to eq('user_test') expect(last_response.cookie_jar).to contain_exactly( { 'name' => 'sandbox', 'value' => 'true' }, { 'name' => 'username', 'value' => 'user_test' } ) end it 'deletes cookie' do set_cookie %w[delete_this_cookie=1 and_this=2] subject.get('/test') do sum = 0 cookies.each do |name, val| sum += val.to_i cookies.delete name end sum end get '/test' expect(last_response.body).to eq('3') expect(last_response.cookie_jar).to contain_exactly( { 'name' => 'and_this', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) }, { 'name' => 'delete_this_cookie', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) } ) end it 'deletes cookies with path' do set_cookie %w[delete_this_cookie=1 and_this=2] subject.get('/test') do sum = 0 cookies.each do |name, val| sum += val.to_i cookies.delete name, path: '/test' end sum end get '/test' expect(last_response.body).to eq('3') expect(last_response.cookie_jar).to contain_exactly( { 'name' => 'and_this', 'path' => '/test', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) }, { 'name' => 'delete_this_cookie', 'path' => '/test', 'value' => '', 'max-age' => 0, 'expires' => Time.at(0) } ) end end describe '#params' do context 'default class' do it 'is a ActiveSupport::HashWithIndifferentAccess' do subject.get '/foo' do params.class end get '/foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end end context 'sets a value to params' do it 'params' do subject.params do requires :a, type: String end subject.get '/foo' do params[:a] = 'bar' end get '/foo', a: 'foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('bar') end end end describe '#params' do it 'is available to the caller' do subject.get('/hey') do params[:howdy] end get '/hey?howdy=hey' expect(last_response.body).to eq('hey') end it 'parses from path segments' do subject.get('/hey/:id') do params[:id] end get '/hey/12' expect(last_response.body).to eq('12') end it 'deeply converts nested params' do subject.get '/location' do params[:location][:city] end get '/location?location[city]=Dallas' expect(last_response.body).to eq('Dallas') end context 'with special requirements' do it 'parses email param with provided requirements for params' do subject.get('/:person_email', requirements: { person_email: /.*/ }) do params[:person_email] end get '/someone@example.com' expect(last_response.body).to eq('someone@example.com') get 'someone@example.com.pl' expect(last_response.body).to eq('someone@example.com.pl') end it 'parses many params with provided regexps' do subject.get('/:person_email/test/:number', requirements: { person_email: /someone@(.*).com/, number: /[0-9]/ }) do params[:person_email] << params[:number] end get '/someone@example.com/test/1' expect(last_response.body).to eq('someone@example.com1') get '/someone@testing.wrong/test/1' expect(last_response.status).to eq(404) get 'someone@test.com/test/wrong_number' expect(last_response.status).to eq(404) get 'someone@test.com/wrong_middle/1' expect(last_response.status).to eq(404) end context 'namespace requirements' do before do subject.namespace :outer, requirements: { person_email: /abc@(.*).com/ } do get('/:person_email') do params[:person_email] end namespace :inner, requirements: { number: /[0-9]/, person_email: /someone@(.*).com/ } do get '/:person_email/test/:number' do params[:person_email] << params[:number] end end end end it 'parse email param with provided requirements for params' do get '/outer/abc@example.com' expect(last_response.body).to eq('abc@example.com') end it "overrides outer namespace's requirements" do get '/outer/inner/someone@testing.wrong/test/1' expect(last_response.status).to eq(404) get '/outer/inner/someone@testing.com/test/1' expect(last_response.status).to eq(200) expect(last_response.body).to eq('someone@testing.com1') end end end context 'from body parameters' do before do subject.post '/request_body' do params[:user] end subject.put '/request_body' do params[:user] end end it 'converts JSON bodies to params' do post '/request_body', Grape::Json.dump(user: 'Bobby T.'), 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('Bobby T.') end it 'does not convert empty JSON bodies to params' do put '/request_body', '', 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('') end if Object.const_defined? :MultiXml it 'converts XML bodies to params' do post '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('Bobby T.') end it 'converts XML bodies to params' do put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq('Bobby T.') end else let(:body) { 'Bobby T.' } it 'converts XML bodies to params' do post '/request_body', body, 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq(Grape::Xml.parse(body)['user'].to_s) end it 'converts XML bodies to params' do put '/request_body', body, 'CONTENT_TYPE' => 'application/xml' expect(last_response.body).to eq(Grape::Xml.parse(body)['user'].to_s) end end it 'does not include parameters not defined by the body' do subject.post '/omitted_params' do error! 400, 'expected nil' if params[:version] params[:user] end post '/omitted_params', Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) expect(last_response.body).to eq('Bob') end # Rack swallowed this error until v2.2.0 it 'returns a 400 if given an invalid multipart body', if: Gem::Version.new(Rack.release) >= Gem::Version.new('2.2.0') do subject.params do requires :file, type: Rack::Multipart::UploadedFile end subject.post '/upload' do params[:file][:filename] end post '/upload', { file: '' }, 'CONTENT_TYPE' => 'multipart/form-data; boundary=foobar' expect(last_response.status).to eq(400) expect(last_response.body).to eq('file is invalid') end end context 'when the limit on multipart files is exceeded' do around do |example| limit = Rack::Utils.multipart_part_limit Rack::Utils.multipart_part_limit = 1 example.run Rack::Utils.multipart_part_limit = limit end it 'returns a 413 if given too many multipart files' do subject.params do requires :file, type: Rack::Multipart::UploadedFile end subject.post '/upload' do params[:file][:filename] end post '/upload', { file: Rack::Test::UploadedFile.new(__FILE__, 'text/plain'), extra: Rack::Test::UploadedFile.new(__FILE__, 'text/plain') } expect(last_response.status).to eq(413) expect(last_response.body).to eq("the number of uploaded files exceeded the system's configured limit (1)") end end it 'responds with a 415 for an unsupported content-type' do subject.format :json # subject.content_type :json, "application/json" subject.put '/request_body' do params[:user] end put '/request_body', 'Bobby T.', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq(415) expect(last_response.body).to eq('{"error":"The provided content-type \'application/xml\' is not supported."}') end it 'does not accept text/plain in JSON format if application/json is specified as content type' do subject.format :json subject.default_format :json subject.put '/request_body' do params[:user] end put '/request_body', Grape::Json.dump(user: 'Bob'), 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq(415) expect(last_response.body).to eq('{"error":"The provided content-type \'text/plain\' is not supported."}') end context 'content type with params' do before do subject.format :json subject.content_type :json, 'application/json; charset=utf-8' subject.post do params[:data] end post '/', Grape::Json.dump(data: { some: 'payload' }), 'CONTENT_TYPE' => 'application/json' end it 'does not response with 406 for same type without params' do expect(last_response.status).not_to be 406 end it 'responses with given content type in headers' do expect(last_response.content_type).to eq 'application/json; charset=utf-8' end end context 'precedence' do before do subject.format :json subject.namespace '/:id' do get do { params: params[:id] } end post do { params: params[:id] } end put do { params: params[:id] } end end end it 'route string params have higher precedence than body params' do post '/123', { id: 456 }.to_json expect(JSON.parse(last_response.body)['params']).to eq '123' put '/123', { id: 456 }.to_json expect(JSON.parse(last_response.body)['params']).to eq '123' end it 'route string params have higher precedence than URL params' do get '/123?id=456' expect(JSON.parse(last_response.body)['params']).to eq '123' post '/123?id=456' expect(JSON.parse(last_response.body)['params']).to eq '123' end end context 'sets a value to params' do it 'params' do subject.params do requires :a, type: String end subject.get '/foo' do params[:a] = 'bar' end get '/foo', a: 'foo' expect(last_response.status).to eq(200) expect(last_response.body).to eq('bar') end end end describe '#error!' do it 'accepts a message' do subject.get('/hey') do error! 'This is not valid.' 'This is valid.' end get '/hey' expect(last_response.status).to eq(500) expect(last_response.body).to eq('This is not valid.') end it 'accepts a code' do subject.desc 'patate' do http_codes [[401, 'Unauthorized']] end subject.get('/hey') do error! 'Unauthorized.', 401 end get '/hey' expect(last_response.status).to eq(401) expect(last_response.body).to eq('Unauthorized.') end it 'accepts an object and render it in format' do subject.get '/hey' do error!({ 'dude' => 'rad' }, 403) end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.body).to eq('{"dude":"rad"}') end it 'accepts a frozen object' do subject.get '/hey' do error!({ 'dude' => 'rad' }.freeze, 403) end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.body).to eq('{"dude":"rad"}') end it 'can specifiy headers' do subject.get '/hey' do error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value') end get '/hey.json' expect(last_response.status).to eq(403) expect(last_response.headers['X-Custom']).to eq('value') end it 'merges additional headers with headers set before call' do subject.before do header 'X-Before-Test', 'before-sample' end subject.get '/hey' do header 'X-Test', 'test-sample' error!({ 'dude' => 'rad' }, 403, 'X-Error' => 'error') end get '/hey.json' expect(last_response.headers['X-Before-Test']).to eq('before-sample') expect(last_response.headers['X-Test']).to eq('test-sample') expect(last_response.headers['X-Error']).to eq('error') end it 'does not merges additional headers with headers set after call' do subject.after do header 'X-After-Test', 'after-sample' end subject.get '/hey' do error!({ 'dude' => 'rad' }, 403, 'X-Error' => 'error') end get '/hey.json' expect(last_response.headers['X-Error']).to eq('error') expect(last_response.headers['X-After-Test']).to be_nil end it 'sets the status code for the endpoint' do memoized_endpoint = nil subject.get '/hey' do memoized_endpoint = self error!({ 'dude' => 'rad' }, 403, 'X-Custom' => 'value') end get '/hey.json' expect(memoized_endpoint.status).to eq(403) end end describe '#redirect' do it 'redirects to a url with status 302' do subject.get('/hey') do redirect '/ha' end get '/hey' expect(last_response.status).to eq 302 expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved temporarily to /ha.' end it 'has status code 303 if it is not get request and it is http 1.1' do subject.post('/hey') do redirect '/ha' end post '/hey', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' expect(last_response.status).to eq 303 expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'An alternate resource is located at /ha.' end it 'support permanent redirect' do subject.get('/hey') do redirect '/ha', permanent: true end get '/hey' expect(last_response.status).to eq 301 expect(last_response.location).to eq '/ha' expect(last_response.body).to eq 'This resource has been moved permanently to /ha.' end it 'allows for an optional redirect body override' do subject.get('/hey') do redirect '/ha', body: 'test body' end get '/hey' expect(last_response.body).to eq 'test body' end end describe 'NameError' do context 'when referencing an undefined local variable or method' do it 'raises NameError but stripping the internals of the Grape::Endpoint class and including the API route' do subject.get('/hey') { undefined_helper } expect { get '/hey' }.to raise_error(NameError, /^undefined local variable or method ['`]undefined_helper' for/) end end end it 'does not persist params between calls' do subject.post('/new') do params[:text] end post '/new', text: 'abc' expect(last_response.body).to eq('abc') post '/new', text: 'def' expect(last_response.body).to eq('def') end it 'resets all instance variables (except block) between calls' do subject.helpers do def memoized @memoized ||= params[:howdy] end end subject.get('/hello') do memoized end get '/hello?howdy=hey' expect(last_response.body).to eq('hey') get '/hello?howdy=yo' expect(last_response.body).to eq('yo') end context 'when calling return' do it 'does not raise a LocalJumpError' do subject.get('/home') do return 'Hello' end get '/home' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello') end end context 'filters' do describe 'before filters' do it 'runs the before filter if set' do subject.before { env['before_test'] = 'OK' } subject.get('/before_test') { env['before_test'] } get '/before_test' expect(last_response.body).to eq('OK') end end describe 'after filters' do it 'overrides the response body if it sets it' do subject.after { body 'after' } subject.get('/after_test') { 'during' } get '/after_test' expect(last_response.body).to eq('after') end it 'does not override the response body with its return' do subject.after { 'after' } subject.get('/after_test') { 'body' } get '/after_test' expect(last_response.body).to eq('body') end end it 'allows adding to response with present' do subject.format :json subject.before { present :before, 'before' } subject.before_validation { present :before_validation, 'before_validation' } subject.after_validation { present :after_validation, 'after_validation' } subject.after { present :after, 'after' } subject.get :all_filters do present :endpoint, 'endpoint' end get '/all_filters' json = JSON.parse(last_response.body) expect(json.keys).to match_array %w[before before_validation after_validation endpoint after] end context 'when terminating the response with error!' do it 'breaks normal call chain' do called = [] subject.before { called << 'before' } subject.before_validation { called << 'before_validation' } subject.after_validation { error! :oops, 500 } subject.after { called << 'after' } subject.get :error_filters do called << 'endpoint' '' end get '/error_filters' expect(last_response.status).to be 500 expect(called).to match_array %w[before before_validation] end it 'allows prior and parent filters of same type to run' do called = [] subject.before { called << 'parent' } subject.namespace :parent do before { called << 'prior' } before { error! :oops, 500 } before { called << 'subsequent' } get :hello do called << :endpoint 'Hello!' end end get '/parent/hello' expect(last_response.status).to be 500 expect(called).to match_array %w[parent prior] end end end context 'anchoring' do describe 'delete 204' do it 'allows for the anchoring option with a delete method' do subject.delete('/example', anchor: true) delete '/example/and/some/more' expect(last_response).to be_not_found end it 'anchors paths by default for the delete method' do subject.delete '/example' delete '/example/and/some/more' expect(last_response).to be_not_found end it 'responds to /example/and/some/more for the non-anchored delete method' do subject.delete '/example', anchor: false delete '/example/and/some/more' expect(last_response).to be_no_content expect(last_response.body).to be_empty end end describe 'delete 200, with response body' do it 'responds to /example/and/some/more for the non-anchored delete method' do subject.delete('/example', anchor: false) do status 200 body 'deleted' end delete '/example/and/some/more' expect(last_response).to be_successful expect(last_response.body).not_to be_empty end end describe 'delete 200, with a return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { 'deleted' } delete '/example' expect(last_response.status).to be 200 expect(last_response.body).not_to be_empty end end describe 'delete 204, with nil has return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { nil } delete '/example' expect(last_response.status).to be 204 expect(last_response.body).to be_empty end end describe 'delete 204, with empty array has return value (no explicit body)' do it 'responds to /example delete method' do subject.delete(:example) { '' } delete '/example' expect(last_response.status).to be 204 expect(last_response.body).to be_empty end end describe 'all other' do %w[post get head put options patch].each do |verb| it "allows for the anchoring option with a #{verb.upcase} method" do subject.__send__(verb, '/example', anchor: true) do verb end __send__(verb, '/example/and/some/more') expect(last_response.status).to be 404 end it "anchors paths by default for the #{verb.upcase} method" do subject.__send__(verb, '/example') do verb end __send__(verb, '/example/and/some/more') expect(last_response.status).to be 404 end it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do subject.__send__(verb, '/example', anchor: false) do verb end __send__(verb, '/example/and/some/more') expect(last_response.status).to eql verb == 'post' ? 201 : 200 expect(last_response.body).to eql verb == 'head' ? '' : verb end end end end context 'request' do it 'is set to the url requested' do subject.get('/url') do request.url end get '/url' expect(last_response.body).to eq('http://example.org/url') end ['v1', :v1].each do |version| it "includes version #{version}" do subject.version version, using: :path subject.get('/url') do request.url end get "/#{version}/url" expect(last_response.body).to eq("http://example.org/#{version}/url") end end it 'includes prefix' do subject.version 'v1', using: :path subject.prefix 'api' subject.get('/url') do request.url end get '/api/v1/url' expect(last_response.body).to eq('http://example.org/api/v1/url') end end context 'version headers' do before do # NOTE: a 404 is returned instead of the 406 if cascade: false is not set. subject.version 'v1', using: :header, vendor: 'ohanapi', cascade: false subject.get '/test' do 'Hello!' end end it 'result in a 406 response if they are invalid' do get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json' expect(last_response.status).to eq(406) end it 'result in a 406 response if they cannot be parsed' do get '/test', {}, 'HTTP_ACCEPT' => 'application/vnd.ohanapi.v1+json; version=1' expect(last_response.status).to eq(406) end end context 'binary' do before do subject.get do stream FileStreamer.new(__FILE__) end end it 'suports stream objects in response' do get '/' expect(last_response.status).to eq 200 expect(last_response.body).to eq File.read(__FILE__) end end context 'validation errors' do before do subject.before do header['Access-Control-Allow-Origin'] = '*' end subject.params do requires :id, type: String end subject.get do 'should not get here' end end it 'returns the errors, and passes headers' do get '/' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'id is missing' expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') end end context 'instrumentation' do before do subject.before do # Placeholder end subject.get do 'hello' end @events = [] @subscriber = ActiveSupport::Notifications.subscribe(/grape/) do |*args| @events << ActiveSupport::Notifications::Event.new(*args) end end after do ActiveSupport::Notifications.unsubscribe(@subscriber) end it 'notifies AS::N' do get '/' # In order that the events finalized (time each block ended) expect(@events).to contain_exactly( have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), validators: [], request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :finally }), have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(described_class), env: an_instance_of(Hash) }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) ) # In order that events were initialized expect(@events.sort_by(&:time)).to contain_exactly( have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(described_class), env: an_instance_of(Hash) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: a_collection_containing_exactly(an_instance_of(Proc)), type: :before }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :before_validation }), have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class), validators: [], request: a_kind_of(Grape::Request) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after_validation }), have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :after }), have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class), filters: [], type: :finally }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) ) end end describe '#inspect' do subject { described_class.new(settings, **options).inspect } let(:options) do { method: :path, path: '/path', app: {}, route_options: { anchor: false }, forward_match: true, for: Class.new } end let(:settings) { Grape::Util::InheritableSetting.new } it 'does not raise an error' do expect { subject }.not_to raise_error end end end ================================================ FILE: spec/grape/exceptions/base_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::Base do describe '#to_s' do subject { described_class.new(message: message).to_s } let(:message) { 'a_message' } it { is_expected.to eq(message) } end describe '#message' do subject { described_class.new(message: message).message } let(:message) { 'a_message' } it { is_expected.to eq(message) } end describe '#compose_message' do subject { described_class.new.__send__(:compose_message, key, **attributes) } let(:key) { :invalid_formatter } let(:attributes) { { klass: String, to_format: 'xml' } } after { I18n.reload! } context 'when I18n enforces available locales' do context 'when the fallback locale is available' do around do |example| I18n.available_locales = %i[de en] I18n.with_locale(:de) { example.run } ensure I18n.available_locales = %i[en] end it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end context 'when the fallback locale is not available' do around do |example| I18n.available_locales = %i[de jp] I18n.with_locale(:de) do example.run ensure I18n.available_locales = %i[en] end end it 'returns the scoped translation key as a string' do expect(subject).to eq("grape.errors.messages.#{key}") end end end context 'when I18n does not enforce available locales' do around do |example| I18n.enforce_available_locales = false example.run ensure I18n.enforce_available_locales = true end context 'when the fallback locale is available' do around do |example| I18n.available_locales = %i[de en] I18n.with_locale(:de) { example.run } ensure I18n.available_locales = %i[en] end it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end context 'when the fallback locale is not available' do around do |example| I18n.available_locales = %i[de jp] I18n.with_locale(:de) { example.run } ensure I18n.available_locales = %i[en] end it 'returns the translated message' do expect(subject).to eq('cannot convert String to xml') end end end end end ================================================ FILE: spec/grape/exceptions/body_parse_errors_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::ValidationErrors do context 'api with rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.rescue_from :all do |_e| error! 'message was processed', 400 end subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'with content_type json' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with content_type xml' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with content_type text' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'with no specific content_type' do it 'can recover from failed body parsing' do post '/beer', 'test', {} expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end end context 'api with rescue_from :grape_exceptions handler' do subject { Class.new(Grape::API) } before do subject.rescue_from :all do |_e| error! 'message was processed', 400 end subject.rescue_from :grape_exceptions subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'with content_type json' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' end end context 'with content_type xml' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' end end end context 'api with rescue_from :grape_exceptions handler with block' do subject { Class.new(Grape::API) } before do subject.rescue_from :grape_exceptions do |e| error! "Custom Error Contents, Original Message: #{e.message}", 400 end subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'with content_type json' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' expect(last_response.body).to include 'Custom Error Contents, Original Message' end end context 'with content_type xml' do it 'returns body parsing error message' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to include 'message body does not match declared format' expect(last_response.body).to include 'Custom Error Contents, Original Message' end end end context 'api without a rescue handler' do subject { Class.new(Grape::API) } before do subject.params do requires :beer end subject.post '/beer' do 'beer received' end end def app subject end context 'and with content_type json' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq 400 expect(last_response.body).to include('message body does not match declared format') expect(last_response.body).to include('application/json') end end context 'with content_type xml' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml' expect(last_response.status).to eq 400 expect(last_response.body).to include('message body does not match declared format') expect(last_response.body).to include('application/xml') end end context 'with content_type text' do it 'can recover from failed body parsing' do post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain' expect(last_response.status).to eq 400 expect(last_response.body).to eq('beer is missing') end end context 'and with no specific content_type' do it 'can recover from failed body parsing' do post '/beer', 'test', {} expect(last_response.status).to eq 400 # plain response with text/html expect(last_response.body).to eq('beer is missing') end end end end ================================================ FILE: spec/grape/exceptions/invalid_accept_header_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::InvalidAcceptHeader do shared_examples_for 'a valid request' do it 'does return with status 200' do expect(last_response.status).to eq 200 end it 'does return the expected result' do expect(last_response.body).to eq('beer received') end end shared_examples_for 'a cascaded request' do it 'does not find a matching route' do expect(last_response.status).to eq 404 end end shared_examples_for 'a not-cascaded request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers).not_to have_key('X-Cascade') end it 'does not accept the request' do expect(last_response.status).to eq 406 end end shared_examples_for 'a rescued request' do it 'does not include the X-Cascade=pass header' do expect(last_response.headers['X-Cascade']).to be_nil end it 'does show rescue handler processing' do expect(last_response.status).to eq 400 expect(last_response.body).to eq('message was processed') end end context 'API with cascade=false and rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| error! 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a rescued request' end end end context 'API with cascade=false and without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end end end context 'API with cascade=false and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.rescue_from :all do |e| error! 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a rescued request' end end end context 'API with cascade=false, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: false subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a not-cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a not-cascaded request' end end end context 'API with cascade=true and rescue_from :all handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| error! 'message was processed', 400, e[:headers] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a cascaded request' end end end context 'API with cascade=true and without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end end end context 'API with cascade=true and with rescue_from :all handler and http_codes' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.rescue_from :all do |e| error! 'message was processed', 400, e[:headers] end subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do before do get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99', 'CONTENT_TYPE' => 'application/json' end it_behaves_like 'a cascaded request' end end end context 'API with cascade=true, http_codes but without a rescue handler' do subject { Class.new(Grape::API) } before do subject.version 'v99', using: :header, vendor: 'vendorname', format: :json, cascade: true subject.desc 'Get beer' do failure [[400, 'Bad Request'], [401, 'Unauthorized'], [403, 'Forbidden'], [404, 'Resource not found'], [406, 'API vendor or version not found'], [500, 'Internal processing error']] end subject.get '/beer' do 'beer received' end end def app subject end context 'that received a request with correct vendor and version' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v99' } it_behaves_like 'a valid request' end context 'that receives' do context 'an invalid version in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.vendorname-v77' } it_behaves_like 'a cascaded request' end context 'an invalid vendor in the request' do before { get '/beer', {}, 'HTTP_ACCEPT' => 'application/vnd.invalidvendor-v99' } it_behaves_like 'a cascaded request' end end end end ================================================ FILE: spec/grape/exceptions/invalid_formatter_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::InvalidFormatter do describe '#message' do let(:error) do described_class.new(String, 'xml') end it 'contains the problem in the message' do expect(error.message).to include( 'cannot convert String to xml' ) end end end ================================================ FILE: spec/grape/exceptions/invalid_response_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::InvalidResponse do describe '#message' do let(:error) { described_class.new } it 'contains the problem in the message' do expect(error.message).to include('Invalid response') end end end ================================================ FILE: spec/grape/exceptions/invalid_versioner_option_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::InvalidVersionerOption do describe '#message' do let(:error) do described_class.new('headers') end it 'contains the problem in the message' do expect(error.message).to include( 'unknown :using for versioner: headers' ) end end end ================================================ FILE: spec/grape/exceptions/missing_group_type_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Grape::Exceptions::MissingGroupType do describe '#message' do subject { described_class.new.message } it { is_expected.to include 'group type is required' } end end ================================================ FILE: spec/grape/exceptions/missing_mime_type_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::MissingMimeType do describe '#message' do let(:error) do described_class.new('new_json') end it 'contains the problem in the message' do expect(error.message).to include 'missing mime type for new_json' end it 'contains the resolution in the message' do expect(error.message).to include "or add your own with content_type :new_json, 'application/new_json' " end end end ================================================ FILE: spec/grape/exceptions/unknown_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::UnknownValidator do describe '#message' do let(:error) do described_class.new('gt_10') end it 'contains the problem in the message' do expect(error.message).to include( 'unknown validator: gt_10' ) end end end ================================================ FILE: spec/grape/exceptions/unsupported_group_type_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Grape::Exceptions::UnsupportedGroupType do subject { described_class.new } describe '#message' do subject { described_class.new.message } it { is_expected.to include 'group type must be Array, Hash, JSON or Array[JSON]' } end end ================================================ FILE: spec/grape/exceptions/validation_errors_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::ValidationErrors do let(:validation_message) { 'FooBar is invalid' } let(:validation_error) { instance_double Grape::Exceptions::Validation, params: [validation_message], message: '' } context 'initialize' do subject do described_class.new(errors: [validation_error], headers: headers) end let(:headers) do { 'A-Header-Key' => 'A-Header-Value' } end it 'assigns headers through base class' do expect(subject.headers).to eq(headers) end end context 'message' do context 'is not repeated' do subject(:message) { error.message.split(',').map(&:strip) } let(:error) do described_class.new(errors: [validation_error, validation_error]) end it { expect(message).to include validation_message } it { expect(message.size).to eq 1 } end end describe '#full_messages' do context 'with errors' do subject { described_class.new(errors: [validation_error_1, validation_error_2]).full_messages } let(:validation_error_1) { Grape::Exceptions::Validation.new(params: ['id'], message: :presence) } let(:validation_error_2) { Grape::Exceptions::Validation.new(params: ['name'], message: :presence) } it 'returns an array with each errors full message' do expect(subject).to contain_exactly('id is missing', 'name is missing') end end context 'when attributes is an array of symbols' do subject { described_class.new(errors: [validation_error]).full_messages } let(:validation_error) { Grape::Exceptions::Validation.new(params: [:admin_field], message: 'Can not set admin-only field') } it 'returns an array with an error full message' do expect(subject.first).to eq('admin_field Can not set admin-only field') end end end context 'api' do subject { Class.new(Grape::API) } def app subject end it 'can return structured json with separate fields' do subject.format :json subject.rescue_from described_class do |e| error!(e, 400) end subject.params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end subject.get '/exactly_one_of' do 'exactly_one_of works!' end get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response).to be_bad_request expect(JSON.parse(last_response.body)).to eq( [ 'params' => %w[beer wine], 'messages' => ['are mutually exclusive'] ] ) end end end ================================================ FILE: spec/grape/exceptions/validation_spec.rb ================================================ # frozen_string_literal: true describe Grape::Exceptions::Validation do it 'fails when params are missing' do expect { described_class.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) end context 'when message is a Symbol' do subject(:error) { described_class.new(params: ['id'], message: :presence) } it 'stores message_key' do expect(error.message_key).to eq(:presence) end it 'translates the message' do expect(error.message).to eq('is missing') end end context 'when message is a Hash' do subject(:error) { described_class.new(params: ['id'], message: { key: :between, min: 2, max: 10 }) } before do I18n.backend.store_translations(:en, grape: { errors: { messages: { between: 'must be between %s and %s' } } }) end after { I18n.reload! } it 'stores the :key entry as message_key' do expect(error.message_key).to eq(:between) end it 'translates the message with interpolation params' do expect(error.message).to eq('must be between 2 and 10') end end context 'when message is a Proc' do it 'calls the proc to produce the message' do expect(described_class.new(params: ['id'], message: -> { 'computed' }).message).to eq('computed') end end context 'when message is a String' do subject(:error) { described_class.new(params: ['id'], message: 'raw message') } it 'does not store the message_key' do expect(error.message_key).to be_nil end it 'returns the string as-is' do expect(error.message).to eq('raw message') end end end ================================================ FILE: spec/grape/integration/global_namespace_function_spec.rb ================================================ # frozen_string_literal: true # see https://github.com/ruby-grape/grape/issues/1348 def namespace raise end describe Grape::API do subject do Class.new(Grape::API) do format :json get do { ok: true } end end end def app subject end context 'with a global namespace function' do it 'works' do get '/' expect(last_response.status).to eq 200 end end end ================================================ FILE: spec/grape/integration/rack_sendfile_spec.rb ================================================ # frozen_string_literal: true describe Rack::Sendfile do subject do content_object = file_object app = Class.new(Grape::API) do use Rack::Sendfile, 'X-Accel-Redirect' format :json get do if content_object.is_a?(String) sendfile content_object else stream content_object end end end options = { method: Rack::GET, 'HTTP_X_ACCEL_MAPPING' => '/accel/mapping/=/replaced/' } env = Rack::MockRequest.env_for('/', options) app.call(env) end context 'when calling sendfile' do let(:file_object) do '/accel/mapping/some/path' end it 'contains Sendfile headers' do headers = subject[1] expect(headers).to include('X-Accel-Redirect') end end context 'when streaming non file content' do let(:file_object) do double(:file_object, each: nil) end it 'not contains Sendfile headers' do headers = subject[1] expect(headers).not_to include('X-Accel-Redirect') end end end ================================================ FILE: spec/grape/integration/rack_spec.rb ================================================ # frozen_string_literal: true describe Rack do describe 'from a Tempfile' do subject { last_response.body } let(:app) do Class.new(Grape::API) do format :json params do requires :file, type: File end post do params[:file].then do |file| { filename: file[:filename], type: file[:type], content: file[:tempfile].read } end end end end let(:response_body) do { filename: File.basename(tempfile.path), type: 'text/plain', content: 'rubbish' }.to_json end let(:tempfile) do Tempfile.new.tap do |t| t.write('rubbish') t.rewind end end before do post '/', file: Rack::Test::UploadedFile.new(tempfile.path, 'text/plain') end it 'correctly populates params from a Tempfile' do expect(subject).to eq(response_body) ensure tempfile.close! end end context 'when the app is mounted' do let(:ping_mount) do Class.new(Grape::API) do get 'ping' end end let(:app) do app_to_mount = ping_mount Class.new(Grape::API) do namespace 'namespace' do mount app_to_mount end end end it 'finds the app on the namespace' do get '/namespace/ping' expect(last_response).to be_successful end end # https://github.com/ruby-grape/grape/issues/2576 describe 'when an api is mounted' do let(:api) do Class.new(Grape::API) do format :json version 'v1', using: :path resource :system do get :ping do { message: 'pong' } end end end end let(:parent_api) do api_to_mount = api Class.new(Grape::API) do format :json namespace '/api' do mount api_to_mount end end end it 'is not polluted with the parent namespace' do env = Rack::MockRequest.env_for('/v1/api/system/ping', method: 'GET') response = Rack::MockResponse[*parent_api.call(env)] expect(response.status).to eq(200) parsed_body = JSON.parse(response.body) expect(parsed_body['message']).to eq('pong') end it 'can call the api' do env = Rack::MockRequest.env_for('/v1/system/ping', method: 'GET') response = Rack::MockResponse[*api.call(env)] expect(response.status).to eq(200) parsed_body = JSON.parse(response.body) expect(parsed_body['message']).to eq('pong') end end end ================================================ FILE: spec/grape/loading_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do subject do context = self Class.new(Grape::API) do format :json mount context.combined_api => '/' end end let(:jobs_api) do Class.new(Grape::API) do namespace :one do namespace :two do namespace :three do get :one do end get :two do end end end end end end let(:combined_api) do context = self Class.new(Grape::API) do version :v1, using: :accept_version_header, cascade: true mount context.jobs_api end end def app subject end it 'execute first request in reasonable time' do started = Time.now get '/mount1/nested/test_method' expect(Time.now - started).to be < 5 end end ================================================ FILE: spec/grape/middleware/auth/base_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Auth::Base do subject do Class.new(Grape::API) do http_basic realm: 'my_realm' do |user, password| user && password && user == password end get '/authorized' do 'DONE' end end end let(:app) { subject } it 'authenticates if given valid creds' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') expect(last_response).to be_successful expect(last_response.body).to eq('DONE') end it 'throws a 401 is wrong auth is given' do get '/authorized', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') expect(last_response).to be_unauthorized end end ================================================ FILE: spec/grape/middleware/auth/dsl_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Auth::DSL do subject { Class.new(Grape::API) } let(:block) { -> {} } let(:settings) do { opaque: 'secret', proc: block, realm: 'API Authorization', type: :http_digest } end describe '.auth' do it 'sets auth parameters' do expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] expect(subject.auth).to eq(settings) end it 'can be called multiple times' do expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings) expect(subject.base_instance).to receive(:use).with(Grape::Middleware::Auth::Base, settings.merge(realm: 'super_secret')) subject.auth :http_digest, realm: settings[:realm], opaque: settings[:opaque], &settings[:proc] first_settings = subject.auth subject.auth :http_digest, realm: 'super_secret', opaque: settings[:opaque], &settings[:proc] expect(subject.auth).to eq(settings.merge(realm: 'super_secret')) expect(subject.auth.object_id).not_to eq(first_settings.object_id) end end describe '.http_basic' do it 'sets auth parameters' do subject.http_basic realm: 'my_realm', &settings[:proc] expect(subject.auth).to eq(realm: 'my_realm', type: :http_basic, proc: block) end end describe '.http_digest' do context 'when realm is a hash' do it 'sets auth parameters' do subject.http_digest realm: { realm: 'my_realm', opaque: 'my_opaque' }, &settings[:proc] expect(subject.auth).to eq(realm: { realm: 'my_realm', opaque: 'my_opaque' }, type: :http_digest, proc: block) end end context 'when realm is not hash' do it 'sets auth parameters' do subject.http_digest realm: 'my_realm', opaque: 'my_opaque', &settings[:proc] expect(subject.auth).to eq(realm: 'my_realm', type: :http_digest, proc: block, opaque: 'my_opaque') end end end end ================================================ FILE: spec/grape/middleware/auth/strategies_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Auth::Strategies do describe 'Basic Auth' do let(:app) do proc = ->(u, p) { u && p && u == p } Rack::Builder.app do use Grape::Middleware::Error use(Grape::Middleware::Auth::Base, type: :http_basic, proc: proc) run ->(_env) { [200, {}, ['Hello there.']] } end end it 'throws a 401 if no auth is given' do get '/whatever' expect(last_response).to be_unauthorized end it 'authenticates if given valid creds' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'admin') expect(last_response).to be_successful expect(last_response.body).to eq('Hello there.') end it 'throws a 401 is wrong auth is given' do get '/whatever', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('admin', 'wrong') expect(last_response).to be_unauthorized end end describe 'Unknown Auth' do context 'when type is not register' do let(:app) do Class.new(Grape::API) do use Grape::Middleware::Auth::Base, type: :unknown get('/whatever') { 'Hello there.' } end end it 'throws a 401' do expect { get '/whatever' }.to raise_error(Grape::Exceptions::UnknownAuthStrategy, 'unknown auth strategy: unknown') end end end end ================================================ FILE: spec/grape/middleware/base_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Base do subject { described_class.new(blank_app) } let(:blank_app) { ->(_) { [200, {}, 'Hi there.'] } } before do # Keep it one object for testing. allow(subject).to receive(:dup).and_return(subject) end it 'has the app as an accessor' do expect(subject.app).to eq(blank_app) end it 'calls through to the app' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'callbacks' do after { subject.call!({}) } it 'calls #before' do expect(subject).to receive(:before) end it 'calls #after' do expect(subject).to receive(:after) end end context 'callbacks on error' do let(:blank_app) { ->(_) { raise StandardError } } it 'calls #after' do expect(subject).to receive(:after) expect { subject.call({}) }.to raise_error(StandardError) end end context 'after callback' do before do allow(subject).to receive(:after).and_return([200, {}, 'Hello from after callback']) end it 'overwrites application response' do expect(subject.call!({}).last).to eq('Hello from after callback') end end context 'after callback with errors' do it 'does not overwrite the application response' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'with patched warnings' do before do @warnings = warnings = [] allow(subject).to receive(:warn) { |m| warnings << m } allow(subject).to receive(:after).and_raise(StandardError) end it 'does show a warning' do expect { subject.call({}) }.to raise_error(StandardError) expect(@warnings).not_to be_empty end end end it 'is able to access the response' do subject.call({}) expect(subject.response).to be_a(Rack::Response) end describe '#response' do subject do described_class.new(response) end before { subject.call({}) } context 'when Array' do let(:rack_response) { Rack::Response.new('test', 204, abc: 1) } let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } } it 'status' do expect(subject.response.status).to eq(204) end it 'body' do expect(subject.response.body).to eq(['test']) end it 'header' do expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do allow(Rack::Response).to receive(:new).and_return(rack_response) expect(subject.response).to eq(rack_response) end end context 'when Rack::Response' do let(:rack_response) { Rack::Response.new('test', 204, abc: 1) } let(:response) { ->(_) { rack_response } } it 'status' do expect(subject.response.status).to eq(204) end it 'body' do expect(subject.response.body).to eq(['test']) end it 'header' do expect(subject.response.headers).to have_key(:abc) end it 'returns the memoized Rack::Response instance' do expect(subject.response).to eq(rack_response) end end end describe '#context' do subject { described_class.new(blank_app) } it 'allows access to response context' do subject.call(Grape::Env::API_ENDPOINT => { header: 'some header' }) expect(subject.context).to eq(header: 'some header') end end context 'options' do it 'persists options passed at initialization' do expect(described_class.new(blank_app, abc: true).options[:abc]).to be true end context 'defaults' do let(:example_ware) do Class.new(Grape::Middleware::Base) do const_set(:DEFAULT_OPTIONS, { monkey: true }.freeze) end end it 'persists the default options' do expect(example_ware.new(blank_app).options[:monkey]).to be true end it 'overrides default options when provided' do expect(example_ware.new(blank_app, monkey: false).options[:monkey]).to be false end end end context 'header' do let(:example_ware) do Class.new(Grape::Middleware::Base) do def before header 'X-Test-Before', 'Hi' end def after header 'X-Test-After', 'Bye' nil end end end let(:app) do context = self Rack::Builder.app do use context.example_ware run ->(_) { [200, {}, ['Yeah']] } end end it 'is able to set a header' do get '/' expect(last_response.headers['X-Test-Before']).to eq('Hi') expect(last_response.headers['X-Test-After']).to eq('Bye') end end context 'header overwrite' do let(:example_ware) do Class.new(Grape::Middleware::Base) do def before header 'X-Test-Overwriting', 'Hi' end def after header 'X-Test-Overwriting', 'Bye' nil end end end let(:api) do Class.new(Grape::API) do get('/') do header 'X-Test-Overwriting', 'Yeah' 'Hello' end end end let(:app) do context = self Rack::Builder.app do use context.example_ware run context.api.new end end it 'overwrites header by after headers' do get '/' expect(last_response.headers['X-Test-Overwriting']).to eq('Bye') end end describe 'query_params' do let(:dummy_middleware) do Class.new(Grape::Middleware::Base) do def before query_params end end end let(:app) do context = self Rack::Builder.app do use context.dummy_middleware run ->(_) { [200, {}, ['Yeah']] } end end context 'when query params are conflicting' do it 'raises an ConflictingTypes error' do expect { get '/?x[y]=1&x[y]z=2' }.to raise_error(Grape::Exceptions::ConflictingTypes) end end context 'when query params is over the specified limit' do let(:query_params) { "foo#{'[a]' * Rack::Utils.param_depth_limit}=bar" } it 'raises an ConflictingTypes error' do expect { get "/?foo#{'[a]' * Rack::Utils.param_depth_limit}=bar" }.to raise_error(Grape::Exceptions::TooDeepParameters) end end end end ================================================ FILE: spec/grape/middleware/error_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Error do let(:error_entity) do Class.new(Grape::Entity) do expose :code expose :static def static 'static text' end end end let(:err_app) do Class.new do class << self attr_accessor :error, :format def call(_env) throw :error, error end end end end let(:options) { { default_message: 'Aww, hamburgers.' } } let(:app) do opts = options context = self Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, **opts # rubocop:disable RSpec/DescribedClass run context.err_app end end it 'sets the status code appropriately' do err_app.error = { status: 410 } get '/' expect(last_response.status).to eq(410) end it 'sets the status code based on the rack util status code symbol' do err_app.error = { status: :gone } get '/' expect(last_response.status).to eq(410) end it 'sets the error message appropriately' do err_app.error = { message: 'Awesome stuff.' } get '/' expect(last_response.body).to eq('Awesome stuff.') end it 'defaults to a 500 status' do err_app.error = {} get '/' expect(last_response).to be_server_error end it 'has a default message' do err_app.error = {} get '/' expect(last_response.body).to eq('Aww, hamburgers.') end context 'with http code' do let(:options) { { default_message: 'Aww, hamburgers.' } } it 'adds the status code if wanted' do err_app.error = { message: { code: 200 } } get '/' expect(last_response.body).to eq({ code: 200 }.to_json) end end end ================================================ FILE: spec/grape/middleware/exception_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Error do let(:exception_app) do Class.new do class << self def call(_env) raise 'rain!' end end end end let(:other_exception_app) do Class.new do class << self def call(_env) raise NotImplementedError, 'snow!' end end end end let(:custom_error_app) do custom_error = Class.new(Grape::Exceptions::Base) Class.new do define_singleton_method(:call) do |_env| raise custom_error.new(status: 400, message: 'failed validation') end end end let(:error_hash_app) do Class.new do class << self def error!(message, status) throw :error, message: { error: message, detail: 'missing widget' }, status: status end def call(_env) error!('rain!', 401) end end end end let(:access_denied_app) do Class.new do class << self def error!(message, status) throw :error, message: message, status: status end def call(_env) error!('Access Denied', 401) end end end end let(:app) do opts = options app = running_app Rack::Builder.app do use Rack::Lint use Spec::Support::EndpointFaker if opts.any? use Grape::Middleware::Error, **opts else use Grape::Middleware::Error end run app end end context 'with defaults' do let(:running_app) { exception_app } let(:options) { {} } it 'does not trap errors by default' do expect { get '/' }.to raise_error(RuntimeError, 'rain!') end end context 'with rescue_all' do context 'StandardError exception' do let(:running_app) { exception_app } let(:options) { { rescue_all: true } } it 'sets the message appropriately' do get '/' expect(last_response.body).to eq('rain!') end it 'defaults to a 500 status' do get '/' expect(last_response.status).to eq(500) end end context 'Non-StandardError exception' do let(:running_app) { other_exception_app } let(:options) { { rescue_all: true } } it 'does not trap errors other than StandardError' do expect { get '/' }.to raise_error(NotImplementedError, 'snow!') end end end context 'Non-StandardError exception with a provided rescue handler' do context 'default error response' do let(:running_app) { other_exception_app } let(:options) { { rescue_handlers: { NotImplementedError => nil } } } it 'rescues the exception using the default handler' do get '/' expect(last_response.body).to eq('snow!') end end context 'custom error response' do let(:running_app) { other_exception_app } let(:options) { { rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } } } it 'rescues the exception using the provided handler' do get '/' expect(last_response.body).to eq('rescued') end end end context do let(:running_app) { exception_app } let(:options) { { rescue_all: true, default_status: 500 } } it 'is possible to specify a different default status code' do get '/' expect(last_response.status).to eq(500) end end context do let(:running_app) { exception_app } let(:options) { { rescue_all: true, format: :json } } it 'is possible to return errors in json format' do get '/' expect(last_response.body).to eq('{"error":"rain!"}') end end context do let(:running_app) { error_hash_app } let(:options) { { rescue_all: true, format: :json } } it 'is possible to return hash errors in json format' do get '/' expect(['{"error":"rain!","detail":"missing widget"}', '{"detail":"missing widget","error":"rain!"}']).to include(last_response.body) end end context do let(:running_app) { exception_app } let(:options) { { rescue_all: true, format: :xml } } it 'is possible to return errors in xml format' do get '/' expect(last_response.body).to eq("\n\n rain!\n\n") end end context do let(:running_app) { error_hash_app } let(:options) { { rescue_all: true, format: :xml } } it 'is possible to return hash errors in xml format' do get '/' expect(["\n\n missing widget\n rain!\n\n", "\n\n rain!\n missing widget\n\n"]).to include(last_response.body) end end context do let(:running_app) { exception_app } let(:options) do { rescue_all: true, format: :custom, error_formatters: { custom: lambda do |message, _backtrace, _options, _env, _original_exception| { custom_formatter: message }.inspect end } } end it 'is possible to specify a custom formatter' do get '/' response = Rack::Utils.escape_html({ custom_formatter: 'rain!' }.inspect) expect(last_response.body).to eq(response) end end context do let(:running_app) { access_denied_app } let(:options) { {} } it 'does not trap regular error! codes' do get '/' expect(last_response.status).to eq(401) end end context do let(:running_app) { custom_error_app } let(:options) { { rescue_all: false } } it 'responds to custom Grape exceptions appropriately' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('failed validation') end end context 'with rescue_options :backtrace and :exception set to true' do let(:running_app) { exception_app } let(:options) do { rescue_all: true, format: :json, rescue_options: { backtrace: true, original_exception: true } } end it 'is possible to return the backtrace and the original exception in json format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original_exception', 'RuntimeError') end end context do let(:running_app) { exception_app } let(:options) do { rescue_all: true, format: :xml, rescue_options: { backtrace: true, original_exception: true } } end it 'is possible to return the backtrace and the original exception in xml format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original-exception', 'RuntimeError') end end context do let(:running_app) { exception_app } let(:options) do { rescue_all: true, format: :txt, rescue_options: { backtrace: true, original_exception: true } } end it 'is possible to return the backtrace and the original exception in txt format' do get '/' expect(last_response.body).to include('error', 'rain!', 'backtrace', 'original exception', 'RuntimeError') end end end ================================================ FILE: spec/grape/middleware/formatter_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Formatter do subject { described_class.new(app) } before { allow(subject).to receive(:dup).and_return(subject) } let(:body) { { 'foo' => 'bar' } } let(:app) { ->(_env) { [200, {}, [body]] } } context 'serialization' do let(:body) { { 'abc' => 'def' } } let(:env) do { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } end it 'looks at the bodies for possibly serializable data' do r = Rack::MockResponse[*subject.call(env)] expect(r.body).to eq(Grape::Json.dump(body)) end context 'default format' do let(:body) { ['foo'] } let(:env) do { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => '*/*' } end before do subject.options[:default_format] = :json end it 'returns JSON' do body.instance_eval do def to_json(*_args) '"bar"' end end r = Rack::MockResponse[*subject.call(env)] expect(r.body).to eq('"bar"') end end context 'xml' do let(:body) { +'string' } let(:env) do { Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json' } end it 'calls #to_xml if the content type is xml' do body.instance_eval do def to_xml '' end end r = Rack::MockResponse[*subject.call(env)] expect(r.body).to eq('') end end end context 'error handling' do let(:formatter) { double(:formatter) } let(:env) do { Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json' } end before do allow(Grape::Formatter).to receive(:formatter_for) { formatter } end it 'rescues formatter-specific exceptions' do allow(formatter).to receive(:call) { raise Grape::Exceptions::InvalidFormatter.new(String, 'xml') } expect do catch(:error) { subject.call(env) } end.not_to raise_error end it 'does not rescue other exceptions' do allow(formatter).to receive(:call) { raise StandardError } expect do catch(:error) { subject.call(Rack::PATH_INFO => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json') } end.to raise_error(StandardError) end end context 'detection' do context 'when path contains invalid byte sequence' do it 'does not raise an exception' do expect { subject.call(Rack::PATH_INFO => "/info.\x80") }.not_to raise_error end end it 'uses the xml extension if one is provided' do subject.call(Rack::PATH_INFO => '/info.xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses the json extension if one is provided' do subject.call(Rack::PATH_INFO => '/info.json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the format parameter if one is provided' do subject.call(Rack::PATH_INFO => '/info', Rack::QUERY_STRING => 'format=json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the default format if none is provided' do subject.call(Rack::PATH_INFO => '/info') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:txt) end it 'uses the requested format if provided in headers' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'uses the file extension format if provided before headers' do subject.call(Rack::PATH_INFO => '/info.txt', 'HTTP_ACCEPT' => 'application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:txt) end end context 'accept header detection' do context 'when header contains invalid byte sequence' do it 'does not raise an exception' do expect { subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => "Hello \x80") }.not_to raise_error end end it 'detects from the Accept header' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end it 'uses quality rankings to determine formats' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; q=0.3,application/xml; q=1.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; q=1.0,application/xml; q=0.3') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings mixed with nothing' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml; q=1.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml; q=1.0,application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'handles quality rankings that have a default 1.0 value' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json,application/xml;q=0.5') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml;q=0.5,application/json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'parses headers with other attributes' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json; abc=2.3; q=1.0,application/xml; q=0.7') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:json) end it 'ensures that a quality of 0 is less preferred than any other content type' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/json;q=0.0,application/xml') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml,application/json;q=0.0') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:xml) end context 'with custom vendored content types' do context 'when registered' do subject { described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) } it 'uses the custom type' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(subject.env[Grape::Env::API_FORMAT]).to eq(:custom) end end context 'when unregistered' do it 'returns the default content type text/plain' do r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json')] expect(r.headers[Rack::CONTENT_TYPE]).to eq('text/plain') end end end it 'parses headers with symbols as hash keys' do subject.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/xml', system_time: '091293') expect(subject.env[:system_time]).to eq('091293') end end context 'content-type' do it 'is set for json' do _, headers, = subject.call(Rack::PATH_INFO => '/info.json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/json') end it 'is set for xml' do _, headers, = subject.call(Rack::PATH_INFO => '/info.xml') expect(headers[Rack::CONTENT_TYPE]).to eq('application/xml') end it 'is set for txt' do _, headers, = subject.call(Rack::PATH_INFO => '/info.txt') expect(headers[Rack::CONTENT_TYPE]).to eq('text/plain') end it 'is set for custom' do s = described_class.new(app, content_types: { custom: 'application/x-custom' }) _, headers, = s.call(Rack::PATH_INFO => '/info.custom') expect(headers[Rack::CONTENT_TYPE]).to eq('application/x-custom') end it 'is set for vendored with registered type' do s = described_class.new(app, content_types: { custom: 'application/vnd.test+json' }) _, headers, = s.call(Rack::PATH_INFO => '/info', 'HTTP_ACCEPT' => 'application/vnd.test+json') expect(headers[Rack::CONTENT_TYPE]).to eq('application/vnd.test+json') end end context 'format' do it 'uses custom formatter' do s = described_class.new(app, content_types: { custom: "don't care" }, formatters: { custom: ->(_obj, _env) { 'CUSTOM FORMAT' } }) r = Rack::MockResponse[*s.call(Rack::PATH_INFO => '/info.custom')] expect(r.body).to eq('CUSTOM FORMAT') end context 'default' do let(:body) { ['blah'] } it 'uses default json formatter' do r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.json')] expect(r.body).to eq(Grape::Json.dump(body)) end end it 'uses custom json formatter' do subject.options[:formatters] = { json: ->(_obj, _env) { 'CUSTOM JSON FORMAT' } } r = Rack::MockResponse[*subject.call(Rack::PATH_INFO => '/info.json')] expect(r.body).to eq('CUSTOM JSON FORMAT') end end context 'no content responses' do let(:no_content_response) { ->(status) { [status, {}, []] } } statuses_without_body = if Gem::Version.new(Rack.release) >= Gem::Version.new('2.1.0') Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.keys else Rack::Utils::STATUS_WITH_NO_ENTITY_BODY end statuses_without_body.each do |status| it "does not modify a #{status} response" do expected_response = no_content_response[status] allow(app).to receive(:call).and_return(expected_response) expect(subject.call({})).to eq(expected_response) end end end context 'input' do content_types = ['application/json', 'application/json; charset=utf-8'].freeze %w[POST PATCH PUT DELETE].each do |method| context 'when body is not nil or empty' do context 'when Content-Type is supported' do let(:io) { StringIO.new('{"is_boolean":true,"string":"thing"}') } let(:content_type) { 'application/json' } it "parses the body from #{method} and copies values into rack.request.form_hash" do subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end end context 'when Content-Type is not supported' do let(:io) { StringIO.new('{"is_boolean":true,"string":"thing"}') } let(:content_type) { 'application/atom+xml' } it 'returns a 415 HTTP error status' do error = catch(:error) do subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) end expect(error[:status]).to eq(415) expect(error[:message]).to eq("The provided content-type 'application/atom+xml' is not supported.") end end end context 'when body is nil' do let(:io) { double } before do allow(io).to receive_message_chain(rewind: nil, read: nil) end it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => '0' ) end end context 'when body is empty' do let(:io) { double } before do allow(io).to receive_messages(rewind: nil, read: '') end it 'does not read and parse the body' do expect(subject).not_to receive(:read_rack_input) subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => 0 ) end end content_types.each do |content_type| context content_type do it "parses the body from #{method} and copies values into rack.request.form_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end end end it "parses the chunked body from #{method} and copies values into rack.request.from_hash" do io = StringIO.new('{"is_boolean":true,"string":"thing"}') subject.call( Rack::PATH_INFO => '/infol', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end it 'rewinds IO' do io = StringIO.new('{"is_boolean":true,"string":"thing"}') io.read subject.call( Rack::PATH_INFO => '/infol', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, 'HTTP_TRANSFER_ENCODING' => 'chunked' ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing') end it "parses the body from an xml #{method} and copies values into rack.request.from_hash" do io = StringIO.new('Test') subject.call( Rack::PATH_INFO => '/info.xml', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => 'application/xml', Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) if Object.const_defined? :MultiXml expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['thing']['name']).to eq('Test') else expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['thing']['name']['__content__']).to eq('Test') end end [Rack::Request::FORM_DATA_MEDIA_TYPES, Rack::Request::PARSEABLE_DATA_MEDIA_TYPES].flatten.each do |content_type| it "ignores #{content_type}" do io = StringIO.new('name=Other+Test+Thing') subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => method, 'CONTENT_TYPE' => content_type, Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]).to be_nil end end end end context 'send file' do let(:file) { double(File) } let(:file_body) { Grape::ServeStream::StreamResponse.new(file) } let(:app) { ->(_env) { [200, {}, file_body] } } let(:body) { 'data' } let(:env) do { Rack::PATH_INFO => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } end let(:headers) do if Gem::Version.new(Rack.release) < Gem::Version.new('3.1') { Rack::CONTENT_TYPE => 'application/json', Rack::CONTENT_LENGTH => body.bytesize.to_s } else { Rack::CONTENT_TYPE => 'application/json' } end end it 'returns a file response' do expect(file).to receive(:each).and_yield(body) r = Rack::MockResponse[*subject.call(env)] expect(r).to be_successful expect(r.headers).to eq(headers) expect(r.body).to eq('data') end end context 'inheritable formatters' do subject { described_class.new(app, formatters: { invalid: invalid_formatter }, content_types: { invalid: 'application/x-invalid' }) } let(:invalid_formatter) do Class.new do def self.call(_, _) { message: 'invalid' }.to_json end end end let(:app) { ->(_env) { [200, {}, ['']] } } let(:env) do Rack::MockRequest.env_for('/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid') end it 'returns response by invalid formatter' do r = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(r.body)).to eq('message' => 'invalid') end end context 'custom parser raises exception and rescue options are enabled for backtrace and original_exception' do it 'adds the backtrace and original_exception to the error output' do subject = described_class.new( app, rescue_options: { backtrace: true, original_exception: true }, parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } } ) io = StringIO.new('{invalid}') error = catch(:error) do subject.call( Rack::PATH_INFO => '/info', Rack::REQUEST_METHOD => Rack::POST, 'CONTENT_TYPE' => 'application/json', Rack::RACK_INPUT => io, 'CONTENT_LENGTH' => io.length.to_s ) end expect(error[:message]).to eq 'fail' expect(error[:backtrace].size).to be >= 1 expect(error[:original_exception].class).to eq StandardError end end end ================================================ FILE: spec/grape/middleware/globals_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Globals do subject { described_class.new(blank_app) } before { allow(subject).to receive(:dup).and_return(subject) } let(:blank_app) { ->(_env) { [200, {}, 'Hi there.'] } } it 'calls through to the app' do expect(subject.call({})).to eq([200, {}, 'Hi there.']) end context 'environment' do it 'sets the grape.request environment' do subject.call({}) expect(subject.env[Grape::Env::GRAPE_REQUEST]).to be_a(Grape::Request) end it 'sets the grape.request.headers environment' do subject.call({}) expect(subject.env[Grape::Env::GRAPE_REQUEST_HEADERS]).to be_a(Hash) end it 'sets the grape.request.params environment' do subject.call(Rack::QUERY_STRING => 'test=1', Rack::RACK_INPUT => StringIO.new) expect(subject.env[Grape::Env::GRAPE_REQUEST_PARAMS]).to be_a(Hash) end end end ================================================ FILE: spec/grape/middleware/stack_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Stack do subject { described_class.new } let(:foo_middleware) { Class.new } let(:bar_middleware) { Class.new } let(:block_middleware) do Class.new do attr_reader :block def initialize(&block) @block = block end end end let(:proc) { -> {} } let(:others) { [[:use, bar_middleware], [:insert_before, bar_middleware, block_middleware, proc]] } before do subject.use foo_middleware end describe '#use' do it 'pushes a middleware class onto the stack' do expect { subject.use bar_middleware } .to change(subject, :size).by(1) expect(subject.last).to eq(bar_middleware) end it 'pushes a middleware class with arguments onto the stack' do expect { subject.use bar_middleware, false, my_arg: 42 } .to change(subject, :size).by(1) expect(subject.last).to eq(bar_middleware) expect(subject.last.args).to eq([false, { my_arg: 42 }]) end it 'pushes a middleware class with block arguments onto the stack' do expect { subject.use block_middleware, &proc } .to change(subject, :size).by(1) expect(subject.last).to eq(block_middleware) expect(subject.last.args).to eq([]) expect(subject.last.block).to eq(proc) end end describe '#insert' do it 'inserts a middleware class at the integer index' do expect { subject.insert 0, bar_middleware } .to change(subject, :size).by(1) expect(subject[0]).to eq(bar_middleware) expect(subject[1]).to eq(foo_middleware) end end describe '#insert_before' do it 'inserts a middleware before another middleware class' do expect { subject.insert_before foo_middleware, bar_middleware } .to change(subject, :size).by(1) expect(subject[0]).to eq(bar_middleware) expect(subject[1]).to eq(foo_middleware) end it 'inserts a middleware before an anonymous class given by its superclass' do subject.use Class.new(block_middleware) expect { subject.insert_before block_middleware, bar_middleware } .to change(subject, :size).by(1) expect(subject[1]).to eq(bar_middleware) expect(subject[2]).to eq(block_middleware) end it 'raises an error on an invalid index' do stub_const('StackSpec::BlockMiddleware', block_middleware) expect { subject.insert_before block_middleware, bar_middleware } .to raise_error(RuntimeError, 'No such middleware to insert before: StackSpec::BlockMiddleware') end end describe '#insert_after' do it 'inserts a middleware after another middleware class' do expect { subject.insert_after foo_middleware, bar_middleware } .to change(subject, :size).by(1) expect(subject[1]).to eq(bar_middleware) expect(subject[0]).to eq(foo_middleware) end it 'inserts a middleware after an anonymous class given by its superclass' do subject.use Class.new(block_middleware) expect { subject.insert_after block_middleware, bar_middleware } .to change(subject, :size).by(1) expect(subject[1]).to eq(block_middleware) expect(subject[2]).to eq(bar_middleware) end it 'raises an error on an invalid index' do stub_const('StackSpec::BlockMiddleware', block_middleware) expect { subject.insert_after block_middleware, bar_middleware } .to raise_error(RuntimeError, 'No such middleware to insert after: StackSpec::BlockMiddleware') end end describe '#merge_with' do it 'applies a collection of operations and middlewares' do expect { subject.merge_with(others) } .to change(subject, :size).by(2) expect(subject[0]).to eq(foo_middleware) expect(subject[1]).to eq(block_middleware) expect(subject[2]).to eq(bar_middleware) end context 'middleware spec with proc declaration exists' do let(:middleware_spec_with_proc) { [:use, foo_middleware, proc] } it 'properly forwards spec arguments' do expect(subject).to receive(:use).with(foo_middleware) subject.merge_with([middleware_spec_with_proc]) end end end describe '#build' do it 'returns a rack builder instance' do expect(subject.build).to be_instance_of(Rack::Builder) end context 'when @others are present' do let(:others) { [[:insert_after, Grape::Middleware::Formatter, bar_middleware]] } it 'applies the middleware specs stored in @others' do subject.concat others subject.use Grape::Middleware::Formatter subject.build expect(subject[0]).to eq foo_middleware expect(subject[1]).to eq Grape::Middleware::Formatter expect(subject[2]).to eq bar_middleware end end end describe '#concat' do it 'adds non :use specs to @others' do expect { subject.concat others }.to change(subject, :others).from([]).to([[others.last]]) end it 'calls +merge_with+ with the :use specs' do expect(subject).to receive(:merge_with).with [[:use, bar_middleware]] subject.concat others end end end ================================================ FILE: spec/grape/middleware/versioner/accept_version_header_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Versioner::AcceptVersionHeader do subject { described_class.new(app, **@options) } let(:app) { ->(env) { [200, env, env] } } before do @options = { version_options: { using: :accept_version_header } } end describe '#bad encoding' do before do @options[:versions] = %w[v1] end it 'does not raise an error' do expect do subject.call('HTTP_ACCEPT_VERSION' => "\x80") end.to throw_symbol(:error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'The requested version is not supported.') end end context 'api.version' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT_VERSION' => 'v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is not supported' do expect do subject.call('HTTP_ACCEPT_VERSION' => 'v2').last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'The requested version is not supported.' ) end end it 'succeeds if :strict is not set' do expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT_VERSION' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end context 'when :strict is set' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true end it 'fails with 406 Not Acceptable if header is not set' do expect do subject.call({}).last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'fails with 406 Not Acceptable if header is empty' do expect do subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, headers: { 'X-Cascade' => 'pass' }, message: 'Accept-Version header must be set.' ) end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end context 'when :strict and cascade: false' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true @options[:version_options][:cascade] = false end it 'fails with 406 Not Acceptable if header is not set' do expect do subject.call({}).last end.to throw_symbol( :error, status: 406, headers: {}, message: 'Accept-Version header must be set.' ) end it 'fails with 406 Not Acceptable if header is empty' do expect do subject.call('HTTP_ACCEPT_VERSION' => '').last end.to throw_symbol( :error, status: 406, headers: {}, message: 'Accept-Version header must be set.' ) end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT_VERSION' => 'v1').first).to eq(200) end end end ================================================ FILE: spec/grape/middleware/versioner/header_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Versioner::Header do subject { described_class.new(app, **@options) } let(:app) { ->(env) { [200, env, env] } } before do @options = { version_options: { using: :header, vendor: 'vendor' } } end context 'api.type and api.subtype' do it 'sets type and subtype to first choice of content type if no preference given' do status, _, env = subject.call('HTTP_ACCEPT' => '*/*') expect(env[Grape::Env::API_TYPE]).to eql 'application' expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/*') expect(env[Grape::Env::API_TYPE]).to eql 'application' expect(env[Grape::Env::API_SUBTYPE]).to eql 'vnd.vendor+xml' expect(status).to eq(200) end it 'sets preferred type and subtype' do status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain') expect(env[Grape::Env::API_TYPE]).to eql 'text' expect(env[Grape::Env::API_SUBTYPE]).to eql 'plain' expect(status).to eq(200) end end context 'api.format' do it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end ['v1', :v1].each do |version| context "when version is set to #{version}" do before do @options[:versions] = [version] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_FORMAT]).to eql 'json' expect(status).to eq(200) end it 'is nil if not provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_FORMAT]).to be_nil expect(status).to eq(200) end end end end context 'api.vendor' do it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include 'API vendor not found' end end context 'when version is set' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_VENDOR]).to eql 'vendor' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if vendor is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API vendor not found') end end end end context 'api.version' do before do @options[:versions] = ['v1'] end it 'is set' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'is set if format provided' do status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json') expect(env[Grape::Env::API_VERSION]).to eql 'v1' expect(status).to eq(200) end it 'fails with 406 Not Acceptable if version is invalid' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end end end it 'succeeds if :strict is not set' do expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200) expect(subject.call({}).first).to eq(200) end it 'succeeds if :strict is set to false and given an invalid header' do @options[:version_options][:strict] = false expect(subject.call('HTTP_ACCEPT' => 'yaml').first).to eq(200) expect(subject.call({}).first).to eq(200) end context 'when :strict is set' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true end it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header is empty' do expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end context 'when :strict and cascade: false' do before do @options[:versions] = ['v1'] @options[:version_options][:strict] = true @options[:version_options][:cascade] = false end it 'fails with 406 Not Acceptable if header is not set' do expect { subject.call({}).last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header is application/xml' do expect { subject.call('HTTP_ACCEPT' => 'application/xml').last } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 expect(exception.message).to include('API vendor or version not found.') end end it 'fails with 406 Not Acceptable if header is empty' do expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 expect(exception.message).to include('Accept header must be set.') end end it 'fails with 406 Not Acceptable if header contains a single invalid accept' do expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first } .to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader) expect(exception.headers).to eql({}) expect(exception.status).to be 406 expect(exception.message).to include('API vendor or version not found.') end end it 'succeeds if proper header is set' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end end context 'when multiple versions are specified' do before do @options[:versions] = %w[v1 v2] end it 'succeeds with v1' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200) end it 'succeeds with v2' do expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200) end it 'fails with another version' do expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception| expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader) expect(exception.headers).to eql('X-Cascade' => 'pass') expect(exception.status).to be 406 expect(exception.message).to include('API version not found') end end end context 'when there are multiple versions with complex vendor specified with rescue_from :all' do subject do Class.new(Grape::API) do rescue_from :all end end let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :header, vendor: 'test.a-cool_resource', cascade: false, strict: true content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :header, vendor: 'test.a-cool_resource', strict: true content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end context 'with header versioned endpoints and a rescue_all block defined' do it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :header, vendor: 'test.a-cool_resource' expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :header, vendor: 'test.a-cool_resource' expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end end context 'with missing vendor option' do subject do Class.new(Grape::API) do version 'v1', using: :header end end def app subject end it 'fails' do expect { versioned_get '/', 'v1', using: :header }.to raise_error Grape::Exceptions::MissingVendorOption end end end ================================================ FILE: spec/grape/middleware/versioner/param_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Versioner::Param do subject { described_class.new(app, **options) } let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } it 'sets the API version based on the default param (apiver)' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end it 'cuts (only) the version out of the params' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1', 'other_param' => '5' }) env[Rack::RACK_REQUEST_QUERY_HASH] = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING]) expect(subject.call(env)[1][Rack::RACK_REQUEST_QUERY_HASH]['apiver']).to be_nil expect(subject.call(env)[1][Rack::RACK_REQUEST_QUERY_HASH]['other_param']).to eq('5') end it 'provides a nil version if no version is given' do env = Rack::MockRequest.env_for('/') expect(subject.call(env).last).to be_nil end context 'with specified parameter name' do let(:options) { { version_options: { parameter: 'v' } } } it 'sets the API version based on the custom parameter name' do env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' }) expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end it 'does not set the API version based on the default param' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1][Grape::Env::API_VERSION]).to be_nil end end context 'with specified versions' do let(:options) { { versions: %w[v1 v2] } } it 'throws an error if a non-allowed version is specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v3' }) expect(catch(:error) { subject.call(env) }[:status]).to eq(404) end it 'allows versions that have been specified' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) expect(subject.call(env)[1][Grape::Env::API_VERSION]).to eq('v1') end end context 'when no version is set' do let(:options) do { versions: ['v1'], version_options: { using: :header } } end it 'returns a 200 (matches the first version found)' do env = Rack::MockRequest.env_for('/awesome', params: {}) expect(subject.call(env).first).to eq(200) end end context 'when there are multiple versions without a custom param' do subject { Class.new(Grape::API) } let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :param content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :param content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :param, parameter: :apiver expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :param, parameter: :apiver expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end context 'when there are multiple versions with a custom param' do subject { Class.new(Grape::API) } let(:v1_app) do Class.new(Grape::API) do version 'v1', using: :param, parameter: 'v' content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json' formatter :v1_test, ->(object, _) { object } format :v1_test resources :users do get :hello do 'one' end end end end let(:v2_app) do Class.new(Grape::API) do version 'v2', using: :param, parameter: 'v' content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json' formatter :v2_test, ->(object, _) { object } format :v2_test resources :users do get :hello do 'two' end end end end def app subject.mount v2_app subject.mount v1_app subject end it 'responds correctly to a v1 request' do versioned_get '/users/hello', 'v1', using: :param, parameter: 'v' expect(last_response.body).to eq('one') expect(last_response.body).not_to include('API vendor or version not found') end it 'responds correctly to a v2 request' do versioned_get '/users/hello', 'v2', using: :param, parameter: 'v' expect(last_response.body).to eq('two') expect(last_response.body).not_to include('API vendor or version not found') end end end ================================================ FILE: spec/grape/middleware/versioner/path_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Versioner::Path do subject { described_class.new(app, **options) } let(:app) { ->(env) { [200, env, env[Grape::Env::API_VERSION]] } } let(:options) { {} } it 'sets the API version based on the first path' do expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') end it 'does not cut the version out of the path' do expect(subject.call(Rack::PATH_INFO => '/v1/awesome')[1][Rack::PATH_INFO]).to eq('/v1/awesome') end it 'provides a nil version if no path is given' do expect(subject.call(Rack::PATH_INFO => '/').last).to be_nil end context 'with a pattern' do let(:options) { { pattern: /v./i } } it 'sets the version if it matches' do expect(subject.call(Rack::PATH_INFO => '/v1/awesome').last).to eq('v1') end it 'ignores the version if it fails to match' do expect(subject.call(Rack::PATH_INFO => '/awesome/radical').last).to be_nil end end [%w[v1 v2], %i[v1 v2], [:v1, 'v2'], ['v1', :v2]].each do |versions| context "with specified versions as #{versions}" do let(:options) { { versions: versions } } it 'throws an error if a non-allowed version is specified' do expect(catch(:error) { subject.call(Rack::PATH_INFO => '/v3/awesome') }[:status]).to eq(404) end it 'allows versions that have been specified' do expect(subject.call(Rack::PATH_INFO => '/v1/asoasd').last).to eq('v1') end end end context 'with prefix, but requested version is not matched' do let(:options) { { prefix: '/v1', pattern: /v./i } } it 'recognizes potential version' do expect(subject.call(Rack::PATH_INFO => '/v3/foo').last).to eq('v3') end end context 'with mount path' do let(:options) { { mount_path: '/mounted', versions: [:v1] } } it 'recognizes potential version' do expect(subject.call(Rack::PATH_INFO => '/mounted/v1/foo').last).to eq('v1') end end end ================================================ FILE: spec/grape/middleware/versioner_spec.rb ================================================ # frozen_string_literal: true describe Grape::Middleware::Versioner do subject { described_class.using(strategy) } context 'when :path' do let(:strategy) { :path } it { is_expected.to eq(Grape::Middleware::Versioner::Path) } end context 'when :header' do let(:strategy) { :header } it { is_expected.to eq(Grape::Middleware::Versioner::Header) } end context 'when :param' do let(:strategy) { :param } it { is_expected.to eq(Grape::Middleware::Versioner::Param) } end context 'when :accept_version_header' do let(:strategy) { :accept_version_header } it { is_expected.to eq(Grape::Middleware::Versioner::AcceptVersionHeader) } end context 'when unknown' do let(:strategy) { :unknown } it 'raises an error' do expect { subject }.to raise_error Grape::Exceptions::InvalidVersionerOption, Grape::Exceptions::InvalidVersionerOption.new(strategy).message end end end ================================================ FILE: spec/grape/named_api_spec.rb ================================================ # frozen_string_literal: true describe Grape::API do subject(:api_name) { NamedAPI.endpoints.last.options[:for].to_s } let(:api) do Class.new(Grape::API) do get 'test' do 'response' end end end let(:name) { 'NamedAPI' } before { stub_const(name, api) } it 'can access the name of the API' do expect(api_name).to eq name end end ================================================ FILE: spec/grape/params_builder/hash_spec.rb ================================================ # frozen_string_literal: true describe Grape::ParamsBuilder::Hash do subject { app } let(:app) do Class.new(Grape::API) end describe 'in an endpoint' do describe '#params' do before do subject.params do build_with :hash end subject.get do params.class end end it 'is of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') end end end describe 'in an api' do before do subject.build_with :hash end describe '#params' do before do subject.get do params.class end end it 'is Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hash') end end it 'symbolizes params keys' do subject.params do optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a][:b][:c], params[:a][:d]] end get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'symbolizes the params' do subject.params do build_with :hash requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", nil]') end it 'does not overwrite route_param with a regular param if they have same name' do subject.namespace :route_param do route_param :foo do get { params.to_json } end end get '/route_param/bar', foo: 'baz' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"foo":"bar"}') end it 'does not overwrite route_param with a defined regular param if they have same name' do subject.namespace :route_param do params do requires :foo, type: String end route_param :foo do get do params[:foo] end end end get '/route_param/bar', foo: 'baz' expect(last_response.status).to eq(200) expect(last_response.body).to eq('bar') end end end ================================================ FILE: spec/grape/params_builder/hash_with_indifferent_access_spec.rb ================================================ # frozen_string_literal: true describe Grape::ParamsBuilder::HashWithIndifferentAccess do subject { app } let(:app) do Class.new(Grape::API) end describe 'in an endpoint' do describe '#params' do before do subject.params do build_with :hash_with_indifferent_access end subject.get do params.class end end it 'is of type Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end end end describe 'in an api' do before do subject.build_with :hash_with_indifferent_access end describe '#params' do before do subject.get do params.class end end it 'is a Hash' do get '/' expect(last_response.status).to eq(200) expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess') end it 'parses sub hash params' do subject.params do build_with :hash_with_indifferent_access optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a]['b'][:c], params['a'][:d]] end get '/foo', a: { b: { c: 'bar' }, d: ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'params are indifferent to symbol or string keys' do subject.params do build_with :hash_with_indifferent_access optional :a, type: Hash do optional :b, type: Hash do optional :c, type: String end optional :d, type: Array end end subject.get '/foo' do [params[:a]['b'][:c], params['a'][:d]] end get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] } expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", ["foo"]]') end it 'responds to string keys' do subject.params do build_with :hash_with_indifferent_access requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", "bar"]') end end it 'does not overwrite route_param with a regular param if they have same name' do subject.namespace :route_param do route_param :foo do get { params.to_json } end end get '/route_param/bar', foo: 'baz' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"foo":"bar"}') end it 'does not overwrite route_param with a defined regular param if they have same name' do subject.namespace :route_param do params do requires :foo, type: String end route_param :foo do get do [params[:foo], params['foo']] end end end get '/route_param/bar', foo: 'baz' expect(last_response.status).to eq(200) expect(last_response.body).to eq('["bar", "bar"]') end end end ================================================ FILE: spec/grape/parser_spec.rb ================================================ # frozen_string_literal: true describe Grape::Parser do subject { described_class } describe '.parser_for' do let(:options) { {} } it 'returns parser correctly' do expect(subject.parser_for(:json)).to eq(Grape::Parser::Json) end context 'when parser is available' do let(:parsers) do { customized_json: Grape::Parser::Json } end it 'returns registered parser if available' do expect(subject.parser_for(:customized_json, parsers)).to eq(Grape::Parser::Json) end end context 'when parser does not exist' do it 'returns nil' do expect(subject.parser_for(:undefined)).to be_nil end end end end ================================================ FILE: spec/grape/path_spec.rb ================================================ # frozen_string_literal: true describe Grape::Path do describe '#origin' do context 'mount_path' do it 'is not included when it is nil' do path = described_class.new(nil, nil, mount_path: '/foo/bar') expect(path.origin).to eql '/foo/bar' end it 'is included when it is not nil' do path = described_class.new(nil, nil, {}) expect(path.origin).to eql('/') end end context 'root_prefix' do it 'is not included when it is nil' do path = described_class.new(nil, nil, {}) expect(path.origin).to eql('/') end it 'is included after the mount path' do path = described_class.new( nil, nil, mount_path: '/foo', root_prefix: '/hello' ) expect(path.origin).to eql('/foo/hello') end end it 'uses the namespace after the mount path and root prefix' do path = described_class.new( nil, 'namespace', mount_path: '/foo', root_prefix: '/hello' ) expect(path.origin).to eql('/foo/hello/namespace') end it 'uses the raw path after the namespace' do path = described_class.new( 'raw_path', 'namespace', mount_path: '/foo', root_prefix: '/hello' ) expect(path.origin).to eql('/foo/hello/namespace/raw_path') end end describe '#suffix' do context 'when using a specific format' do it 'accepts specified format' do path = described_class.new(nil, nil, format: 'json', content_types: 'application/json') expect(path.suffix).to eql('(.json)') end end context 'when path versioning is used' do it "includes a '/'" do path = described_class.new(nil, nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(/.:format)') end end context 'when path versioning is not used' do it "does not include a '/' when the path has a namespace" do path = described_class.new(nil, 'namespace', {}) expect(path.suffix).to eql('(.:format)') end it "does not include a '/' when the path has a path" do path = described_class.new('/path', nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(.:format)') end it "includes a '/' otherwise" do path = described_class.new(nil, nil, version: :v1, version_options: { using: :path }) expect(path.suffix).to eql('(/.:format)') end end end end ================================================ FILE: spec/grape/presenters/presenter_spec.rb ================================================ # frozen_string_literal: true describe Grape::Presenters::Presenter do subject { dummy_class.new } let(:dummy_class) do Class.new do include Grape::DSL::InsideRoute attr_reader :env, :request, :new_settings def initialize @env = {} @header = {} @new_settings = { namespace_inheritable: {}, namespace_stackable: {} } end end end describe 'represent' do let(:object_mock) do Object.new end it 'represent object' do expect(described_class.represent(object_mock)).to eq object_mock end end describe 'present' do let(:hash_mock) do { key: :value } end describe 'instance' do before do subject.present hash_mock, with: described_class end it 'presents dummy hash' do expect(subject.body).to eq hash_mock end end describe 'multiple presenter' do let(:hash_mock1) do { key1: :value1 } end let(:hash_mock2) do { key2: :value2 } end describe 'instance' do before do subject.present hash_mock1, with: described_class subject.present hash_mock2, with: described_class end it 'presents both dummy presenter' do expect(subject.body[:key1]).to eq hash_mock1[:key1] expect(subject.body[:key2]).to eq hash_mock2[:key2] end end end end end ================================================ FILE: spec/grape/request_spec.rb ================================================ # frozen_string_literal: true describe Grape::Request do let(:default_method) { Rack::GET } let(:default_params) { {} } let(:default_options) do { method: method, params: params } end let(:default_env) do Rack::MockRequest.env_for('/', options) end let(:method) { default_method } let(:params) { default_params } let(:options) { default_options } let(:env) { default_env } let(:request) do described_class.new(env) end describe '#params' do let(:params) do { a: '123', b: 'xyz' } end it 'by default returns stringified parameter keys' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) end context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do let(:request) do described_class.new(env, build_params_with: :hash) end it 'returns symbolized params' do expect(request.params).to eq(a: '123', b: 'xyz') end end describe 'with grape.routing_args' do let(:options) do default_options.merge('grape.routing_args' => routing_args) end let(:routing_args) do { version: '123', route_info: '456', c: 'ccc' } end it 'cuts version and route_info' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end end context 'when rack_params raises an EOF error' do before do allow(request).to receive(:rack_params).and_raise(EOFError) end let(:message) { Grape::Exceptions::EmptyMessageBody.new(nil).to_s } it 'raises an Grape::Exceptions::EmptyMessageBody' do expect { request.params }.to raise_error(Grape::Exceptions::EmptyMessageBody, message) end end context 'when rack_params raises a Rack::Multipart::MultipartPartLimitError' do before do allow(request).to receive(:rack_params).and_raise(Rack::Multipart::MultipartPartLimitError) end let(:message) { Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit).to_s } it 'raises an Rack::Multipart::MultipartPartLimitError' do expect { request.params }.to raise_error(Grape::Exceptions::TooManyMultipartFiles, message) end end context 'when rack_params raises a Rack::Multipart::MultipartTotalPartLimitError' do before do allow(request).to receive(:rack_params).and_raise(Rack::Multipart::MultipartTotalPartLimitError) end let(:message) { Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit).to_s } it 'raises an Rack::Multipart::MultipartPartLimitError' do expect { request.params }.to raise_error(Grape::Exceptions::TooManyMultipartFiles, message) end end context 'when rack_params raises a Rack::QueryParser::ParamsTooDeepError' do before do allow(request).to receive(:rack_params).and_raise(Rack::QueryParser::ParamsTooDeepError) end let(:message) { Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit).to_s } it 'raises a Grape::Exceptions::TooDeepParameters' do expect { request.params }.to raise_error(Grape::Exceptions::TooDeepParameters, message) end end context 'when rack_params raises a Rack::Utils::ParameterTypeError' do before do allow(request).to receive(:rack_params).and_raise(Rack::Utils::ParameterTypeError) end let(:message) { Grape::Exceptions::ConflictingTypes.new.to_s } it 'raises a Grape::Exceptions::ConflictingTypes' do expect { request.params }.to raise_error(Grape::Exceptions::ConflictingTypes, message) end end context 'when rack_params raises a Rack::Utils::InvalidParameterError' do before do allow(request).to receive(:rack_params).and_raise(Rack::Utils::InvalidParameterError) end let(:message) { Grape::Exceptions::InvalidParameters.new.to_s } it 'raises an Rack::Multipart::MultipartPartLimitError' do expect { request.params }.to raise_error(Grape::Exceptions::InvalidParameters, message) end end end describe '#headers' do let(:options) do default_options.merge(request_headers) end describe 'with http headers in env' do let(:request_headers) do { 'HTTP_X_GRAPE_IS_COOL' => 'yeah' } end let(:x_grape_is_cool_header) do 'x-grape-is-cool' end it 'cuts HTTP_ prefix and capitalizes header name words' do expect(request.headers).to eq(x_grape_is_cool_header => 'yeah') end end describe 'with non-HTTP_* stuff in env' do let(:request_headers) do { 'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape' } end it 'does not include them' do expect(request.headers).to eq({}) end end describe 'with symbolic header names' do let(:request_headers) do { HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true' } end let(:env) do default_env.merge(request_headers) end let(:grape_likes_symbolic_header) do 'grape-likes-symbolic' end it 'converts them to string' do expect(request.headers).to eq(grape_likes_symbolic_header => 'it is true') end end end end ================================================ FILE: spec/grape/router/greedy_route_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Grape::Router::GreedyRoute do let(:instance) { described_class.new(pattern, endpoint: endpoint, allow_header: allow_header) } let(:pattern) { :pattern } let(:endpoint) { instance_double(Grape::Endpoint) } let(:allow_header) { false } describe 'inheritance' do subject { instance } it { is_expected.to be_a(Grape::Router::BaseRoute) } end describe '#pattern' do subject { instance.pattern } it { is_expected.to eq(pattern) } end describe '#endpoint' do subject { instance.endpoint } it { is_expected.to eq(endpoint) } end describe '#allow_header' do subject { instance.allow_header } it { is_expected.to eq(allow_header) } end describe '#params' do subject { instance.params } it { is_expected.to be_nil } end end ================================================ FILE: spec/grape/router_spec.rb ================================================ # frozen_string_literal: true describe Grape::Router do describe '.normalize_path' do subject { described_class.normalize_path(path) } context 'when no leading slash' do let(:path) { 'foo%20bar%20baz' } it { is_expected.to eq '/foo%20bar%20baz' } end context 'when path ends with slash' do let(:path) { '/foo%20bar%20baz/' } it { is_expected.to eq '/foo%20bar%20baz' } end context 'when path has recurring slashes' do let(:path) { '////foo%20bar%20baz' } it { is_expected.to eq '/foo%20bar%20baz' } end context 'when not greedy' do let(:path) { '/foo%20bar%20baz' } it { is_expected.to eq '/foo%20bar%20baz' } end context 'when encoded string in lowercase' do let(:path) { '/foo%aabar%aabaz' } it { is_expected.to eq '/foo%AAbar%AAbaz' } end context 'when nil' do let(:path) { nil } it { is_expected.to eq '/' } end context 'when empty string' do let(:path) { '' } it { is_expected.to eq '/' } end context 'when encoding is different' do subject { described_class.normalize_path(path).encoding } let(:path) { '/foo%AAbar%AAbaz'.b } it { is_expected.to eq(Encoding::BINARY) } end end end ================================================ FILE: spec/grape/util/inheritable_setting_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::InheritableSetting do before do described_class.reset_global! subject.inherit_from parent end let(:parent) do described_class.new.tap do |settings| settings.global[:global_thing] = :global_foo_bar settings.namespace[:namespace_thing] = :namespace_foo_bar settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar settings.route[:route_thing] = :route_foo_bar end end let(:other_parent) do described_class.new.tap do |settings| settings.namespace[:namespace_thing] = :namespace_foo_bar_other settings.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar_other settings.namespace_stackable[:namespace_stackable_thing] = :namespace_stackable_foo_bar_other settings.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :namespace_reverse_stackable_foo_bar_other settings.route[:route_thing] = :route_foo_bar_other end end describe '#global' do it 'sets a global value' do subject.global[:some_thing] = :foo_bar expect(subject.global[:some_thing]).to eq :foo_bar subject.global[:some_thing] = :foo_bar_next expect(subject.global[:some_thing]).to eq :foo_bar_next end it 'sets the global inherited values' do expect(subject.global[:global_thing]).to eq :global_foo_bar end it 'overrides global values' do subject.global[:global_thing] = :global_new_foo_bar expect(parent.global[:global_thing]).to eq :global_new_foo_bar end it 'handles different parents' do subject.global[:global_thing] = :global_new_foo_bar subject.inherit_from other_parent expect(parent.global[:global_thing]).to eq :global_new_foo_bar expect(other_parent.global[:global_thing]).to eq :global_new_foo_bar end end describe '#api_class' do it 'is specific to the class' do subject.api_class[:some_thing] = :foo_bar parent.api_class[:some_thing] = :some_thing expect(subject.api_class[:some_thing]).to eq :foo_bar expect(parent.api_class[:some_thing]).to eq :some_thing end end describe '#namespace' do it 'sets a value until the end of a namespace' do subject.namespace[:some_thing] = :foo_bar expect(subject.namespace[:some_thing]).to eq :foo_bar end it 'uses new values when a new namespace starts' do subject.namespace[:namespace_thing] = :new_namespace_foo_bar expect(subject.namespace[:namespace_thing]).to eq :new_namespace_foo_bar expect(parent.namespace[:namespace_thing]).to eq :namespace_foo_bar end end describe '#namespace_inheritable' do it 'works with inheritable values' do expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar end it 'handles different parents' do expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.inherit_from other_parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar_other subject.inherit_from parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.inherit_from other_parent subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing subject.inherit_from parent expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing end end describe '#namespace_stackable' do it 'works with stackable values' do expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] subject.inherit_from other_parent expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar_other] end end describe '#namespace_reverse_stackable' do it 'works with reverse stackable values' do expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] subject.inherit_from other_parent expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar_other] end end describe '#route' do it 'sets a value until the next route' do subject.route[:some_thing] = :foo_bar expect(subject.route[:some_thing]).to eq :foo_bar subject.route_end expect(subject.route[:some_thing]).to be_nil end it 'works with route values' do expect(subject.route[:route_thing]).to eq :route_foo_bar end end describe '#api_class' do it 'is specific to the class' do subject.api_class[:some_thing] = :foo_bar expect(subject.api_class[:some_thing]).to eq :foo_bar end end describe '#inherit_from' do it 'notifies clones' do new_settings = subject.point_in_time_copy expect(new_settings).to receive(:inherit_from).with(other_parent) subject.inherit_from other_parent end end describe '#point_in_time_copy' do let!(:cloned_obj) { subject.point_in_time_copy } it 'resets point_in_time_copies' do expect(cloned_obj.point_in_time_copies).to be_empty end it 'decouples namespace values' do subject.namespace[:namespace_thing] = :namespace_foo_bar cloned_obj.namespace[:namespace_thing] = :new_namespace_foo_bar expect(subject.namespace[:namespace_thing]).to eq :namespace_foo_bar end it 'decouples namespace inheritable values' do expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar subject.namespace_inheritable[:namespace_inheritable_thing] = :my_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :namespace_inheritable_foo_bar cloned_obj.namespace_inheritable[:namespace_inheritable_thing] = :my_cloned_thing expect(cloned_obj.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_cloned_thing expect(subject.namespace_inheritable[:namespace_inheritable_thing]).to eq :my_thing end it 'decouples namespace stackable values' do expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] subject.namespace_stackable[:namespace_stackable_thing] = :other_thing expect(subject.namespace_stackable[:namespace_stackable_thing]).to eq %i[namespace_stackable_foo_bar other_thing] expect(cloned_obj.namespace_stackable[:namespace_stackable_thing]).to eq [:namespace_stackable_foo_bar] end it 'decouples namespace reverse stackable values' do expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = :other_thing expect(subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq %i[other_thing namespace_reverse_stackable_foo_bar] expect(cloned_obj.namespace_reverse_stackable[:namespace_reverse_stackable_thing]).to eq [:namespace_reverse_stackable_foo_bar] end it 'decouples route values' do expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar subject.route[:route_thing] = :new_route_foo_bar expect(cloned_obj.route[:route_thing]).to eq :route_foo_bar end it 'adds itself to original as clone' do expect(subject.point_in_time_copies).to include(cloned_obj) end end describe '#to_hash' do it 'return all settings as a hash' do subject.global[:global_thing] = :global_foo_bar subject.namespace[:namespace_thing] = :namespace_foo_bar subject.namespace_inheritable[:namespace_inheritable_thing] = :namespace_inheritable_foo_bar subject.namespace_stackable[:namespace_stackable_thing] = [:namespace_stackable_foo_bar] subject.namespace_reverse_stackable[:namespace_reverse_stackable_thing] = [:namespace_reverse_stackable_foo_bar] subject.route[:route_thing] = :route_foo_bar expect(subject.to_hash).to match( global: { global_thing: :global_foo_bar }, namespace: { namespace_thing: :namespace_foo_bar }, namespace_inheritable: { namespace_inheritable_thing: :namespace_inheritable_foo_bar }, namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] }, namespace_reverse_stackable: { namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] }, route: { route_thing: :route_foo_bar } ) end end end ================================================ FILE: spec/grape/util/inheritable_values_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::InheritableValues do subject { described_class.new(parent) } let(:parent) { described_class.new } describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to be_nil end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq :foo end end describe '#[]' do it 'returns a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end it 'overwrites parent value with the current one' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq :foo_bar end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq :foo end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq :foo end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq(some_thing: :foo, some_thing_more: :foo_bar) end end describe '#clone' do let(:obj_cloned) { subject.clone } context 'complex (i.e. not primitive) data types (ex. entity classes, please see bug #891)' do let(:description) { { entity: double } } before { subject[:description] = description } it 'copies values; does not duplicate them' do expect(obj_cloned[:description]).to eq description end end end end ================================================ FILE: spec/grape/util/media_type_spec.rb ================================================ # frozen_string_literal: true RSpec.describe Grape::Util::MediaType do shared_examples 'MediaType' do it { is_expected.to eq(described_class.new(type: type, subtype: subtype)) } end describe '.parse' do subject(:media_type) { described_class.parse(header) } context 'when header blank?' do let(:header) { nil } it { is_expected.to be_nil } end context 'when header is not a mime type' do let(:header) { 'abc' } it { is_expected.to be_nil } end context 'when header is a valid mime type' do let(:header) { [type, subtype].join('/') } let(:type) { 'text' } let(:subtype) { 'html' } it_behaves_like 'MediaType' context 'when header is a vendor mime type' do let(:type) { 'application' } let(:subtype) { 'vnd.test-v1+json' } it_behaves_like 'MediaType' end context 'when header is a vendor mime type without version' do let(:type) { 'application' } let(:subtype) { 'vnd.ms-word' } it_behaves_like 'MediaType' end end end describe '.match?' do subject { described_class.match?(media_type) } context 'when media_type is blank?' do let(:media_type) { nil } it { is_expected.to be_falsey } end context 'when header is not a mime type' do let(:media_type) { 'abc' } it { is_expected.to be_falsey } end context 'when header is a valid mime type but not vendor' do let(:media_type) { 'text/html' } it { is_expected.to be_falsey } end context 'when header is a vendor mime type' do let(:media_type) { 'application/vnd.test-v1+json' } it { is_expected.to be_truthy } end end describe '.best_quality' do subject(:media_type) { described_class.best_quality(header, available_media_types) } let(:available_media_types) { %w[application/json text/html] } context 'when header is blank?' do let(:header) { nil } let(:type) { 'application' } let(:subtype) { 'json' } it_behaves_like 'MediaType' end context 'when header is not blank' do let(:header) { [type, subtype].join('/') } let(:type) { 'text' } let(:subtype) { 'html' } it 'calls Rack::Utils.best_q_match' do allow(Rack::Utils).to receive(:best_q_match).and_call_original expect(media_type).to eq(described_class.new(type: type, subtype: subtype)) end end end describe '.==' do subject { described_class.new(type: type, subtype: subtype) } let(:type) { 'application' } let(:subtype) { 'vnd.test-v1+json' } let(:other_media_type_class) { Class.new(Struct.new(:type, :subtype, :vendor, :version, :format)) } let(:other_media_type_instance) { other_media_type_class.new(type, subtype, 'test', 'v1', 'json') } it { is_expected.not_to eq(other_media_type_class.new(type, subtype, 'test', 'v1', 'json')) } end describe '.hash' do subject { Set.new([described_class.new(type: type, subtype: subtype)]) } let(:type) { 'text' } let(:subtype) { 'html' } it { is_expected.to include(described_class.new(type: type, subtype: subtype)) } end end ================================================ FILE: spec/grape/util/registry_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::Registry do # Create a test class that includes the Registry module subject { test_registry_class.new } let(:test_registry_class) do Class.new do include Grape::Util::Registry # Public methods to expose private functionality for testing def registry_empty? registry.empty? end def registry_get(key) registry[key] end end end describe '#register' do let(:test_class) do Class.new do def self.name 'TestModule::TestClass' end end end let(:simple_class) do Class.new do def self.name 'SimpleClass' end end end let(:camel_case_class) do Class.new do def self.name 'CamelCaseClass' end end end let(:anonymous_class) { Class.new } let(:nil_name_class) do Class.new do def self.name nil end end end let(:empty_name_class) do Class.new do def self.name '' end end end context 'with valid class names' do it 'registers a class with demodulized and underscored name' do subject.register(test_class) expect(subject.registry_get('test_class')).to eq(test_class) end it 'registers a simple class name correctly' do subject.register(simple_class) expect(subject.registry_get('simple_class')).to eq(simple_class) end it 'handles camel case class names' do subject.register(camel_case_class) expect(subject.registry_get('camel_case_class')).to eq(camel_case_class) end it 'uses indifferent access for registry keys' do subject.register(test_class) expect(subject.registry_get(:test_class)).to eq(test_class) expect(subject.registry_get('test_class')).to eq(test_class) end end context 'with invalid class names' do it 'does not register anonymous classes' do subject.register(anonymous_class) expect(subject.registry_empty?).to be true end it 'does not register classes with nil names' do subject.register(nil_name_class) expect(subject.registry_empty?).to be true end it 'does not register classes with empty names' do subject.register(empty_name_class) expect(subject.registry_empty?).to be true end end context 'with duplicate registrations' do it 'warns when registering a duplicate short name' do expect do subject.register(test_class) subject.register(test_class) end.to output(/test_class is already registered with class.*It will be overridden/).to_stderr end it 'warns with correct short name for different class types' do expect do subject.register(simple_class) subject.register(simple_class) end.to output(/simple_class is already registered with class.*It will be overridden/).to_stderr end it 'warns with correct short name for camel case classes' do expect do subject.register(camel_case_class) subject.register(camel_case_class) end.to output(/camel_case_class is already registered with class.*It will be overridden/).to_stderr end it 'warns for each duplicate registration' do expect do subject.register(test_class) subject.register(test_class) subject.register(test_class) # Third registration should warn again end.to output(/test_class is already registered with class.*It will be overridden/).to_stderr end it 'warns with exact message format' do expected_message = "test_class is already registered with class #{test_class}. It will be overridden globally with the following: #{test_class.name}" expect do subject.register(test_class) subject.register(test_class) end.to output(/#{Regexp.escape(expected_message)}/).to_stderr end it 'overwrites existing registration when duplicate short name is registered' do subject.register(test_class) subject.register(test_class) expect(subject.registry_get('test_class')).to eq(test_class) end end end describe 'edge cases' do it 'handles classes with special characters in names' do special_class = Class.new do def self.name 'Special::Class::With::Many::Modules' end end subject.register(special_class) expect(subject.registry_get('modules')).to eq(special_class) end it 'handles classes with numbers in names' do numbered_class = Class.new do def self.name 'Class123WithNumbers' end end subject.register(numbered_class) expect(subject.registry_get('class123_with_numbers')).to eq(numbered_class) end it 'handles classes with acronyms' do acronym_class = Class.new do def self.name 'API::HTTPClient' end end subject.register(acronym_class) expect(subject.registry_get('http_client')).to eq(acronym_class) end end end ================================================ FILE: spec/grape/util/reverse_stackable_values_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::ReverseStackableValues do subject { described_class.new(parent) } let(:parent) { described_class.new } describe '#keys' do it 'returns all keys' do subject[:some_thing] = :foo_bar subject[:some_thing_else] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else].sort end it 'returns merged keys with parent' do parent[:some_thing] = :foo parent[:some_thing_else] = :foo subject[:some_thing] = :foo_bar subject[:some_thing_more] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort end end describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [] end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [:foo] end end describe '#[]' do it 'returns an array of values' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'combines parent and actual values (actual first)' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq %i[foo_bar foo] end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq [:foo] end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'pushes further values' do subject[:some_thing] = :foo subject[:some_thing] = :bar expect(subject[:some_thing]).to eq %i[foo bar] end it 'can handle array values' do subject[:some_thing] = :foo subject[:some_thing] = %i[bar more] expect(subject[:some_thing]).to eq [:foo, %i[bar more]] parent[:some_thing_else] = %i[foo bar] subject[:some_thing_else] = %i[some bar foo] expect(subject[:some_thing_else]).to eq [%i[some bar foo], %i[foo bar]] end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing] = %i[bar more] subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq( some_thing: [%i[bar more], :foo], some_thing_more: [:foo_bar] ) end end describe '#clone' do let(:obj_cloned) { subject.clone } it 'copies all values' do parent = described_class.new child = described_class.new parent grandchild = described_class.new child parent[:some_thing] = :foo child[:some_thing] = %i[bar more] grandchild[:some_thing] = :grand_foo_bar grandchild[:some_thing_more] = :foo_bar expect(grandchild.clone.to_hash).to eq( some_thing: [:grand_foo_bar, %i[bar more], :foo], some_thing_more: [:foo_bar] ) end context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do let(:middleware) { double } before { subject[:middleware] = middleware } it 'copies values; does not duplicate them' do expect(obj_cloned[:middleware]).to eq [middleware] end end end end ================================================ FILE: spec/grape/util/stackable_values_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::StackableValues do subject { described_class.new(parent) } let(:parent) { described_class.new } describe '#keys' do it 'returns all keys' do subject[:some_thing] = :foo_bar subject[:some_thing_else] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else].sort end it 'returns merged keys with parent' do parent[:some_thing] = :foo parent[:some_thing_else] = :foo subject[:some_thing] = :foo_bar subject[:some_thing_more] = :foo_bar expect(subject.keys).to eq %i[some_thing some_thing_else some_thing_more].sort end end describe '#delete' do it 'deletes a key' do subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [] end it 'does not delete parent values' do parent[:some_thing] = :foo subject[:some_thing] = :new_foo_bar subject.delete :some_thing expect(subject[:some_thing]).to eq [:foo] end end describe '#[]' do it 'returns an array of values' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'returns parent value when no value is set' do parent[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'combines parent and actual values' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(subject[:some_thing]).to eq %i[foo foo_bar] end it 'parent values are not changed' do parent[:some_thing] = :foo subject[:some_thing] = :foo_bar expect(parent[:some_thing]).to eq [:foo] end end describe '#[]=' do it 'sets a value' do subject[:some_thing] = :foo expect(subject[:some_thing]).to eq [:foo] end it 'pushes further values' do subject[:some_thing] = :foo subject[:some_thing] = :bar expect(subject[:some_thing]).to eq %i[foo bar] end it 'can handle array values' do subject[:some_thing] = :foo subject[:some_thing] = %i[bar more] expect(subject[:some_thing]).to eq [:foo, %i[bar more]] parent[:some_thing_else] = %i[foo bar] subject[:some_thing_else] = %i[some bar foo] expect(subject[:some_thing_else]).to eq [%i[foo bar], %i[some bar foo]] end end describe '#to_hash' do it 'returns a Hash representation' do parent[:some_thing] = :foo subject[:some_thing] = %i[bar more] subject[:some_thing_more] = :foo_bar expect(subject.to_hash).to eq(some_thing: [:foo, %i[bar more]], some_thing_more: [:foo_bar]) end end describe '#clone' do let(:obj_cloned) { subject.clone } it 'copies all values' do parent = described_class.new child = described_class.new parent grandchild = described_class.new child parent[:some_thing] = :foo child[:some_thing] = %i[bar more] grandchild[:some_thing] = :grand_foo_bar grandchild[:some_thing_more] = :foo_bar expect(grandchild.clone.to_hash).to eq(some_thing: [:foo, %i[bar more], :grand_foo_bar], some_thing_more: [:foo_bar]) end context 'complex (i.e. not primitive) data types (ex. middleware, please see bug #930)' do let(:middleware) { double } before { subject[:middleware] = middleware } it 'copies values; does not duplicate them' do expect(obj_cloned[:middleware]).to eq [middleware] end end end end ================================================ FILE: spec/grape/util/translation_spec.rb ================================================ # frozen_string_literal: true describe Grape::Util::Translation do subject(:translator) do Class.new do include Grape::Util::Translation def translate_message(key, **opts) translate(key, **opts) end end.new end describe '#translate_message' do context 'when the translation value uses a reserved I18n interpolation key' do around do |example| I18n.backend.store_translations(:en, grape: { errors: { messages: { reserved_key_test: 'value %{scope}' } } }) # rubocop:disable Style/FormatStringToken example.run ensure I18n.reload! end it 'raises I18n::ReservedInterpolationKey' do expect { translator.translate_message(:reserved_key_test) }.to raise_error(I18n::ReservedInterpolationKey) end end end end ================================================ FILE: spec/grape/validations/multiple_attributes_iterator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::MultipleAttributesIterator do describe '#each' do subject(:iterator) { described_class.new(validator, scope, params) } let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } let(:validator) { double(attrs: %i[first second third]) } context 'when params is a hash' do let(:params) do { first: 'string', second: 'string' } end it 'yields the whole params hash without the list of attrs' do expect { |b| iterator.each(&b) }.to yield_with_args(params) end end context 'when params is an array' do let(:params) do [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] end it 'yields each element of the array without the list of attrs' do expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1]) end end context 'when params is empty optional placeholder' do let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue] } it 'does not yield it' do expect { |b| iterator.each(&b) }.to yield_successive_args end end end end ================================================ FILE: spec/grape/validations/param_scope_tracker_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::ParamScopeTracker do describe '.current' do it 'returns nil when no tracker is active' do expect(described_class.current).to be_nil end end describe '.track' do it 'sets .current inside the block' do described_class.track do expect(described_class.current).to be_a(described_class) end end it 'restores nil after the block' do described_class.track { nil } expect(described_class.current).to be_nil end it 'restores nil after an exception' do expect { described_class.track { raise 'boom' } }.to raise_error('boom') expect(described_class.current).to be_nil end it 'creates a fresh tracker for each invocation' do first = nil second = nil described_class.track { first = described_class.current } described_class.track { second = described_class.current } expect(first).not_to equal(second) end context 'when nested (reentrant)' do it 'restores the outer tracker, not nil' do outer = nil inner = nil described_class.track do outer = described_class.current described_class.track { inner = described_class.current } expect(described_class.current).to equal(outer) end expect(inner).not_to equal(outer) expect(described_class.current).to be_nil end it 'restores outer tracker after inner raises' do described_class.track do outer = described_class.current expect { described_class.track { raise 'inner' } }.to raise_error('inner') expect(described_class.current).to equal(outer) end end end end describe '#store_index / #index_for' do subject(:tracker) { described_class.new } let(:scope_a) { instance_double(Grape::Validations::ParamsScope) } let(:scope_b) { instance_double(Grape::Validations::ParamsScope) } it 'returns nil for an unknown scope' do expect(tracker.index_for(scope_a)).to be_nil end it 'returns the stored index for the given scope' do tracker.store_index(scope_a, 3) expect(tracker.index_for(scope_a)).to eq(3) end it 'stores indices independently per scope' do tracker.store_index(scope_a, 0) tracker.store_index(scope_b, 7) expect(tracker.index_for(scope_a)).to eq(0) expect(tracker.index_for(scope_b)).to eq(7) end it 'uses object identity, not value equality, as the key' do equal_double = instance_double(Grape::Validations::ParamsScope) tracker.store_index(scope_a, 1) expect(tracker.index_for(equal_double)).to be_nil end it 'overwrites a previously stored index' do tracker.store_index(scope_a, 1) tracker.store_index(scope_a, 5) expect(tracker.index_for(scope_a)).to eq(5) end end describe '#store_qualifying_params / #qualifying_params' do subject(:tracker) { described_class.new } let(:scope) { instance_double(Grape::Validations::ParamsScope) } it 'returns EMPTY_PARAMS for an unknown scope' do expect(tracker.qualifying_params(scope)).to equal(described_class::EMPTY_PARAMS) end it 'returns the stored params for the given scope' do params = [{ id: 1 }, { id: 2 }] tracker.store_qualifying_params(scope, params) expect(tracker.qualifying_params(scope)).to eq(params) end it 'treats an explicitly stored empty array the same as never stored (blank)' do tracker.store_qualifying_params(scope, []) expect(tracker.qualifying_params(scope).presence).to be_nil end it 'uses object identity as the key' do other_scope = instance_double(Grape::Validations::ParamsScope) tracker.store_qualifying_params(scope, [{ id: 1 }]) expect(tracker.qualifying_params(other_scope)).to equal(described_class::EMPTY_PARAMS) end end end ================================================ FILE: spec/grape/validations/params_documentation_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::ParamsDocumentation do subject { klass.new(api_double) } let(:api_double) do Class.new do include Grape::DSL::Settings end.new end let(:klass) do Class.new do include Grape::Validations::ParamsDocumentation attr_accessor :api def initialize(api) @api = api end def full_name(name) "full_name_#{name}" end end end describe '#document_params' do it 'stores documented params with all details' do attrs = %i[foo bar] validations = { presence: true, default: 42, length: { min: 1, max: 10 }, desc: 'A foo', documentation: { note: 'doc' } } type = Integer values = [1, 2, 3] except_values = [4, 5, 6] subject.document_params(attrs, validations.dup, type, values, except_values) expect(api_double.inheritable_setting.namespace_stackable[:params].first.keys).to include('full_name_foo', 'full_name_bar') expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).to include( required: true, type: 'Integer', values: [1, 2, 3], except_values: [4, 5, 6], default: 42, min_length: 1, max_length: 10, desc: 'A foo', documentation: { note: 'doc' } ) end context 'when do_not_document is set' do let(:validations) do { desc: 'desc', description: 'description', documentation: { foo: 'bar' }, another_param: 'test' } end before do api_double.inheritable_setting.namespace_inheritable[:do_not_document] = true end it 'removes desc, description, and documentation' do subject.document_params([:foo], validations) expect(validations).to eq({ another_param: 'test' }) end end context 'when validation is empty' do let(:validations) do {} end it 'does not raise an error' do expect { subject.document_params([:foo], validations) }.not_to raise_error expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).to eq({ required: false }) end end context 'when desc is not present' do let(:validations) do { description: 'desc2' } end it 'uses description if desc is not present' do subject.document_params([:foo], validations) expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo'][:desc]).to eq('desc2') end end context 'when desc nor description is present' do let(:validations) do {} end it 'uses description if desc is not present' do subject.document_params([:foo], validations) expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).to eq({ required: false }) end end context 'when documentation is not present' do let(:validations) do {} end it 'does not include documentation' do subject.document_params([:foo], validations) expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo']).not_to have_key(:documentation) end end context 'when type is nil' do let(:validations) do { presence: true } end it 'sets type as nil' do subject.document_params([:foo], validations) expect(api_double.inheritable_setting.namespace_stackable[:params].first['full_name_foo'][:type]).to be_nil end end end end ================================================ FILE: spec/grape/validations/params_scope_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::ParamsScope do subject do Class.new(Grape::API) end def app subject end context 'when using custom types' do let(:custom_type) do Class.new do attr_reader :value def self.parse(value) raise if value == 'invalid' new(value) end def initialize(value) @value = value end end end it 'coerces the parameter via the type\'s parse method' do context = self subject.params do requires :foo, type: context.custom_type end subject.get('/types') { params[:foo].value } get '/types', foo: 'valid' expect(last_response.status).to eq(200) expect(last_response.body).to eq('valid') get '/types', foo: 'invalid' expect(last_response.status).to eq(400) expect(last_response.body).to match(/foo is invalid/) end end context 'param renaming' do it do subject.params do requires :foo, as: :bar optional :super, as: :hiper end subject.get('/renaming') { "#{declared(params)['bar']}-#{declared(params)['hiper']}" } get '/renaming', foo: 'any', super: 'any2' expect(last_response.status).to eq(200) expect(last_response.body).to eq('any-any2') end it do subject.params do requires :foo, as: :bar, type: String, coerce_with: lambda(&:strip) end subject.get('/renaming-coerced') { "#{params['bar']}-#{params['foo']}" } get '/renaming-coerced', foo: ' there we go ' expect(last_response.status).to eq(200) expect(last_response.body).to eq('-there we go') end it do subject.params do requires :foo, as: :bar, allow_blank: false end subject.get('/renaming-not-blank') {} get '/renaming-not-blank', foo: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('foo is empty') end it do subject.params do requires :foo, as: :bar, allow_blank: false end subject.get('/renaming-not-blank-with-value') {} get '/renaming-not-blank-with-value', foo: 'any' expect(last_response.status).to eq(200) end it do subject.params do requires :foo, as: :baz, type: Hash do requires :bar, as: :qux end end subject.get('/nested-renaming') { declared(params).to_json } get '/nested-renaming', foo: { bar: 'any' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"baz":{"qux":"any"}}') end it 'renaming can be defined before default' do subject.params do optional :foo, as: :bar, default: 'before' end subject.get('/rename-before-default') { declared(params)[:bar] } get '/rename-before-default' expect(last_response.status).to eq(200) expect(last_response.body).to eq('before') end it 'renaming can be defined after default' do subject.params do optional :foo, default: 'after', as: :bar end subject.get('/rename-after-default') { declared(params)[:bar] } get '/rename-after-default' expect(last_response.status).to eq(200) expect(last_response.body).to eq('after') end end context 'array without coerce type explicitly given' do it 'sets the type based on first element' do subject.params do requires :periods, type: Array, values: -> { %w[day month] } end subject.get('/required') { 'required works' } get '/required', periods: %w[day month] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'fails to call API without Array type' do subject.params do requires :periods, type: Array, values: -> { %w[day month] } end subject.get('/required') { 'required works' } get '/required', periods: 'day' expect(last_response.status).to eq(400) expect(last_response.body).to eq('periods is invalid') end it 'raises exception when values are of different type' do expect do subject.params { requires :numbers, type: Array, values: [1, 'definitely not a number', 3] } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises exception when range values have different endpoint types' do expect do subject.params { requires :numbers, type: Array, values: 0.0..10 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context 'coercing values validation with proc' do it 'allows the proc to pass validation without checking' do subject.params { requires :numbers, type: Integer, values: -> { [0, 1, 2] } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '1' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end it 'allows the proc to pass validation without checking in value' do subject.params { requires :numbers, type: Integer, values: { value: -> { [0, 1, 2] } } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '1' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end it 'allows the proc to pass validation without checking in except' do subject.params { requires :numbers, type: Integer, except_values: -> { [0, 1, 2] } } subject.post('/required') { 'coercion with proc works' } post '/required', numbers: '10' expect(last_response.status).to eq(201) expect(last_response.body).to eq('coercion with proc works') end end context 'a Set with coerce type explicitly given' do context 'and the values are allowed' do it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Set[Integer], values: 0..2, default: 0..2 } end.not_to raise_error end end context 'and the values are not allowed' do it 'raises exception' do expect do subject.params { optional :numbers, type: Set[Integer], values: %w[a b] } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end end context 'with range values' do context "when left range endpoint isn't #kind_of? the type" do it 'raises exception' do expect do subject.params { requires :latitude, type: Integer, values: -90.0..90 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context "when right range endpoint isn't #kind_of? the type" do it 'raises exception' do expect do subject.params { requires :latitude, type: Integer, values: -90..90.0 } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context 'when the default is an array' do context 'and is the entire range of allowed values' do it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: 0..2, default: 0..2 } end.not_to raise_error end end context 'and is a subset of allowed values' do it 'does not raise an exception' do expect do subject.params { optional :numbers, type: Array[Integer], values: [0, 1, 2], default: [1, 0] } end.not_to raise_error end end end context 'when both range endpoints are #kind_of? the type' do it 'accepts values in the range' do subject.params do requires :letter, type: String, values: 'a'..'z' end subject.get('/letter') { params[:letter] } get '/letter', letter: 'j' expect(last_response.status).to eq(200) expect(last_response.body).to eq('j') end it 'rejects values outside the range' do subject.params do requires :letter, type: String, values: 'a'..'z' end subject.get('/letter') { params[:letter] } get '/letter', letter: 'J' expect(last_response.status).to eq(400) expect(last_response.body).to eq('letter does not have a valid value') end end end context 'parameters in group' do it 'errors when no type is provided' do expect do subject.params do group :a do requires :b end end end.to raise_error Grape::Exceptions::MissingGroupType expect do subject.params do optional :a do requires :b end end end.to raise_error Grape::Exceptions::MissingGroupType end it 'allows Hash as type' do subject.params do group :a, type: Hash do requires :b end end subject.get('/group') { 'group works' } get '/group', a: { b: true } expect(last_response.status).to eq(200) expect(last_response.body).to eq('group works') subject.params do optional :a, type: Hash do requires :b end end get '/optional_type_hash' end it 'allows Array as type' do subject.params do group :a, type: Array do requires :b end end subject.get('/group') { 'group works' } get '/group', a: [{ b: true }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('group works') subject.params do optional :a, type: Array do requires :b end end get '/optional_type_array' end it 'handles missing optional Array type' do subject.params do optional :a, type: Array do requires :b end end subject.get('/test') { declared(params).to_json } get '/test' expect(last_response.status).to eq(200) expect(last_response.body).to eq('{"a":[]}') end it 'errors with an unsupported type' do expect do subject.params do group :a, type: Set do requires :b end end end.to raise_error Grape::Exceptions::UnsupportedGroupType expect do subject.params do optional :a, type: Set do requires :b end end end.to raise_error Grape::Exceptions::UnsupportedGroupType end end context 'when validations are dependent on a parameter' do before do subject.params do optional :a given :a do requires :b end end subject.get('/test') { declared(params).to_json } end it 'applies the validations only if the parameter is present' do get '/test' expect(last_response.status).to eq(200) get '/test', a: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('b is missing') get '/test', a: true, b: true expect(last_response.status).to eq(200) end it 'applies the validations of multiple parameters' do subject.params do optional :a, :b given :a, :b do requires :c end end subject.get('/multiple') { declared(params).to_json } get '/multiple' expect(last_response.status).to eq(200) get '/multiple', a: true expect(last_response.status).to eq(200) get '/multiple', b: true expect(last_response.status).to eq(200) get '/multiple', a: true, b: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', a: true, b: true, c: true expect(last_response.status).to eq(200) end it 'applies only the appropriate validation' do subject.params do optional :a optional :b mutually_exclusive :a, :b given :a do requires :c, type: String end given :b do requires :c, type: Integer end end subject.get('/multiple') { declared(params).to_json } get '/multiple' expect(last_response.status).to eq(200) get '/multiple', a: true, c: 'test' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).symbolize_keys).to eq a: 'true', b: nil, c: 'test' get '/multiple', b: true, c: '3' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body).symbolize_keys).to eq a: nil, b: 'true', c: 3 get '/multiple', a: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', b: true expect(last_response.status).to eq(400) expect(last_response.body).to eq('c is missing') get '/multiple', a: true, b: true, c: 'test' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a, b are mutually exclusive, c is invalid') end it 'raises an error if the dependent parameter was never specified' do expect do subject.params do given :c do end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'does not raise an error if the dependent parameter is a Hash' do expect do subject.params do optional :a, type: Hash do requires :b end given :a do requires :c end end end.not_to raise_error end it 'does not raise an error if when using nested given' do expect do subject.params do optional :a, type: Hash do requires :b end given :a do requires :c given :c do requires :d end end end end.not_to raise_error end it 'allows nested dependent parameters' do subject.params do optional :a given a: ->(val) { val == 'a' } do optional :b given b: ->(val) { val == 'b' } do optional :c given c: ->(val) { val == 'c' } do requires :d end end end end subject.get('/') { declared(params).to_json } get '/' expect(last_response.status).to eq 200 get '/', a: 'a', b: 'b', c: 'c' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'd is missing' get '/', a: 'a', b: 'b', c: 'c', d: 'd' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ a: 'a', b: 'b', c: 'c', d: 'd' }.to_json) end it 'allows renaming of dependent parameters' do subject.params do optional :a given :a do requires :b, as: :c end end subject.get('/multiple') { declared(params).to_json } get '/multiple', a: 'a', b: 'b' body = JSON.parse(last_response.body) expect(body.keys).to include('c') expect(body.keys).not_to include('b') end it 'allows renaming of dependent on parameter' do subject.params do optional :a, as: :b given a: ->(val) { val == 'x' } do requires :c end end subject.get('/') { declared(params) } get '/', a: 'x' expect(last_response.status).to eq 400 expect(last_response.body).to eq 'c is missing' get '/', a: 'y' expect(last_response.status).to eq 200 end it 'does not raise if the dependent parameter is not the renamed one' do expect do subject.params do optional :a, as: :b given :a do requires :c end end end.not_to raise_error end it 'raises an error if the dependent parameter is the renamed one' do expect do subject.params do optional :a, as: :b given :b do requires :c end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'does not validate nested requires when given is false' do subject.params do requires :a, type: String, allow_blank: false, values: %w[x y z] given a: ->(val) { val == 'x' } do requires :inner1, type: Hash, allow_blank: false do requires :foo, type: Integer, allow_blank: false end end given a: ->(val) { val == 'y' } do requires :inner2, type: Hash, allow_blank: false do requires :bar, type: Integer, allow_blank: false requires :baz, type: Array, allow_blank: false do requires :baz_category, type: String, allow_blank: false end end end given a: ->(val) { val == 'z' } do requires :inner3, type: Array, allow_blank: false do requires :bar, type: Integer, allow_blank: false requires :baz, type: Array, allow_blank: false do requires :baz_category, type: String, allow_blank: false end end end end subject.get('/varying') { declared(params).to_json } get '/varying', a: 'x', inner1: { foo: 1 } expect(last_response.status).to eq(200) get '/varying', a: 'y', inner2: { bar: 2, baz: [{ baz_category: 'barstools' }] } expect(last_response.status).to eq(200) get '/varying', a: 'y', inner2: { bar: 2, baz: [{ unrelated: 'yep' }] } expect(last_response.status).to eq(400) get '/varying', a: 'z', inner3: [{ bar: 3, baz: [{ baz_category: 'barstools' }] }] expect(last_response.status).to eq(200) end it 'detect unmet nested dependency' do subject.params do requires :a, type: String, allow_blank: false, values: %w[x y z] given a: ->(val) { val == 'z' } do requires :inner3, type: Array, allow_blank: false do requires :bar, type: String, allow_blank: false given bar: ->(val) { val == 'b' } do requires :baz, type: Array do optional :baz_category, type: String end end given bar: ->(val) { val == 'c' } do requires :baz, type: Array do requires :baz_category, type: String end end end end end subject.get('/nested-dependency') { declared(params).to_json } get '/nested-dependency', a: 'z', inner3: [{ bar: 'c', baz: [{ unrelated: 'nope' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq 'inner3[0][baz][0][baz_category] is missing' end context 'detect when json array' do before do subject.params do requires :array, type: Array do requires :a, type: String given a: ->(val) { val == 'a' } do requires :json, type: Hash do requires :b, type: String end end end end subject.post '/nested_array' do 'nested array works!' end end it 'succeeds' do params = { array: [ { a: 'a', json: { b: 'b' } }, { a: 'b' } ] } post '/nested_array', params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) end it 'fails' do params = { array: [ { a: 'a', json: { b: 'b' } }, { a: 'a' } ] } post '/nested_array', params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) expect(last_response.body).to eq('array[1][json] is missing, array[1][json][b] is missing') end end context 'array without given' do before do subject.params do requires :array, type: Array do requires :a, type: Integer requires :b, type: Integer end end subject.post '/array_without_given' end it 'fails' do params = { array: [ { a: 1, b: 2 }, { a: 3 }, { a: 5 } ] } post '/array_without_given', params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('array[1][b] is missing, array[2][b] is missing') expect(last_response.status).to eq(400) end end context 'array with given' do before do subject.params do requires :array, type: Array do requires :a, type: Integer given a: lambda(&:odd?) do requires :b, type: Integer end end end subject.post '/array_with_given' end it 'fails' do params = { array: [ { a: 1, b: 2 }, { a: 3 }, { a: 5 } ] } post '/array_with_given', params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('array[1][b] is missing, array[2][b] is missing') expect(last_response.status).to eq(400) end end context 'nested json array with given' do before do subject.params do requires :workflow_nodes, type: Array do requires :steps, type: Array do requires :id, type: String optional :type, type: String, values: %w[send_messsge assign_user assign_team tag_conversation snooze close add_commit] given type: ->(val) { val == 'send_messsge' } do requires :message, type: Hash do requires :content, type: String end end end end end subject.post '/nested_json_array_with_given' end it 'passes' do params = { workflow_nodes: [ { id: 'eqibmvEzPo8hQOSt', title: 'Node 1', is_start: true, steps: [ { id: 'DvdSZaIm1hEd5XO5', type: 'send_messsge', message: { content: '打击好', menus: [] } }, { id: 'VY6MIwycBw0b51Ib', type: 'add_commit', comment_content: '初来乍到' } ] } ] } post '/nested_json_array_with_given', params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(201) end end it 'includes the parameter within #declared(params)' do get '/test', a: true, b: true expect(JSON.parse(last_response.body)).to eq('a' => 'true', 'b' => 'true') end it 'returns a sensible error message within a nested context' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :b end end end subject.get('/nested') { 'worked' } get '/nested', bar: { a: true } expect(last_response.status).to eq(400) expect(last_response.body).to eq('bar[b] is missing') end it 'includes the nested parameter within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :b end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: true, b: 'yes' } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'b' => 'yes' }) end it 'includes level 2 nested parameters outside the given within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given :a do requires :c, type: Hash do requires :b end end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: true, c: { b: 'yes' } } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'true', 'c' => { 'b' => 'yes' } }) end context 'when the dependent parameter is not present #declared(params)' do context 'lateral parameter' do before do [true, false].each do |evaluate_given| subject.params do optional :a given :a do optional :b end end subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } end end it 'evaluate_given_false' do get '/evaluate_given_false', b: 'b' expect(JSON.parse(last_response.body)).to eq('a' => nil, 'b' => 'b') end it 'evaluate_given_true' do get '/evaluate_given_true', b: 'b' expect(JSON.parse(last_response.body)).to eq('a' => nil) end end context 'lateral hash parameter' do before do [true, false].each do |evaluate_given| subject.params do optional :a, values: %w[x y] given a: ->(a) { a == 'x' } do optional :b, type: Hash do optional :c end optional :e end given a: ->(a) { a == 'y' } do optional :b, type: Hash do optional :d end optional :f end end subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } end end it 'evaluate_given_false' do get '/evaluate_given_false', a: 'x' expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil }, 'e' => nil, 'f' => nil) get '/evaluate_given_false', a: 'y' expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }, 'e' => nil, 'f' => nil) end it 'evaluate_given_true' do get '/evaluate_given_true', a: 'x' expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => nil }, 'e' => nil) get '/evaluate_given_true', a: 'y' expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }, 'f' => nil) end end context 'lateral parameter within lateral hash parameter' do before do [true, false].each do |evaluate_given| subject.params do optional :a, values: %w[x y] given a: ->(a) { a == 'x' } do optional :b, type: Hash do optional :c given :c do optional :g optional :e, type: Hash do optional :h end end end end given a: ->(a) { a == 'y' } do optional :b, type: Hash do optional :d given :d do optional :f optional :e, type: Hash do optional :i end end end end end subject.get("/evaluate_given_#{evaluate_given}") { declared(params, evaluate_given: evaluate_given).to_json } end end it 'evaluate_given_false' do get '/evaluate_given_false', a: 'x' expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) get '/evaluate_given_false', a: 'x', b: { c: 'c' } expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) get '/evaluate_given_false', a: 'y' expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil, 'f' => nil, 'e' => { 'i' => nil } }) get '/evaluate_given_false', a: 'y', b: { d: 'd' } expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => 'd', 'f' => nil, 'e' => { 'i' => nil } }) end it 'evaluate_given_true' do get '/evaluate_given_true', a: 'x' expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => nil }) get '/evaluate_given_true', a: 'x', b: { c: 'c' } expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => { 'c' => 'c', 'g' => nil, 'e' => { 'h' => nil } }) get '/evaluate_given_true', a: 'y' expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => nil }) get '/evaluate_given_true', a: 'y', b: { d: 'd' } expect(JSON.parse(last_response.body)).to eq('a' => 'y', 'b' => { 'd' => 'd', 'f' => nil, 'e' => { 'i' => nil } }) end end context 'lateral parameter within an array param' do before do [true, false].each do |evaluate_given| subject.params do optional :array, type: Array do optional :a given :a do optional :b end end end subject.post("/evaluate_given_#{evaluate_given}") do declared(params, evaluate_given: evaluate_given).to_json end end end it 'evaluate_given_false' do post '/evaluate_given_false', { array: [{ b: 'b' }, { a: 'a', b: 'b' }] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => nil, 'b' => 'b' }, { 'a' => 'a', 'b' => 'b' }]) end it 'evaluate_given_true' do post '/evaluate_given_true', { array: [{ b: 'b' }, { a: 'a', b: 'b' }] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => nil }, { 'a' => 'a', 'b' => 'b' }]) end end context 'nested given parameter' do before do [true, false].each do |evaluate_given| subject.params do optional :a optional :c given :a do given :c do optional :b end end end subject.post("/evaluate_given_#{evaluate_given}") do declared(params, evaluate_given: evaluate_given).to_json end end end it 'evaluate_given_false' do post '/evaluate_given_false', { a: 'a', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => nil) post '/evaluate_given_false', { c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => nil, 'b' => 'b', 'c' => 'c') post '/evaluate_given_false', { a: 'a', c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => 'c') end it 'evaluate_given_true' do post '/evaluate_given_true', { a: 'a', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'c' => nil) post '/evaluate_given_true', { c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => nil, 'c' => 'c') post '/evaluate_given_true', { a: 'a', c: 'c', b: 'b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('a' => 'a', 'b' => 'b', 'c' => 'c') end end context 'nested given parameter within an array param' do before do [true, false].each do |evaluate_given| subject.params do optional :array, type: Array do optional :a optional :c given :a do given :c do optional :b end end end end subject.post("/evaluate_given_#{evaluate_given}") do declared(params, evaluate_given: evaluate_given).to_json end end end let :evaluate_given_params do { array: [ { a: 'a', b: 'b' }, { c: 'c', b: 'b' }, { a: 'a', c: 'c', b: 'b' } ] } end it 'evaluate_given_false' do post '/evaluate_given_false', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => 'a', 'b' => 'b', 'c' => nil }, { 'a' => nil, 'b' => 'b', 'c' => 'c' }, { 'a' => 'a', 'b' => 'b', 'c' => 'c' }]) end it 'evaluate_given_true' do post '/evaluate_given_true', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq('array' => [{ 'a' => 'a', 'c' => nil }, { 'a' => nil, 'c' => 'c' }, { 'a' => 'a', 'b' => 'b', 'c' => 'c' }]) end end context 'nested given parameter within a nested given parameter within an array param' do before do [true, false].each do |evaluate_given| subject.params do optional :array, type: Array do optional :a optional :c given :a do given :c do optional :array, type: Array do optional :a optional :c given :a do given :c do optional :b end end end end end end end subject.post("/evaluate_given_#{evaluate_given}") do declared(params, evaluate_given: evaluate_given).to_json end end end let :evaluate_given_params do { array: [{ a: 'a', c: 'c', array: [ { a: 'a', b: 'b' }, { c: 'c', b: 'b' }, { a: 'a', c: 'c', b: 'b' } ] }] } end it 'evaluate_given_false' do expected_response_hash = { 'array' => [{ 'a' => 'a', 'c' => 'c', 'array' => [ { 'a' => 'a', 'b' => 'b', 'c' => nil }, { 'a' => nil, 'c' => 'c', 'b' => 'b' }, { 'a' => 'a', 'c' => 'c', 'b' => 'b' } ] }] } post '/evaluate_given_false', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq(expected_response_hash) end it 'evaluate_given_true' do expected_response_hash = { 'array' => [{ 'a' => 'a', 'c' => 'c', 'array' => [ { 'a' => 'a', 'c' => nil }, { 'a' => nil, 'c' => 'c' }, { 'a' => 'a', 'b' => 'b', 'c' => 'c' } ] }] } post '/evaluate_given_true', evaluate_given_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(JSON.parse(last_response.body)).to eq(expected_response_hash) end end end end context 'default value in given block' do before do subject.params do optional :a, values: %w[a b] given a: ->(val) { val == 'a' } do optional :b, default: 'default' end end subject.get('/') { params.to_json } end context 'when dependency meets' do it 'sets default value for dependent parameter' do get '/', a: 'a' expect(last_response.body).to eq({ a: 'a', b: 'default' }.to_json) end end context 'when dependency does not meet' do it 'does not set default value for dependent parameter' do get '/', a: 'b' expect(last_response.body).to eq({ a: 'b' }.to_json) end end end context 'when validations are dependent on a parameter within an array param' do before do subject.params do requires :foos, type: Array do optional :foo given :foo do requires :bar end end end subject.get('/test') { 'ok' } end it 'passes none Hash params' do get '/test', foos: [''] expect(last_response.status).to eq(200) expect(last_response.body).to eq('ok') end end context 'when validations are dependent on a parameter within an array param within #declared(params).to_json' do before do subject.params do requires :foos, type: Array do optional :foo_type, :baz_type given :foo_type do requires :bar end end end subject.post('/test') { declared(params).to_json } end it 'applies the constraint within each value' do post '/test', { foos: [{ foo_type: 'a' }, { baz_type: 'c' }] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) expect(last_response.body).to eq('foos[0][bar] is missing') end end context 'when validations are dependent on a parameter with specific value' do # build test cases from all combinations of declarations and options a_decls = %i[optional requires] a_options = [{}, { values: %w[x y z] }] b_options = [{}, { type: String }, { allow_blank: false }, { type: String, allow_blank: false }] combinations = a_decls.product(a_options, b_options) combinations.each_with_index do |(a_decl, a_opts, b_opts), i| context "(case #{i})" do before do # puts "a_decl: #{a_decl}, a_opts: #{a_opts}, b_opts: #{b_opts}" subject.params do __send__ a_decl, :a, **a_opts given(a: ->(val) { val == 'x' }) { requires :b, **b_opts } given(a: ->(val) { val == 'y' }) { requires :c, **b_opts } end subject.get('/test') { declared(params).to_json } end if a_decl == :optional it 'skips validation when base param is missing' do get '/test' expect(last_response.status).to eq(200) end end it 'skips validation when base param does not have a specified value' do get '/test', a: 'z' expect(last_response.status).to eq(200) get '/test', a: 'z', b: '' expect(last_response.status).to eq(200) end it 'applies the validation when base param has the specific value' do get '/test', a: 'x' expect(last_response.status).to eq(400) expect(last_response.body).to include('b is missing') get '/test', a: 'x', b: true expect(last_response.status).to eq(200) get '/test', a: 'x', b: true, c: '' expect(last_response.status).to eq(200) end it 'includes the parameter within #declared(params)' do get '/test', a: 'x', b: true expect(JSON.parse(last_response.body)).to eq('a' => 'x', 'b' => 'true', 'c' => nil) end end end end it 'raises an error if the dependent parameter was never specified' do expect do subject.params do given :c do end end end.to raise_error(Grape::Exceptions::UnknownParameter) end it 'returns a sensible error message within a nested context' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :b end end end subject.get('/nested') { 'worked' } get '/nested', bar: { a: 'x' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('bar[b] is missing') end it 'includes the nested parameter within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :b end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: 'x', b: 'yes' } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'b' => 'yes' }) end it 'includes level 2 nested parameters outside the given within #declared(params)' do subject.params do requires :bar, type: Hash do optional :a given a: ->(val) { val == 'x' } do requires :c, type: Hash do requires :b end end end end subject.get('/nested') { declared(params).to_json } get '/nested', bar: { a: 'x', c: { b: 'yes' } } expect(JSON.parse(last_response.body)).to eq('bar' => { 'a' => 'x', 'c' => { 'b' => 'yes' } }) end it 'includes deeply nested parameters within #declared(params)' do subject.params do requires :arr1, type: Array do requires :hash1, type: Hash do requires :arr2, type: Array do requires :hash2, type: Hash do requires :something, type: String end end end end end subject.get('/nested_deep') { declared(params).to_json } get '/nested_deep', arr1: [{ hash1: { arr2: [{ hash2: { something: 'value' } }] } }] expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq('arr1' => [{ 'hash1' => { 'arr2' => [{ 'hash2' => { 'something' => 'value' } }] } }]) end context 'failing fast' do context 'when fail_fast is not defined' do it 'does not stop validation' do subject.params do requires :one requires :two requires :three end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is missing, two is missing, three is missing') end end context 'when fail_fast is defined it stops the validation' do it 'of other params' do subject.params do requires :one, fail_fast: true requires :two end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is missing') end it 'for a single param' do subject.params do requires :one, allow_blank: false, regexp: /[0-9]+/, fail_fast: true end subject.get('/fail-fast') { declared(params).to_json } get '/fail-fast', one: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('one is empty') end end end context 'when params have group attributes' do context 'with validations' do before do subject.params do with(allow_blank: false) do requires :id optional :name optional :address, allow_blank: true end end subject.get('test') end context 'when data is invalid' do before do get 'test', id: '', name: '' end it 'returns a validation error' do expect(last_response.status).to eq(400) end it 'applies group validations for every parameter' do expect(last_response.body).to eq('id is empty, name is empty') end end context 'when parameter has the same validator as a group' do before do get 'test', id: 'id', address: '' end it 'returns a successful response' do expect(last_response.status).to eq(200) end it 'prioritizes parameter validation over group validation' do expect(last_response.body).not_to include('address is empty') end end end context 'with types' do before do subject.params do with(type: Date) do requires :created_at end end subject.get('test') { params[:created_at] } end context 'when invalid date provided' do before do get 'test', created_at: 'not_a_date' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('created_at is invalid') end end context 'when created_at receives a valid date' do before do get 'test', created_at: '2016-01-01' end it 'returns a successful response' do expect(last_response.status).to eq(200) end it 'returns a date' do expect(last_response.body).to eq('2016-01-01') end end end context 'with several group attributes' do before do subject.params do with(values: [1]) do requires :id, type: Integer end with(allow_blank: false) do optional :address, type: String end requires :name end subject.get('test') end context 'when data is invalid' do before do get 'test', id: 2, address: '' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('id does not have a valid value, address is empty, name is missing') end end context 'when correct data is provided' do before do get 'test', id: 1, address: 'Some street', name: 'John' end it 'returns a successful response' do expect(last_response.status).to eq(200) end end end context 'with nested groups' do before do subject.params do with(type: Integer) do requires :id with(type: Date) do requires :created_at optional :updated_at end end end subject.get('test') end context 'when data is invalid' do before do get 'test', id: 'wrong', created_at: 'not_a_date', updated_at: '2016-01-01' end it 'responds with HTTP error' do expect(last_response.status).to eq(400) end it 'returns a validation error' do expect(last_response.body).to eq('id is invalid, created_at is invalid') end end context 'when correct data is provided' do before do get 'test', id: 1, created_at: '2016-01-01' end it 'returns a successful response' do expect(last_response.status).to eq(200) end end end context 'with many levels of nested groups' do before do subject.params do requires :first_level, type: Hash do with(type: Integer) do requires :value with(type: String) do optional :second_level, type: Array do optional :name, type: String with(type: Integer) do optional :third_level, type: Array do requires :value, type: Integer optional :position end end end end requires :id end end end subject.put('/nested') { declared(params).to_json } end context 'when data is valid' do let(:request_params) do { first_level: { value: '10', second_level: [ { name: '13', third_level: [ { value: '2', position: '1' } ] } ], id: '20' } } end it 'validates and coerces correctly' do put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body, symbolize_names: true)).to eq( first_level: { value: 10, second_level: [ { name: '13', third_level: [{ value: 2, position: 1 }] } ], id: 20 } ) end end context 'when data is invalid' do let(:request_params) do { first_level: { value: 'wrong', second_level: [ { name: 'name', third_level: [{ position: 'wrong' }] } ] } } end it 'responds with HTTP error' do put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) end it 'responds with a validation error' do put '/nested', request_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body) .to include('first_level[value] is invalid') .and include('first_level[id] is missing') .and include('first_level[second_level][0][third_level][0][value] is missing') .and include('first_level[second_level][0][third_level][0][position] is invalid') end end end end context 'with exactly_one_of validation for optional parameters within an Hash param' do before do subject.params do optional :memo, type: Hash do optional :text, type: String optional :custom_body, type: Hash, coerce_with: JSON exactly_one_of :text, :custom_body end end subject.get('test') end context 'when correct data is provided' do it 'returns a successful response' do get 'test', memo: {} expect(last_response.status).to eq(200) get 'test', memo: { text: 'HOGEHOGE' } expect(last_response.status).to eq(200) get 'test', memo: { custom_body: '{ "xxx": "yyy" }' } expect(last_response.status).to eq(200) end end context 'when invalid data is provided' do it 'returns a failure response' do get 'test', memo: { text: 'HOGEHOGE', custom_body: '{ "xxx": "yyy" }' } expect(last_response.status).to eq(400) get 'test', memo: '{ "custom_body": "HOGE" }' expect(last_response.status).to eq(400) end end end end ================================================ FILE: spec/grape/validations/single_attribute_iterator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::SingleAttributeIterator do describe '#each' do subject(:iterator) { described_class.new(%i[first second], scope, params) } let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) } context 'when params is a hash' do let(:params) do { first: 'string', second: 'string' } end it 'yields params and every single attribute from the list' do expect { |b| iterator.each(&b) } .to yield_successive_args([params, :first, false], [params, :second, false]) end end context 'when params is an array' do let(:params) do [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }] end it 'yields every single attribute from the list for each of the array elements' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[0], :first, false], [params[0], :second, false], [params[1], :first, false], [params[1], :second, false] ) end context 'empty values' do let(:params) { [{}, '', 10] } it 'marks params with empty values' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[0], :first, true], [params[0], :second, true], [params[1], :first, true], [params[1], :second, true], [params[2], :first, false], [params[2], :second, false] ) end end context 'when missing optional value' do let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] } it 'does not yield skipped values' do expect { |b| iterator.each(&b) }.to yield_successive_args( [params[1], :first, false], [params[1], :second, false] ) end end end end end ================================================ FILE: spec/grape/validations/types/array_coercer_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Types::ArrayCoercer do subject { described_class.new(type) } describe '#call' do context 'an array of primitives' do let(:type) { Array[String] } it 'coerces elements in the array' do expect(subject.call([10, 20])).to eq(%w[10 20]) end end context 'an array of arrays' do let(:type) { Array[Array[Integer]] } it 'coerces elements in the nested array' do expect(subject.call([%w[10 20]])).to eq([[10, 20]]) expect(subject.call([['10'], ['20']])).to eq([[10], [20]]) end end context 'an array of sets' do let(:type) { Array[Set[Integer]] } it 'coerces elements in the nested set' do expect(subject.call([%w[10 20]])).to eq([Set[10, 20]]) expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]]) end end end end ================================================ FILE: spec/grape/validations/types/primitive_coercer_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Types::PrimitiveCoercer do subject { described_class.new(type, strict) } let(:strict) { false } describe '#call' do context 'BigDecimal' do let(:type) { BigDecimal } it 'coerces to BigDecimal' do expect(subject.call(5)).to eq(BigDecimal('5')) end it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Boolean' do let(:type) { Grape::API::Boolean } [true, 'true', 1].each do |val| it "coerces '#{val}' to true" do expect(subject.call(val)).to be(true) end end [false, 'false', 0].each do |val| it "coerces '#{val}' to false" do expect(subject.call(val)).to be(false) end end it 'returns an error when the given value cannot be coerced' do expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'DateTime' do let(:type) { DateTime } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Float' do let(:type) { Float } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'Integer' do let(:type) { Integer } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end it 'accepts non-nil value' do expect(subject.call(42)).to be_a(Integer) end end context 'Numeric' do let(:type) { Numeric } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end it 'accepts a non-nil value' do expect(subject.call(42)).to be_a(Numeric) # in fact Integer end end context 'Time' do let(:type) { Time } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'String' do let(:type) { String } it 'coerces to String' do expect(subject.call(10)).to eq('10') end it 'does not coerce an empty string to nil' do expect(subject.call('')).to eq('') end end context 'Symbol' do let(:type) { Symbol } it 'coerces an empty string to nil' do expect(subject.call('')).to be_nil end end context 'a type unknown in Dry-types' do let(:type) { Complex } it 'raises error on init' do expect(Grape::DryTypes::Params.constants).not_to include(type.name.to_sym) expect { subject }.to raise_error(/type Complex should support coercion/) end end context 'the strict mode' do let(:strict) { true } context 'Boolean' do let(:type) { Grape::API::Boolean } it 'returns an error when the given value is not Boolean' do expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'returns a value as it is when the given value is Boolean' do expect(subject.call(true)).to be(true) end end context 'BigDecimal' do let(:type) { BigDecimal } it 'returns an error when the given value is not BigDecimal' do expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue) end it 'returns a value as it is when the given value is BigDecimal' do expect(subject.call(BigDecimal('0'))).to eq(BigDecimal('0')) end end end end end ================================================ FILE: spec/grape/validations/types/set_coercer_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Types::SetCoercer do subject { described_class.new(type) } describe '#call' do context 'a set of primitives' do let(:type) { Set[String] } it 'coerces elements to the set' do expect(subject.call([10, 20])).to eq(Set['10', '20']) end end context 'a set of sets' do let(:type) { Set[Set[Integer]] } it 'coerces elements in the nested set' do expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]]) expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]]) end end context 'a set of sets of arrays' do let(:type) { Set[Set[Array[Integer]]] } it 'coerces elements in the nested set' do expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]]) end end end end ================================================ FILE: spec/grape/validations/types_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Types do let(:foo_type) do Class.new do def self.parse(_); end end end let(:bar_type) do Class.new do def self.parse; end end end describe '::primitive?' do [ Integer, Float, Numeric, BigDecimal, Grape::API::Boolean, String, Symbol, Date, DateTime, Time ].each do |type| it "recognizes #{type} as a primitive" do expect(described_class).to be_primitive(type) end end it 'identifies unknown types' do expect(described_class).not_to be_primitive(Object) expect(described_class).not_to be_primitive(foo_type) end end describe '::structure?' do [ Hash, Array, Set ].each do |type| it "recognizes #{type} as a structure" do expect(described_class).to be_structure(type) end end end describe '::special?' do [ JSON, Array[JSON], File, Rack::Multipart::UploadedFile ].each do |type| it "provides special handling for #{type.inspect}" do expect(described_class).to be_special(type) end end end describe 'special types' do subject { described_class::SPECIAL[type] } context 'when JSON' do let(:type) { JSON } it { is_expected.to eq(Grape::Validations::Types::Json) } end context 'when Array[JSON]' do let(:type) { Array[JSON] } it { is_expected.to eq(Grape::Validations::Types::JsonArray) } end context 'when File' do let(:type) { File } it { is_expected.to eq(Grape::Validations::Types::File) } end context 'when Rack::Multipart::UploadedFile' do let(:type) { Rack::Multipart::UploadedFile } it { is_expected.to eq(Grape::Validations::Types::File) } end end describe '::custom?' do it 'returns false if the type does not respond to :parse' do expect(described_class).not_to be_custom(Object) end it 'returns true if the type responds to :parse with one argument' do expect(described_class).to be_custom(foo_type) end it 'returns false if the type\'s #parse method takes other than one argument' do expect(described_class).not_to be_custom(bar_type) end end describe '::build_coercer' do it 'caches the result of the build_coercer method' do a_coercer = described_class.build_coercer(Array[String]) b_coercer = described_class.build_coercer(Array[String]) expect(a_coercer.object_id).to eq(b_coercer.object_id) end end end ================================================ FILE: spec/grape/validations/validators/all_or_none_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::AllOrNoneOfValidator do describe '#validate!' do subject(:validate) { post path, params } describe '/' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, type: Grape::API::Boolean all_or_none_of :beer, :wine end post do end end end context 'when all restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when a subset of restricted params are present' do let(:path) { '/' } let(:params) { { beer: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['provide all or none of parameters'] ) end end context 'when no restricted params are present' do let(:path) { '/' } let(:params) { { somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end describe '/mixed-params' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, :other, type: Grape::API::Boolean all_or_none_of :beer, :wine end post 'mixed-params' do end end end let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, type: Grape::API::Boolean all_or_none_of :beer, :wine, message: 'choose all or none' end post '/custom-message' do end end end let(:path) { '/custom-message' } let(:params) { { beer: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['choose all or none'] ) end end describe '/nested-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :item, type: Hash do optional :beer, :wine, type: Grape::API::Boolean all_or_none_of :beer, :wine end end post '/nested-hash' do end end end let(:path) { '/nested-hash' } let(:params) { { item: { beer: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['provide all or none of parameters'] ) end end describe '/nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do optional :beer, :wine, type: Grape::API::Boolean all_or_none_of :beer, :wine end end post '/nested-array' do end end end let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[1][beer],items[1][wine]' => ['provide all or none of parameters'] ) end end describe '/deeply-nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, type: Grape::API::Boolean all_or_none_of :beer, :wine end end end post '/deeply-nested-array' do end end end let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => [ 'provide all or none of parameters' ] ) end end end end ================================================ FILE: spec/grape/validations/validators/allow_blank_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::AllowBlankValidator do describe 'bad encoding' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, type: String, allow_blank: false end get '/bad_encoding' end end context 'when value has bad encoding' do it 'does not raise an error' do expect { get('/bad_encoding', { name: "Hello \x80" }) }.not_to raise_error end end end describe '/disallow_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, allow_blank: false end get '/disallow_blank' end end it 'refuses empty string' do get '/disallow_blank', name: '' expect(last_response.status).to eq(400) end it 'refuses only whitespaces' do get '/disallow_blank', name: ' ' expect(last_response.status).to eq(400) get '/disallow_blank', name: " \n " expect(last_response.status).to eq(400) get '/disallow_blank', name: "\n" expect(last_response.status).to eq(400) end it 'refuses nil' do get '/disallow_blank', name: nil expect(last_response.status).to eq(400) end it 'refuses missing' do get '/disallow_blank' expect(last_response.status).to eq(400) end it 'accepts valid input' do get '/disallow_blank', name: 'bob' expect(last_response.status).to eq(200) end end describe '/opt_disallow_string_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :name, type: String, allow_blank: false end get '/opt_disallow_string_blank' end end it 'allows missing optional strings' do get 'opt_disallow_string_blank' expect(last_response.status).to eq(200) end end describe '/allow_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, allow_blank: true end get '/allow_blank' end end it 'accepts empty input when allow_blank is true' do get '/allow_blank', name: '' expect(last_response.status).to eq(200) end end describe 'type-specific blanks' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: DateTime, allow_blank: true end get '/allow_datetime_blank' params do requires :val, type: DateTime, allow_blank: false end get '/disallow_datetime_blank' params do requires :val, type: DateTime end get '/default_allow_datetime_blank' params do requires :val, type: Date, allow_blank: true end get '/allow_date_blank' params do requires :val, type: Integer, allow_blank: true end get '/allow_integer_blank' params do requires :val, type: Float, allow_blank: true end get '/allow_float_blank' params do requires :val, type: Symbol, allow_blank: true end get '/allow_symbol_blank' params do requires :val, type: Grape::API::Boolean, allow_blank: true end get '/allow_boolean_blank' params do requires :val, type: Grape::API::Boolean, allow_blank: false end get '/disallow_boolean_blank' end end it 'refuses empty string for disallow_datetime_blank' do get '/disallow_datetime_blank', val: '' expect(last_response.status).to eq(400) end it 'accepts value when time allow_blank' do get '/disallow_datetime_blank', val: Time.now expect(last_response.status).to eq(200) end it 'accepts empty when datetime allow_blank' do get '/allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty input' do get '/default_allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when date allow_blank' do get '/allow_date_blank', val: '' expect(last_response.status).to eq(200) end context 'allow_blank when Numeric' do it 'accepts empty when integer allow_blank' do get '/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when float allow_blank' do get '/allow_float_blank', val: '' expect(last_response.status).to eq(200) end end it 'accepts empty when symbol allow_blank' do get '/allow_symbol_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts empty when boolean allow_blank' do get '/allow_boolean_blank', val: '' expect(last_response.status).to eq(200) end it 'accepts false when boolean allow_blank' do get '/disallow_boolean_blank', val: false expect(last_response.status).to eq(200) end end describe 'in an optional group' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_an_optional_group' params do optional :user, type: Hash do requires :name, type: Date, allow_blank: true end end get '/allow_blank_date_param_in_an_optional_group' params do optional :user, type: Hash do optional :name, allow_blank: false requires :age end end get '/disallow_blank_optional_param_in_an_optional_group' end end context 'as a required param' do it 'accepts a missing group, even with a disallwed blank param' do get '/disallow_blank_required_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing date value' do get '/allow_blank_date_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(200) end it 'refuses a blank value in an existing group' do get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(400) end end context 'as an optional param' do it 'accepts a missing group, even with a disallwed blank param' do get '/disallow_blank_optional_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing optional value' do get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) end end end describe 'in a required group' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_blank_required_param_in_a_required_group' params do requires :user, type: Hash do requires :name, allow_blank: false end end get '/disallow_string_value_in_a_required_hash_group' params do requires :user, type: Hash do optional :name, allow_blank: false end end get '/disallow_blank_optional_param_in_a_required_group' params do optional :user, type: Hash do optional :name, allow_blank: false end end get '/disallow_string_value_in_an_optional_hash_group' end end context 'as a required param' do it 'refuses a blank value in a required existing group' do get '/disallow_blank_required_param_in_a_required_group', user: { name: '' } expect(last_response.status).to eq(400) end it 'refuses a string value in a required hash group' do get '/disallow_string_value_in_a_required_hash_group', user: '' expect(last_response.status).to eq(400) end end context 'as an optional param' do it 'accepts a nested missing value' do get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) end it 'refuses a string value in an optional hash group' do get '/disallow_string_value_in_an_optional_hash_group', user: '' expect(last_response.status).to eq(400) end end end describe 'custom message' do context 'GET /custom_message' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, allow_blank: { value: false, message: 'has no value' } end get '/custom_message' end end it 'refuses empty string' do get '/custom_message', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end it 'refuses only whitespaces' do get '/custom_message', name: ' ' expect(last_response.body).to eq('{"error":"name has no value"}') get '/custom_message', name: " \n " expect(last_response.body).to eq('{"error":"name has no value"}') get '/custom_message', name: "\n" expect(last_response.body).to eq('{"error":"name has no value"}') end it 'refuses nil' do get '/custom_message', name: nil expect(last_response.body).to eq('{"error":"name has no value"}') end it 'accepts valid input' do get '/custom_message', name: 'bob' expect(last_response.status).to eq(200) end end context 'GET /custom_message/disallow_blank_optional_param' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :name, allow_blank: { value: false, message: 'has no value' } end get '/custom_message/disallow_blank_optional_param' end end it 'refuses empty string for an optional param' do get '/custom_message/disallow_blank_optional_param', name: '' expect(last_response.body).to eq('{"error":"name has no value"}') end end context 'GET /custom_message/allow_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, allow_blank: true end get '/custom_message/allow_blank' end end it 'accepts empty input when allow_blank is true' do get '/custom_message/allow_blank', name: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/allow_datetime_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: DateTime, allow_blank: true end get '/custom_message/allow_datetime_blank' end end it 'accepts empty when datetime allow_blank' do get '/custom_message/allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/default_allow_datetime_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: DateTime end get '/custom_message/default_allow_datetime_blank' end end it 'accepts empty input' do get '/custom_message/default_allow_datetime_blank', val: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/allow_date_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Date, allow_blank: true end get '/custom_message/allow_date_blank' end end it 'accepts empty when date allow_blank' do get '/custom_message/allow_date_blank', val: '' expect(last_response.status).to eq(200) end end context 'allow_blank when Numeric' do context 'GET /custom_message/allow_integer_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Integer, allow_blank: true end get '/custom_message/allow_integer_blank' end end it 'accepts empty when integer allow_blank' do get '/custom_message/allow_integer_blank', val: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/allow_float_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Float, allow_blank: true end get '/custom_message/allow_float_blank' end end it 'accepts empty when float allow_blank' do get '/custom_message/allow_float_blank', val: '' expect(last_response.status).to eq(200) end end end context 'GET /custom_message/allow_symbol_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Symbol, allow_blank: true end get '/custom_message/allow_symbol_blank' end end it 'accepts empty when symbol allow_blank' do get '/custom_message/allow_symbol_blank', val: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/allow_boolean_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Grape::API::Boolean, allow_blank: true end get '/custom_message/allow_boolean_blank' end end it 'accepts empty when boolean allow_blank' do get '/custom_message/allow_boolean_blank', val: '' expect(last_response.status).to eq(200) end end context 'GET /custom_message/disallow_boolean_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' } end get '/custom_message/disallow_boolean_blank' end end it 'accepts false when boolean disallow_blank' do get '/custom_message/disallow_boolean_blank', val: false expect(last_response.status).to eq(200) end end context 'in an optional group' do context 'GET /custom_message/disallow_blank_required_param_in_an_optional_group' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/custom_message/disallow_blank_required_param_in_an_optional_group' end end it 'accepts a missing group, even with a disallwed blank param' do get '/custom_message/disallow_blank_required_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'refuses a blank value in an existing group' do get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end context 'GET /custom_message/allow_blank_date_param_in_an_optional_group' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :user, type: Hash do requires :name, type: Date, allow_blank: true end end get '/custom_message/allow_blank_date_param_in_an_optional_group' end end it 'accepts a nested missing date value' do get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' } expect(last_response.status).to eq(200) end end context 'GET /custom_message/disallow_blank_optional_param_in_an_optional_group' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } requires :age end end get '/custom_message/disallow_blank_optional_param_in_an_optional_group' end end it 'accepts a missing group, even with a disallwed blank param' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group' expect(last_response.status).to eq(200) end it 'accepts a nested missing optional value' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end end context 'in a required group' do context 'GET /custom_message/disallow_blank_required_param_in_a_required_group' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/custom_message/disallow_blank_required_param_in_a_required_group' end end it 'refuses a blank value in a required existing group' do get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end context 'GET /custom_message/disallow_string_value_in_a_required_hash_group' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :user, type: Hash do requires :name, allow_blank: { value: false, message: 'has no value' } end end get '/custom_message/disallow_string_value_in_a_required_hash_group' end end it 'refuses a string value in a required hash group' do get '/custom_message/disallow_string_value_in_a_required_hash_group', user: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}') end end context 'GET /custom_message/disallow_blank_optional_param_in_a_required_group' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } end end get '/custom_message/disallow_blank_optional_param_in_a_required_group' end end it 'accepts a nested missing value' do get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' } expect(last_response.status).to eq(200) end it 'refuses a blank existing value in an existing scope' do get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[name] has no value"}') end end context 'GET /custom_message/disallow_string_value_in_an_optional_hash_group' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :user, type: Hash do optional :name, allow_blank: { value: false, message: 'has no value' } end end get '/custom_message/disallow_string_value_in_an_optional_hash_group' end end it 'refuses a string value in an optional hash group' do get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is invalid"}') end end end end end ================================================ FILE: spec/grape/validations/validators/at_least_one_of_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::AtLeastOneOfValidator do describe '#validate!' do subject(:validate) { post path, params } describe '/' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit end post do end end end context 'when all restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when a subset of restricted params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of the restricted params is selected' do let(:path) { '/' } let(:params) { { other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided'] ) end end context 'when exactly one of the restricted params is selected' do let(:path) { '/' } let(:params) { { beer: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end describe '/mixed-params' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, :grapefruit, :other at_least_one_of :beer, :wine, :grapefruit end post 'mixed-params' do end end end let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something' end post '/custom-message' do end end end let(:path) { '/custom-message' } let(:params) { { other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['you should choose something'] ) end end describe '/nested-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :item, type: Hash do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end post '/nested-hash' do end end end let(:path) { '/nested-hash' } context 'when at least one of them is present' do let(:params) { { item: { beer: true, wine: true } } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { item: { other: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine],item[grapefruit]' => ['fail'] ) end end end describe '/nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end post '/nested-array' do end end end let(:path) { '/nested-array' } context 'when at least one of them is present' do let(:params) { { items: [{ beer: true, wine: true }, { grapefruit: true }] } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { items: [{ beer: true, other: true }, { other: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[1][beer],items[1][wine],items[1][grapefruit]' => ['fail'] ) end end end describe '/deeply-nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit at_least_one_of :beer, :wine, :grapefruit, message: 'fail' end end end post '/deeply-nested-array' do end end end let(:path) { '/deeply-nested-array' } context 'when at least one of them is present' do let(:params) { { items: [{ nested_items: [{ wine: true }] }] } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of them are present' do let(:params) { { items: [{ nested_items: [{ other: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => ['fail'] ) end end end end end ================================================ FILE: spec/grape/validations/validators/base_i18n_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::Base do describe 'i18n' do subject { Class.new(Grape::API) } let(:app) { subject } let(:custom_i18n_validator) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params.respond_to?(:key?) && params[attr_name] == 'accept' raise Grape::Exceptions::Validation.new( params: @scope.full_name(attr_name), message: message(:custom_i18n_test) ) end end end around do |example| I18n.available_locales = %i[en zh-CN] I18n.backend.store_translations(:en, grape: { errors: { messages: { custom_i18n_test: 'custom validation failed (en)' } } }) I18n.backend.store_translations(:'zh-CN', grape: { errors: { format: '%s %s', messages: { custom_i18n_test: '自定义校验失败 (zh-CN)' } } }) example.run ensure I18n.available_locales = %i[en] I18n.reload! end before do stub_const('CustomI18nValidator', custom_i18n_validator) Grape::Validations.register(CustomI18nValidator) end after { Grape::Validations.deregister('custom_i18n') } it 'uses the request-time locale regardless of the locale active at definition time' do # Define the API while zh-CN is the active locale I18n.with_locale(:'zh-CN') do subject.params do requires :token, custom_i18n: true end subject.post do end end # Switch to English before making the request I18n.with_locale(:en) do post '/', token: 'reject' end expect(last_response.status).to eq(400) expect(last_response.body).to eq('token custom validation failed (en)') end it 'uses zh-CN message when request is made with zh-CN locale' do I18n.with_locale(:en) do subject.params do requires :token, custom_i18n: true end subject.post do end end I18n.with_locale(:'zh-CN') do post '/', token: 'reject' end expect(last_response.status).to eq(400) expect(last_response.body).to eq('token 自定义校验失败 (zh-CN)') end end end ================================================ FILE: spec/grape/validations/validators/coerce_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::CoerceValidator do subject { Class.new(Grape::API) } let(:app) { subject } describe 'coerce' do let(:secure_uri_only) do Class.new do def self.parse(value) URI.parse(value) end def self.parsed?(value) value.is_a? URI::HTTPS end end end context 'i18n' do before do I18n.available_locales = %i[en zh-CN] zh_cn = { grape: { errors: { format: '%s%s', attributes: { age: '年龄' }, messages: { coerce: '格式不正确' } } } } I18n.backend.store_translations(:'zh-CN', zh_cn) end after do I18n.available_locales = %i[en] I18n.reload! end it 'i18n error on malformed input' do subject.params do requires :age, type: Integer end subject.get '/single' do 'int works' end I18n.with_locale(:'zh-CN') { get '/single', age: '43a' } expect(last_response).to be_bad_request expect(last_response.body).to eq('年龄格式不正确') end context 'when the locale has no translation for the message' do before { I18n.available_locales = %i[en pt-BR] } it 'gives an english fallback error' do subject.params do requires :age, type: Integer end subject.get '/single' do 'int works' end I18n.with_locale(:'pt-BR') { get '/single', age: '43a' } expect(last_response).to be_bad_request expect(last_response.body).to eq('age is invalid') end end end context 'with a custom validation message' do it 'errors on malformed input' do subject.params do requires :int, type: { value: Integer, message: 'type cast is invalid' } end subject.get '/single' do 'int works' end get '/single', int: '43a' expect(last_response).to be_bad_request expect(last_response.body).to eq('int type cast is invalid') get '/single', int: '43' expect(last_response).to be_successful expect(last_response.body).to eq('int works') end context 'on custom coercion rules' do before do subject.params do requires :a, types: { value: [Grape::API::Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val| case val when 'yup' true when 'false' 0 else val end end) end subject.get '/' do params[:a].class.to_s end end it 'respects :coerce_with' do get '/', a: 'yup' expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' expect(last_response).to be_bad_request expect(last_response.body).to eq('a type cast is invalid') end it 'performs no additional coercion' do get '/', a: 'true' expect(last_response).to be_successful expect(last_response.body).to eq('String') end end end it 'error on malformed input' do subject.params do requires :int, type: Integer end subject.get '/single' do 'int works' end get '/single', int: '43a' expect(last_response).to be_bad_request expect(last_response.body).to eq('int is invalid') get '/single', int: '43' expect(last_response).to be_successful expect(last_response.body).to eq('int works') end it 'error on malformed input (Array)' do subject.params do requires :ids, type: Array[Integer] end subject.get '/array' do 'array int works' end get 'array', ids: %w[1 2 az] expect(last_response).to be_bad_request expect(last_response.body).to eq('ids is invalid') get 'array', ids: %w[1 2 890] expect(last_response).to be_successful expect(last_response.body).to eq('array int works') end context 'coerces' do context 'json' do let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } } it 'BigDecimal' do subject.params do requires :bigdecimal, type: BigDecimal end subject.post '/bigdecimal' do "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}" end post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers expect(last_response).to be_created expect(last_response.body).to eq('BigDecimal 45.1') end it 'Grape::API::Boolean' do subject.params do requires :boolean, type: Grape::API::Boolean end subject.post '/boolean' do params[:boolean] end post '/boolean', { boolean: 'true' }.to_json, headers expect(last_response).to be_created expect(last_response.body).to eq('true') end end it 'BigDecimal' do subject.params do requires :bigdecimal, coerce: BigDecimal end subject.get '/bigdecimal' do params[:bigdecimal].class end get '/bigdecimal', bigdecimal: '45' expect(last_response).to be_successful expect(last_response.body).to eq('BigDecimal') end it 'Integer' do subject.params do requires :int, coerce: Integer end subject.get '/int' do params[:int].class end get '/int', int: '45' expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end it 'String' do subject.params do requires :string, coerce: String end subject.get '/string' do params[:string].class end get '/string', string: 45 expect(last_response).to be_successful expect(last_response.body).to eq('String') get '/string', string: nil expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end context 'a custom type' do it 'coerces the given value' do context = self subject.params do requires :uri, coerce: context.secure_uri_only end subject.get '/secure_uri' do params[:uri].class end get 'secure_uri', uri: 'https://www.example.com' expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTPS') get 'secure_uri', uri: 'http://www.example.com' expect(last_response).to be_bad_request expect(last_response.body).to eq('uri is invalid') end context 'returning the InvalidValue instance when invalid' do let(:custom_type) do Class.new do def self.parse(_val) Grape::Validations::Types::InvalidValue.new('must be unique') end end end it 'uses a custom message added to the invalid value' do type = custom_type subject.params do requires :name, type: type end subject.get '/whatever' do params[:name].class end get 'whatever', name: 'Bob' expect(last_response).to be_bad_request expect(last_response.body).to eq('name must be unique') end end end context 'Array' do it 'Array of Integers' do subject.params do requires :arry, coerce: Array[Integer] end subject.get '/array' do params[:arry][0].class end get '/array', arry: %w[1 2 3] expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end it 'Array of Bools' do subject.params do requires :arry, coerce: Array[Grape::API::Boolean] end subject.get '/array' do params[:arry][0].class end get 'array', arry: [1, 0] expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end it 'Array of type implementing parse' do subject.params do requires :uri, type: Array[URI] end subject.get '/uri_array' do params[:uri][0].class end get 'uri_array', uri: ['http://www.google.com'] expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTP') end it 'Set of type implementing parse' do subject.params do requires :uri, type: Set[URI] end subject.get '/uri_array' do "#{params[:uri].class},#{params[:uri].first.class},#{params[:uri].size}" end get 'uri_array', uri: Array.new(2) { 'http://www.example.com' } expect(last_response).to be_successful expect(last_response.body).to eq('Set,URI::HTTP,1') end it 'Array of a custom type' do context = self subject.params do requires :uri, type: Array[context.secure_uri_only] end subject.get '/secure_uris' do params[:uri].first.class end get 'secure_uris', uri: ['https://www.example.com'] expect(last_response).to be_successful expect(last_response.body).to eq('URI::HTTPS') get 'secure_uris', uri: ['https://www.example.com', 'http://www.example.com'] expect(last_response).to be_bad_request expect(last_response.body).to eq('uri is invalid') end end context 'Set' do it 'Set of Integers' do subject.params do requires :set, coerce: Set[Integer] end subject.get '/set' do params[:set].first.class end get '/set', set: Set.new([1, 2, 3, 4]).to_a expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end it 'Set of Bools' do subject.params do requires :set, coerce: Set[Grape::API::Boolean] end subject.get '/set' do params[:set].first.class end get '/set', set: Set.new([1, 0]).to_a expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end end it 'Grape::API::Boolean' do subject.params do requires :boolean, type: Grape::API::Boolean end subject.get '/boolean' do params[:boolean].class end get '/boolean', boolean: 1 expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end context 'File' do let(:file) { Rack::Test::UploadedFile.new(__FILE__) } let(:filename) { File.basename(__FILE__).to_s } it 'Rack::Multipart::UploadedFile' do subject.params do requires :file, type: Rack::Multipart::UploadedFile end subject.post '/upload' do params[:file][:filename] end post '/upload', file: file expect(last_response).to be_created expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') end it 'File' do subject.params do requires :file, coerce: File end subject.post '/upload' do params[:file][:filename] end post '/upload', file: file expect(last_response).to be_created expect(last_response.body).to eq(filename) post '/upload', file: 'not a file' expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' } expect(last_response).to be_bad_request expect(last_response.body).to eq('file is invalid') end it 'collection' do subject.params do requires :files, type: Array[File] end subject.post '/upload' do params[:files].first[:filename] end post '/upload', files: [file] expect(last_response).to be_created expect(last_response.body).to eq(filename) end end it 'Nests integers' do subject.params do requires :integers, type: Hash do requires :int, coerce: Integer end end subject.get '/int' do params[:integers][:int].class end get '/int', integers: { int: '45' } expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) end context 'nil values' do context 'primitive types' do Grape::Validations::Types::PRIMITIVES.each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end end context 'structures types' do Grape::Validations::Types::STRUCTURES.each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end end context 'special types' do Grape::Validations::Types::SPECIAL.each_key do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end context 'variant-member-type collections' do [ Array[Integer, String], [Integer, String, Array[Integer, String]] ].each do |type| it 'respects the nil value' do subject.params do requires :param, type: type end subject.get '/nil_value' do params[:param].class end get '/nil_value', param: nil expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end end end end context 'empty string' do context 'primitive types' do (Grape::Validations::Types::PRIMITIVES - [String]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end it 'is not coerced to nil for type String' do subject.params do requires :param, type: String end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response).to be_successful expect(last_response.body).to eq('String') end end context 'structures types' do (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end end context 'special types' do (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end context 'variant-member-type collections' do [ Array[Integer, String], [Integer, String, Array[Integer, String]] ].each do |type| it "is coerced to nil for type #{type}" do subject.params do requires :param, type: type end subject.get '/empty_string' do params[:param].class end get '/empty_string', param: '' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end end end end end end context 'using coerce_with' do it 'parses parameters with Array type' do subject.params do requires :values, type: Array, coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end subject.get '/ints' do params[:values] end get '/ints', values: '1 2 3 4' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end it 'parses parameters with Array[String] type' do subject.params do requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) } end subject.get '/strings' do params[:values] end get '/strings', values: '1 2 3 4' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4]) get '/strings', values: 'a b c d' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq(%w[a b c d]) end it 'parses parameters with Array[Array[String]] type and coerce_with' do subject.params do requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(',').map(&:strip)] : val } end subject.post '/coerce_nested_strings' do params[:values] end post '/coerce_nested_strings', Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a b c d]]) post '/coerce_nested_strings', Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]]) post '/coerce_nested_strings', Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_created expect(JSON.parse(last_response.body)).to eq([[]]) post '/coerce_nested_strings', Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json' expect(last_response).to be_bad_request end it 'parses parameters with Array[Integer] type' do subject.params do requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } end subject.get '/ints' do params[:values] end get '/ints', values: '1 2 3 4' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 2, 3, 4]) get '/ints', values: 'a b c d' expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([0, 0, 0, 0]) end it 'parses parameters even if type is valid' do subject.params do requires :values, type: Array, coerce_with: ->(array) { array.map { |val| val.to_i + 1 } } end subject.get '/ints' do params[:values] end get '/ints', values: [1, 2, 3, 4] expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([2, 3, 4, 5]) get '/ints', values: %w[a b c d] expect(last_response).to be_successful expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1]) end context 'Array type and coerce_with should' do before do subject.params do optional :arr, type: Array, coerce_with: (lambda do |val| if val.nil? [] else val end end) end subject.get '/' do params[:arr].class.to_s end end it 'coerce nil value to array' do get '/', arr: nil expect(last_response).to be_successful expect(last_response.body).to eq('Array') end it 'not coerce missing field' do get '/' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end it 'coerce array as array' do get '/', arr: [] expect(last_response).to be_successful expect(last_response.body).to eq('Array') end end it 'uses parse where available' do subject.params do requires :ints, type: Array, coerce_with: JSON do requires :i, type: Integer requires :j end end subject.get '/ints' do ints = params[:ints].first 'coercion works' if ints[:i] == 1 && ints[:j] == '2' end get '/ints', ints: [{ i: 1, j: '2' }] expect(last_response).to be_bad_request expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '{"i":1,"j":"2"}' expect(last_response).to be_bad_request expect(last_response.body).to eq('ints is invalid') get '/ints', ints: '[{"i":"1","j":"2"}]' expect(last_response).to be_successful expect(last_response.body).to eq('coercion works') end it 'accepts any callable' do subject.params do requires :ints, type: Hash, coerce_with: JSON.method(:parse) do requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val } end end subject.get '/ints' do params[:ints][:int] end get '/ints', ints: '{"int":"3"}' expect(last_response).to be_bad_request expect(last_response.body).to eq('ints[int] is invalid') get '/ints', ints: '{"int":"three"}' expect(last_response).to be_successful expect(last_response.body).to eq('3') get '/ints', ints: '{"int":3}' expect(last_response).to be_successful expect(last_response.body).to eq('3') end context 'Integer type and coerce_with should' do before do subject.params do optional :int, type: Integer, coerce_with: (lambda do |val| if val.nil? 0 else val.to_i end end) end subject.get '/' do params[:int].class.to_s end end it 'coerce nil value to integer' do get '/', int: nil expect(last_response).to be_successful expect(last_response.body).to eq('Integer') end it 'not coerce missing field' do get '/' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end it 'coerce integer as integer' do get '/', int: 1 expect(last_response).to be_successful expect(last_response.body).to eq('Integer') end end context 'Integer type and coerce_with potentially returning nil' do before do subject.params do requires :int, type: Integer, coerce_with: (lambda do |val| case val when '0' nil when /^-?\d+$/ val.to_i else val end end) end subject.get '/' do params[:int].class.to_s end end it 'accepts value that coerces to nil' do get '/', int: '0' expect(last_response).to be_successful expect(last_response.body).to eq('NilClass') end it 'coerces to Integer' do get '/', int: '1' expect(last_response).to be_successful expect(last_response.body).to eq('Integer') end it 'returns invalid value if coercion returns a wrong type' do get '/', int: 'lol' expect(last_response).to be_bad_request expect(last_response.body).to eq('int is invalid') end end it 'must be supplied with :type or :coerce' do expect do subject.params do requires :ints, coerce_with: JSON end end.to raise_error(ArgumentError) end end context 'first-class JSON' do it 'parses objects, hashes, and arrays' do subject.params do requires :splines, type: JSON do requires :x, type: Integer, values: [1, 2, 3] optional :ints, type: Array[Integer] optional :obj, type: Hash do optional :y end end end subject.get '/' do if params[:splines].is_a? Hash params[:splines][:obj][:y] elsif params[:splines].any? { |s| s.key? :obj } 'arrays work' end end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: { x: 1, ints: [1, 2, 3], obj: { y: 'woof' } } expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }] expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: { x: 4, ints: [2] } expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }] expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end it 'works when declared optional' do subject.params do optional :splines, type: JSON do requires :x, type: Integer, values: [1, 2, 3] optional :ints, type: Array[Integer] optional :obj, type: Hash do optional :y end end end subject.get '/' do if params[:splines].is_a? Hash params[:splines][:obj][:y] elsif params[:splines].any? { |s| s.key? :obj } 'arrays work' end end get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}' expect(last_response).to be_successful expect(last_response.body).to eq('woof') get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]' expect(last_response).to be_successful expect(last_response.body).to eq('arrays work') get '/', splines: '{"x":4,"ints":[2]}' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end it 'accepts Array[JSON] shorthand' do subject.params do requires :splines, type: Array[JSON] do requires :x, type: Integer, values: [1, 2, 3] requires :y end end subject.get '/' do params[:splines].first[:y].class.to_s spline = params[:splines].first "#{spline[:x].class}.#{spline[:y].class}" end get '/', splines: '{"x":"1","y":"woof"}' expect(last_response).to be_successful expect(last_response.body).to eq("#{integer_class_name}.String") get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]' expect(last_response).to be_successful expect(last_response.body).to eq("#{integer_class_name}.#{integer_class_name}") get '/', splines: '{"x":"4","y":"woof"}' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') get '/', splines: '[{"x":"4","y":"woof"}]' expect(last_response).to be_bad_request expect(last_response.body).to eq('splines[x] does not have a valid value') end it "doesn't make sense using coerce_with" do expect do subject.params do requires :bad, type: JSON, coerce_with: JSON do requires :x end end end.to raise_error(ArgumentError) expect do subject.params do requires :bad, type: Array[JSON], coerce_with: JSON do requires :x end end end.to raise_error(ArgumentError) end end context 'multiple types' do it 'coerces to first possible type' do subject.params do requires :a, types: [Grape::API::Boolean, Integer, String] end subject.get '/' do params[:a].class.to_s end get '/', a: 'true' expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') get '/', a: '5' expect(last_response).to be_successful expect(last_response.body).to eq(integer_class_name) get '/', a: 'anything else' expect(last_response).to be_successful expect(last_response.body).to eq('String') end it 'fails when no coercion is possible' do subject.params do requires :a, types: [Grape::API::Boolean, Integer] end subject.get '/' do params[:a].class.to_s end get '/', a: true expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') get '/', a: 'not good' expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') end context 'for primitive collections' do before do subject.params do optional :a, types: [String, Array[String]] optional :b, types: [Array[Integer], Array[String]] optional :c, type: Array[Integer, String] optional :d, types: [Integer, String, Set[Integer, String]] end subject.get '/' do ( params[:a] || params[:b] || params[:c] || params[:d] ).inspect end end it 'allows singular form declaration' do get '/', a: 'one way' expect(last_response).to be_successful expect(last_response.body).to eq('"one way"') get '/', a: %w[the other] expect(last_response).to be_successful expect(last_response.body).to eq('["the", "other"]') get '/', a: { a: 1, b: 2 } expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') get '/', a: [1, 2, 3] expect(last_response).to be_successful expect(last_response.body).to eq('["1", "2", "3"]') end it 'allows multiple collection types' do get '/', b: [1, 2, 3] expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, 3]') get '/', b: %w[1 2 3] expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, 3]') get '/', b: [1, true, 'three'] expect(last_response).to be_successful expect(last_response.body).to eq('["1", "true", "three"]') end it 'allows collections with multiple types' do get '/', c: [1, '2', true, 'three'] expect(last_response).to be_successful expect(last_response.body).to eq('[1, 2, "true", "three"]') get '/', d: '1' expect(last_response).to be_successful expect(last_response.body).to eq('1') get '/', d: 'one' expect(last_response).to be_successful expect(last_response.body).to eq('"one"') get '/', d: %w[1 two] expect(last_response).to be_successful expect(last_response.body).to eq([1, 'two'].to_set.to_s) end end context 'custom coercion rules' do before do subject.params do requires :a, types: [Grape::API::Boolean, String], coerce_with: (lambda do |val| case val when 'yup' true when 'false' 0 else val end end) end subject.get '/' do params[:a].class.to_s end end it 'respects :coerce_with' do get '/', a: 'yup' expect(last_response).to be_successful expect(last_response.body).to eq('TrueClass') end it 'still validates type' do get '/', a: 'false' expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') end it 'performs no additional coercion' do get '/', a: 'true' expect(last_response).to be_successful expect(last_response.body).to eq('String') end end it 'may not be supplied together with a single type' do expect do subject.params do requires :a, type: Integer, types: [Integer, String] end end.to raise_exception ArgumentError end end context 'converter' do it 'does not build a coercer multiple times' do subject.params do requires :something, type: Array[String] end subject.get do end expect(Grape::Validations::Types::ArrayCoercer).to( receive(:new).at_most(:once).and_call_original ) 10.times { get '/' } end end end end ================================================ FILE: spec/grape/validations/validators/contract_scope_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::ContractScopeValidator do describe '.inherits' do subject { described_class } it { is_expected.to be < Grape::Validations::Validators::Base } end end ================================================ FILE: spec/grape/validations/validators/default_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::DefaultValidator do describe '/' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :id optional :type, default: 'default-type' end get '/' do { id: params[:id], type: params[:type] } end end end it 'set default value for optional param' do get('/') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ id: nil, type: 'default-type' }.to_json) end end describe '/user' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type1, default: 'default-type1' optional :type2, default: 'default-type2' end get '/user' do { type1: params[:type1], type2: params[:type2] } end end end it 'set default values for optional params' do get('/user') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ type1: 'default-type1', type2: 'default-type2' }.to_json) end it 'set default values for missing params in the request' do get('/user?type2=value2') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ type1: 'default-type1', type2: 'value2' }.to_json) end end describe '/message' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :id optional :type1, default: 'default-type1' optional :type2, default: 'default-type2' end get '/message' do { id: params[:id], type1: params[:type1], type2: params[:type2] } end end end it 'set default values for optional params and allow to use required fields in the same time' do get('/message?id=1') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json) end end describe '/numbers' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :random, default: -> { Random.rand } optional :not_random, default: Random.rand end get '/numbers' do { random_number: params[:random], non_random_number: params[:non_random_number] } end end end it 'sets lambda based defaults at the time of call' do get('/numbers') expect(last_response.status).to eq(200) before = JSON.parse(last_response.body) get('/numbers') expect(last_response.status).to eq(200) after = JSON.parse(last_response.body) expect(before['non_random_number']).to eq(after['non_random_number']) expect(before['random_number']).not_to eq(after['random_number']) end end describe '/array' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :array, type: Array do requires :name optional :with_default, default: 'default' end end get '/array' do { array: params[:array] } end end end it 'sets default values for grouped arrays' do get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2') expect(last_response.status).to eq(200) expect(last_response.body).to eq({ array: [{ name: 'name', with_default: 'default' }, { name: 'name2', with_default: 'bar2' }] }.to_json) end end describe '/optional_array' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :thing1 optional :more_things, type: Array do requires :nested_thing requires :other_thing, default: 1 end end get '/optional_array' do { thing1: params[:thing1] } end end end it 'lets you leave required values nested inside an optional blank' do get '/optional_array', thing1: 'stuff' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ thing1: 'stuff' }.to_json) end end describe '/nested_optional_array' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :root, type: Hash do optional :some_things, type: Array do requires :foo optional :options, type: Array do requires :name, type: String requires :value, type: String end end end end get '/nested_optional_array' do { root: params[:root] } end end end it 'allows optional arrays to be omitted' do params = { some_things: [{ foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, { foo: 'two' }, { foo: 'three', options: [{ name: 'wooop', value: 'yap' }] }] } get '/nested_optional_array', root: params expect(last_response.status).to eq(200) expect(last_response.body).to eq({ root: params }.to_json) end it 'does not allows faulty optional arrays' do params = { some_things: [ { foo: 'one', options: [{ name: 'wat', value: 'nope' }] }, { foo: 'two', options: [{ name: 'wat' }] }, { foo: 'three' } ] } error = { error: 'root[some_things][1][options][0][value] is missing' } get '/nested_optional_array', root: params expect(last_response.status).to eq(400) expect(last_response.body).to eq(error.to_json) end end describe '/another_nested_optional_array' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :root, type: Hash do optional :some_things, type: Array do requires :foo optional :options, type: Array do optional :name, type: String optional :value, type: String end end end end get '/another_nested_optional_array' do { root: params[:root] } end end end it 'allows optional arrays with optional params' do params = { some_things: [ { foo: 'one', options: [{ value: 'nope' }] }, { foo: 'two', options: [{ name: 'wat' }] }, { foo: 'three' } ] } get '/another_nested_optional_array', root: params expect(last_response.status).to eq(200) expect(last_response.body).to eq({ root: params }.to_json) end end describe '/default_values_from_other_params' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :foo optional :bar, default: ->(params) { params[:foo] } optional :qux, default: ->(params) { params[:bar] } end get '/default_values_from_other_params' do { foo: params[:foo], bar: params[:bar], qux: params[:qux] } end end end it 'sets default value for optional params using other params values' do expected_foo_value = 'foo-value' get("/default_values_from_other_params?foo=#{expected_foo_value}") expect(last_response.status).to eq(200) expect(last_response.body).to eq({ foo: expected_foo_value, bar: expected_foo_value, qux: expected_foo_value }.to_json) end end context 'optional group with defaults' do subject do Class.new(Grape::API) do default_format :json end end def app subject end context 'optional array without default value includes optional param with default value' do before do subject.params do optional :optional_array, type: Array do optional :foo_in_optional_array, default: 'bar' end end subject.post '/optional_array' do { optional_array: params[:optional_array] } end end it 'returns nil for optional array if param is not provided' do post '/optional_array' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_array: nil }.to_json) end end context 'optional array with default value includes optional param with default value' do before do subject.params do optional :optional_array_with_default, type: Array, default: [] do optional :foo_in_optional_array, default: 'bar' end end subject.post '/optional_array_with_default' do { optional_array_with_default: params[:optional_array_with_default] } end end it 'sets default value for optional array if param is not provided' do post '/optional_array_with_default' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_array_with_default: [] }.to_json) end end context 'optional hash without default value includes optional param with default value' do before do subject.params do optional :optional_hash_without_default, type: Hash do optional :foo_in_optional_hash, default: 'bar' end end subject.post '/optional_hash_without_default' do { optional_hash_without_default: params[:optional_hash_without_default] } end end it 'returns nil for optional hash if param is not provided' do post '/optional_hash_without_default' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_hash_without_default: nil }.to_json) end it 'does not fail even if invalid params is passed to default validator' do expect { post '/optional_hash_without_default', optional_hash_without_default: '5678' }.not_to raise_error expect(last_response.status).to eq(400) expect(last_response.body).to eq({ error: 'optional_hash_without_default is invalid' }.to_json) end end context 'optional hash with default value includes optional param with default value' do before do subject.params do optional :optional_hash_with_default, type: Hash, default: {} do optional :foo_in_optional_hash, default: 'bar' end end subject.post '/optional_hash_with_default_empty_hash' do { optional_hash_with_default: params[:optional_hash_with_default] } end subject.params do optional :optional_hash_with_default, type: Hash, default: { foo_in_optional_hash: 'parent_default' } do optional :some_param optional :foo_in_optional_hash, default: 'own_default' end end subject.post '/optional_hash_with_default_inner_params' do { foo_in_optional_hash: params[:optional_hash_with_default][:foo_in_optional_hash] } end end it 'sets default value for optional hash if param is not provided' do post '/optional_hash_with_default_empty_hash' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ optional_hash_with_default: {} }.to_json) end it 'sets default value from parent defaults for inner param if parent param is not provided' do post '/optional_hash_with_default_inner_params' expect(last_response.status).to eq(201) expect(last_response.body).to eq({ foo_in_optional_hash: 'parent_default' }.to_json) end it 'sets own default value for inner param if parent param is provided' do post '/optional_hash_with_default_inner_params', optional_hash_with_default: { some_param: 'param' } expect(last_response.status).to eq(201) expect(last_response.body).to eq({ foo_in_optional_hash: 'own_default' }.to_json) end end end context 'optional with nil as value' do subject do Class.new(Grape::API) do default_format :json end end def app subject end context 'primitive types' do [ [Integer, 0], [Integer, 42], [Float, 0.0], [Float, 4.2], [BigDecimal, 0.0], [BigDecimal, 4.2], [Numeric, 0], [Numeric, 42], [Date, Date.today], [DateTime, DateTime.now], [Time, Time.now], [Time, Time.at(0)], [Grape::API::Boolean, false], [String, ''], [String, 'non-empty-string'], [Symbol, :symbol], [TrueClass, true], [FalseClass, false] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'structures types' do [ [Hash, {}], [Hash, { test: 'non-empty' }], [Array, []], [Array, ['non-empty']], [Array[Integer], []], [Set, []], [Set, [1]] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'special types' do [ [JSON, ''], [JSON, { test: 'non-empty-string' }.to_json], [Array[JSON], []], [Array[JSON], [{ test: 'non-empty-string' }.to_json]], [File, ''], [File, { test: 'non-empty-string' }.to_json], [Rack::Multipart::UploadedFile, ''], [Rack::Multipart::UploadedFile, { test: 'non-empty-string' }.to_json] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end context 'variant-member-type collections' do [ [Array[Integer, String], [0, '']], [Array[Integer, String], [42, 'non-empty-string']], [[Integer, String, Array[Integer, String]], [0, '', [0, '']]], [[Integer, String, Array[Integer, String]], [42, 'non-empty-string', [42, 'non-empty-string']]] ].each do |type, default| it 'respects the default value' do subject.params do optional :param, type: type, default: default end subject.get '/default_value' do params[:param] end get '/default_value', param: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq(default.to_json) end end end end context 'array with default values and given conditions' do subject do Class.new(Grape::API) do default_format :json end end def app subject end it 'applies the default values only if the conditions are met' do subject.params do requires :ary, type: Array do requires :has_value, type: Grape::API::Boolean given has_value: ->(has_value) { has_value } do optional :type, type: String, values: %w[str int], default: 'str' given type: ->(type) { type == 'str' } do optional :str, type: String, default: 'a' end given type: ->(type) { type == 'int' } do optional :int, type: Integer, default: 1 end end end end subject.post('/nested_given_and_default') { declared(self.params) } params = { ary: [ { has_value: false }, { has_value: true, type: 'int', int: 123 }, { has_value: true, type: 'str', str: 'b' } ] } expected = { 'ary' => [ { 'has_value' => false, 'type' => nil, 'int' => nil, 'str' => nil }, { 'has_value' => true, 'type' => 'int', 'int' => 123, 'str' => nil }, { 'has_value' => true, 'type' => 'str', 'int' => nil, 'str' => 'b' } ] } post '/nested_given_and_default', params expect(last_response.status).to eq(201) expect(JSON.parse(last_response.body)).to eq(expected) end end end ================================================ FILE: spec/grape/validations/validators/exactly_one_of_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::ExactlyOneOfValidator do describe '#validate!' do subject(:validate) { post path, params } describe '/' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end post do end end end context 'when all params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end context 'when a subset of params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,grapefruit' => ['are mutually exclusive'] ) end end context 'when exacly one param is present' do let(:path) { '/' } let(:params) { { beer: true, somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end context 'when none of the params are present' do let(:path) { '/' } let(:params) { { somethingelse: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided'] ) end end end describe '/mixed-params' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit optional :other exactly_one_of :beer, :wine, :grapefruit end post 'mixed-params' do end end end let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one' end post '/custom-message' do end end end let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['you should choose one'] ) end end describe '/nested-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :item, type: Hash do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-hash' do end end end let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end describe '/nested-optional-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :item, type: Hash do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-optional-hash' do end end end let(:path) { '/nested-optional-hash' } context 'when params are passed' do let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when params are empty' do let(:params) { { other: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end describe '/nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do optional :beer optional :wine optional :grapefruit exactly_one_of :beer, :wine, :grapefruit end end post '/nested-array' do end end end let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][beer],items[0][wine]' => [ 'are mutually exclusive' ], 'items[1][wine],items[1][grapefruit]' => [ 'are mutually exclusive' ] ) end end describe '/deeply-nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit, type: Grape::API::Boolean exactly_one_of :beer, :wine, :grapefruit end end end post '/deeply-nested-array' do end end end let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => [ 'are mutually exclusive' ] ) end end end end ================================================ FILE: spec/grape/validations/validators/except_values_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::ExceptValuesValidator do describe 'IncompatibleOptionValues' do subject { api } context 'when a default value is set' do let(:api) do ev = except_values dv = default_value Class.new(Grape::API) do params do optional :type, except_values: ev, default: dv end end end context 'when default value is in exclude' do let(:except_values) { 1..10 } let(:default_value) { except_values.to_a.sample } it 'raises IncompatibleOptionValues' do expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues end end context 'when default array has excluded values' do let(:except_values) { 1..10 } let(:default_value) { [8, 9, 10] } it 'raises IncompatibleOptionValues' do expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues end end end context 'when type is incompatible' do let(:api) do Class.new(Grape::API) do params do optional :type, except_values: 1..10, type: Symbol end end end it 'raises IncompatibleOptionValues' do expect { subject }.to raise_error Grape::Exceptions::IncompatibleOptionValues end end end { req_except: { requires: { except_values: %w[invalid-type1 invalid-type2 invalid-type3] }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_hash: { requires: { except_values: { value: %w[invalid-type1 invalid-type2 invalid-type3] } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_custom_message: { requires: { except_values: { value: %w[invalid-type1 invalid-type2 invalid-type3], message: 'is not allowed' } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_no_value: { requires: { except_values: { message: 'is not allowed' } }, tests: [ { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } ] }, req_except_empty: { requires: { except_values: [] }, tests: [ { value: 'invalid-type1', rc: 200, body: { type: 'invalid-type1' }.to_json } ] }, req_except_lambda: { requires: { except_values: -> { %w[invalid-type1 invalid-type2 invalid-type3 invalid-type4] } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type4', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, req_except_lambda_custom_message: { requires: { except_values: { value: -> { %w[invalid-type1 invalid-type2 invalid-type3 invalid-type4] }, message: 'is not allowed' } }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'invalid-type4', rc: 400, body: { error: 'type is not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json } ] }, opt_except_default: { optional: { except_values: %w[invalid-type1 invalid-type2 invalid-type3], default: 'valid-type2' }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, { rc: 200, body: { type: 'valid-type2' }.to_json } ] }, opt_except_lambda_default: { optional: { except_values: -> { %w[invalid-type1 invalid-type2 invalid-type3] }, default: 'valid-type2' }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'invalid-type3', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 'valid-type', rc: 200, body: { type: 'valid-type' }.to_json }, { rc: 200, body: { type: 'valid-type2' }.to_json } ] }, req_except_type_coerce: { requires: { type: Integer, except_values: [10, 11] }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '11', rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '3', rc: 200, body: { type: 3 }.to_json }, { value: 3, rc: 200, body: { type: 3 }.to_json } ] }, opt_except_type_coerce_default: { optional: { type: Integer, except_values: [10, 11], default: 12 }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 10, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: '3', rc: 200, body: { type: 3 }.to_json }, { value: 3, rc: 200, body: { type: 3 }.to_json }, { rc: 200, body: { type: 12 }.to_json } ] }, opt_except_array_type_coerce_default: { optional: { type: Array[Integer], except_values: [10, 11], default: 12 }, tests: [ { value: 'invalid-type1', rc: 400, body: { error: 'type is invalid' }.to_json }, { value: 10, rc: 400, body: { error: 'type is invalid' }.to_json }, { value: [10], rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: ['3'], rc: 200, body: { type: [3] }.to_json }, { value: [3], rc: 200, body: { type: [3] }.to_json }, { rc: 200, body: { type: 12 }.to_json } ] }, req_except_range: { optional: { type: Integer, except_values: 10..12 }, tests: [ { value: 11, rc: 400, body: { error: 'type has a value not allowed' }.to_json }, { value: 13, rc: 200, body: { type: 13 }.to_json } ] } }.each do |path, param_def| param_def[:tests].each do |t| describe "when #{path}" do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, **param_def[:requires] if param_def.key? :requires optional :type, **param_def[:optional] if param_def.key? :optional end get path do { type: params[:type] } end end end let(:body) do {}.tap do |body| body[:type] = t[:value] if t.key? :value end end before do get path.to_s, **body end it "returns body #{t[:body]} with status #{t[:rc]}" do expect(last_response.status).to eq t[:rc] expect(last_response.body).to eq t[:body] end end end end end ================================================ FILE: spec/grape/validations/validators/length_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::LengthValidator do describe '/with_min_max' do let(:app) do Class.new(Grape::API) do params do requires :list, length: { min: 2, max: 3 } end post 'with_min_max' do end end end context 'when length is within limits' do it do post '/with_min_max', list: [1, 2] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length is exceeded' do it do post '/with_min_max', list: [1, 2, 3, 4, 5] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length within 2 and 3') end end context 'when length is less than minimum' do it do post '/with_min_max', list: [1] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length within 2 and 3') end end end describe '/with_max_only' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { max: 3 } end post 'with_max_only' do end end end context 'when length is less than limits' do it do post '/with_max_only', list: [1, 2] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length is exceeded' do it do post '/with_max_only', list: [1, 2, 3, 4, 5] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length less than or equal to 3') end end end describe '/with_min_only' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: 2 } end post 'with_min_only' do end end end context 'when length is greater than limit' do it do post '/with_min_only', list: [1, 2] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length is less than limit' do it do post '/with_min_only', list: [1] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length greater than or equal to 2') end end end describe '/zero_min' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [JSON], length: { min: 0 } end post 'zero_min' do end end end context 'when length is equal to the limit' do it do post '/zero_min', list: '[]' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length is greater than limit' do it do post '/zero_min', list: [{ key: 'value' }] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end end describe '/zero_max' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [JSON], length: { max: 0 } end post 'zero_max' do end end end context 'when length is within the limit' do it do post '/zero_max', list: '[]' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length is greater than limit' do it do post '/zero_max', list: [{ key: 'value' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length less than or equal to 0') end end end describe '/type_is_not_array' do let(:app) do Class.new(Grape::API) do params do requires :list, type: Integer, length: { max: 3 } end post 'type_is_not_array' do end end end context 'does not raise an error' do it do expect do post 'type_is_not_array', list: 12 end.not_to raise_error end end end describe '/type_supports_length' do let(:app) do Class.new(Grape::API) do params do requires :list, type: Hash, length: { max: 3 } end post 'type_supports_length' do end end end context 'when length is within limits' do it do post 'type_supports_length', list: { key: 'value' } expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length exceeds the limit' do it do post 'type_supports_length', list: { key: 'value', key1: 'value', key3: 'value', key4: 'value' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length less than or equal to 3') end end end describe '/negative_min' do context 'when min is negative' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: -3 } end post 'negative_min' do end end end it 'raises an error' do expect { post 'negative_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end end end describe '/negative_max' do context 'it raises an error' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { max: -3 } end post 'negative_max' do end end end it do expect { post 'negative_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end end end describe '/float_min' do context 'when min is not an integer' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: 2.5 } end post 'float_min' do end end end it do expect { post 'float_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero') end end end describe '/float_max' do context 'when max is not an integer' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { max: 2.5 } end post 'float_max' do end end end it do expect { post 'float_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero') end end end describe '/min_greater_than_max' do context 'raises an error' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: 15, max: 3 } end post 'min_greater_than_max' do end end end it do expect { post 'min_greater_than_max', list: [12] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3') end end end describe '/min_equal_to_max' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: 3, max: 3 } end post 'min_equal_to_max' do end end end context 'when array meets expectations' do it do post 'min_equal_to_max', list: [1, 2, 3] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when array is less than min' do it do post 'min_equal_to_max', list: [1, 2] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length within 3 and 3') end end context 'when array is greater than max' do it do post 'min_equal_to_max', list: [1, 2, 3, 4] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list is expected to have length within 3 and 3') end end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do params do requires :list, type: [Integer], length: { min: 2, message: 'not match' } end post '/custom-message' do end end end context 'is within limits' do it do post '/custom-message', list: [1, 2, 3] expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'is outside limit' do it do post '/custom-message', list: [1] expect(last_response.status).to eq(400) expect(last_response.body).to eq('list not match') end end end describe '/is' do let(:app) do Class.new(Grape::API) do params do requires :code, length: { is: 2 } end post 'is' do end end end context 'when length is exact' do it do post 'is', code: 'ZZ' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'when length exceeds the limit' do it do post 'is', code: 'aze' expect(last_response.status).to eq(400) expect(last_response.body).to eq('code is expected to have length exactly equal to 2') end end context 'when length is less than the limit' do it do post 'is', code: 'a' expect(last_response.status).to eq(400) expect(last_response.body).to eq('code is expected to have length exactly equal to 2') end end context 'when length is zero' do it do post 'is', code: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('code is expected to have length exactly equal to 2') end end end describe '/negative_is' do let(:app) do Class.new(Grape::API) do params do requires :code, length: { is: -2 } end post 'negative_is' do end end end context 'when `is` is negative' do it do expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero') end end end describe '/is_with_max' do context 'when `is` is combined with max' do let(:app) do Class.new(Grape::API) do params do requires :code, length: { is: 2, max: 10 } end post 'is_with_max' do end end end it do expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max') end end end end ================================================ FILE: spec/grape/validations/validators/mutually_exclusive_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::MutuallyExclusiveValidator do describe '#validate!' do subject(:validate) { post path, params } describe '/' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end post do end end end context 'when all mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end context 'when a subset of mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,grapefruit' => ['are mutually exclusive'] ) end end context 'when no mutually exclusive params are present' do let(:path) { '/' } let(:params) { { beer: true, somethingelse: true } } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end describe '/mixed-params' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit optional :other mutually_exclusive :beer, :wine, :grapefruit end post 'mixed-params' do end end end let(:path) { '/mixed-params' } let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine,grapefruit' => ['are mutually exclusive'] ) end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine' end post '/custom-message' do end end end let(:path) { '/custom-message' } let(:params) { { beer: true, wine: true } } it 'returns a validation error' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'beer,wine' => ['you should not mix beer and wine'] ) end end describe '/nested-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :item, type: Hash do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-hash' do end end end let(:path) { '/nested-hash' } let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end describe '/nested-optional-hash' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do optional :item, type: Hash do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-optional-hash' do end end end let(:path) { '/nested-optional-hash' } context 'when params are passed' do let(:params) { { item: { beer: true, wine: true } } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'item[beer],item[wine]' => ['are mutually exclusive'] ) end end context 'when params are empty' do let(:params) { {} } it 'does not return a validation error' do validate expect(last_response.status).to eq 201 end end end describe '/nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do optional :beer optional :wine optional :grapefruit mutually_exclusive :beer, :wine, :grapefruit end end post '/nested-array' do end end end let(:path) { '/nested-array' } let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][beer],items[0][wine]' => ['are mutually exclusive'], 'items[1][wine],items[1][grapefruit]' => ['are mutually exclusive'] ) end end describe '/deeply-nested-array' do let(:app) do Class.new(Grape::API) do rescue_from Grape::Exceptions::ValidationErrors do |e| error!(e.errors.transform_keys! { |key| key.join(',') }, 400) end params do requires :items, type: Array do requires :nested_items, type: Array do optional :beer, :wine, :grapefruit, type: Grape::API::Boolean mutually_exclusive :beer, :wine, :grapefruit end end end post '/deeply-nested-array' do end end end let(:path) { '/deeply-nested-array' } let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } it 'returns a validation error with full names of the params' do validate expect(last_response.status).to eq 400 expect(JSON.parse(last_response.body)).to eq( 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => ['are mutually exclusive'] ) end end end end ================================================ FILE: spec/grape/validations/validators/presence_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::PresenceValidator do subject do Class.new(Grape::API) do format :json end end def app subject end context 'without validation' do before do subject.resource :bacons do get do 'All the bacon' end end end it 'does not validate for any params' do get '/bacons' expect(last_response.status).to eq(200) expect(last_response.body).to eq('All the bacon'.to_json) end end context 'with a custom validation message' do before do subject.resource :requires do params do requires :email, type: String, allow_blank: { value: false, message: 'has no value' }, regexp: { value: /^\S+$/, message: 'format is invalid' }, message: 'is required' end get do 'Hello' end end end it 'requires when missing' do get '/requires' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is required, email has no value"}') end it 'requires when empty' do get '/requires', email: '' expect(last_response.body).to eq('{"error":"email has no value, email format is invalid"}') end it 'valid when set' do get '/requires', email: 'bob@example.com' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with a required regexp parameter supplied in the POST body' do before do subject.format :json subject.params do requires :id, regexp: /^[0-9]+$/ end subject.post do { ret: params[:id] } end end it 'validates id' do post '/' expect(last_response).to be_bad_request expect(last_response.body).to eq('{"error":"id is missing"}') post '/', { id: 'a56b' }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('{"error":"id is invalid"}') expect(last_response).to be_bad_request post '/', { id: 56 }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.body).to eq('{"ret":56}') expect(last_response).to be_created end end context 'with a required non-empty string' do before do subject.params do requires :email, type: String, allow_blank: false, regexp: /^\S+$/ end subject.get do 'Hello' end end it 'requires when missing' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is missing, email is empty"}') end it 'requires when empty' do get '/', email: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"email is empty, email is invalid"}') end it 'valid when set' do get '/', email: 'bob@example.com' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with multiple parameters per requires' do before do subject.params do requires :one, :two end subject.get '/single-requires' do 'Hello' end subject.params do requires :one requires :two end subject.get '/multiple-requires' do 'Hello' end end it 'validates for all defined params' do get '/single-requires' expect(last_response.status).to eq(400) single_requires_error = last_response.body get '/multiple-requires' expect(last_response.status).to eq(400) expect(last_response.body).to eq(single_requires_error) end end context 'with required parameters and no type' do before do subject.params do requires :name, :company end subject.get do 'Hello' end end it 'validates name, company' do get '/' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is missing, company is missing"}') get '/', name: 'Bob' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"company is missing"}') get '/', name: 'Bob', company: 'TestCorp' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello'.to_json) end end context 'with nested parameters' do before do subject.params do requires :user, type: Hash do requires :first_name requires :last_name end end subject.get '/nested' do 'Nested' end end it 'validates nested parameters' do get '/nested' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}') get '/nested', user: { first_name: 'Billy' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"user[last_name] is missing"}') get '/nested', user: { first_name: 'Billy', last_name: 'Bob' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('Nested'.to_json) end end context 'with triply nested required parameters' do before do subject.params do requires :admin, type: Hash do requires :admin_name requires :super, type: Hash do requires :user, type: Hash do requires :first_name requires :last_name end end end end subject.get '/nested_triple' do 'Nested triple' end end it 'validates triple nested parameters' do get '/nested_triple' expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', user: { first_name: 'Billy' } expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', admin: { super: { first_name: 'Billy' } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}') get '/nested_triple', super: { user: { first_name: 'Billy', last_name: 'Bob' } } expect(last_response.status).to eq(400) expect(last_response.body).to include '{"error":"admin is missing' get '/nested_triple', admin: { super: { user: { first_name: 'Billy' } } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[admin_name] is missing, admin[super][user][last_name] is missing"}') get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy' } } } expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"admin[super][user][last_name] is missing"}') get '/nested_triple', admin: { admin_name: 'admin', super: { user: { first_name: 'Billy', last_name: 'Bob' } } } expect(last_response.status).to eq(200) expect(last_response.body).to eq('Nested triple'.to_json) end end context 'with reused parameter documentation once required and once optional' do before do docs = { name: { type: String, desc: 'some name' } } subject.params do requires :all, using: docs end subject.get '/required' do 'Hello required' end subject.params do optional :all, using: docs end subject.get '/optional' do 'Hello optional' end end it 'works with required' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is missing"}') get '/required', name: 'Bob' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello required'.to_json) end it 'works with optional' do get '/optional' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello optional'.to_json) get '/optional', name: 'Bob' expect(last_response.status).to eq(200) expect(last_response.body).to eq('Hello optional'.to_json) end end context 'with a custom type' do it 'does not validate their type when it is missing' do custom_type = Class.new do def self.parse(value) return if value.blank? new end end subject.params do requires :custom, type: custom_type end subject.get '/custom' do 'custom' end get 'custom' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"custom is missing"}') get 'custom', custom: 'filled' expect(last_response.status).to eq(200) end end end ================================================ FILE: spec/grape/validations/validators/regexp_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::RegexpValidator do describe '#bad encoding' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, regexp: { value: /^[a-z]+$/ } end get '/bad_encoding' end end context 'when value as bad encoding' do it 'does not raise an error' do expect { get '/bad_encoding', name: "Hello \x80" }.not_to raise_error end end end describe '/' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :name, regexp: /^[a-z]+$/ end get do end end end context 'invalid input' do it 'refuses inapppopriate' do get '/', name: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is invalid"}') end it 'refuses empty' do get '/', name: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name is invalid"}') end end it 'accepts nil' do get '/', name: nil expect(last_response.status).to eq(200) end it 'accepts valid input' do get '/', name: 'bob' expect(last_response.status).to eq(200) end end describe '/regexp_with_array' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :names, type: Array[String], regexp: /^[a-z]+$/ end get 'regexp_with_array' do end end end it 'refuses inapppopriate items' do get '/regexp_with_array', names: ['invalid name', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'refuses empty items' do get '/regexp_with_array', names: ['', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'refuses nil items' do get '/regexp_with_array', names: [nil, 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names is invalid"}') end it 'accepts valid items' do get '/regexp_with_array', names: ['bob'] expect(last_response.status).to eq(200) end it 'accepts nil instead of array' do get '/regexp_with_array', names: nil expect(last_response.status).to eq(200) end end describe '/nested_regexp_with_array' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :people, type: Hash do requires :names, type: Array[String], regexp: /^[a-z]+$/ end end get 'nested_regexp_with_array' do end end end it 'refuses inapppopriate' do get '/nested_regexp_with_array', people: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"people is invalid, people[names] is missing, people[names] is invalid"}') end end describe '/custom_message' do let(:app) do Class.new(Grape::API) do default_format :json resources :custom_message do params do requires :name, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get do end params do requires :names, type: { value: Array[String], message: 'can\'t be nil' }, regexp: { value: /^[a-z]+$/, message: 'format is invalid' } end get 'regexp_with_array' do end end end end context 'with invalid input' do it 'refuses inapppopriate' do get '/custom_message', name: 'invalid name' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name format is invalid"}') end it 'refuses empty' do get '/custom_message', name: '' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"name format is invalid"}') end end it 'accepts nil' do get '/custom_message', name: nil expect(last_response.status).to eq(200) end it 'accepts valid input' do get '/custom_message', name: 'bob' expect(last_response.status).to eq(200) end context 'regexp with array' do it 'refuses inapppopriate items' do get '/custom_message/regexp_with_array', names: ['invalid name', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names format is invalid"}') end it 'refuses empty items' do get '/custom_message/regexp_with_array', names: ['', 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names format is invalid"}') end it 'refuses nil items' do get '/custom_message/regexp_with_array', names: [nil, 'abc'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"names can\'t be nil"}') end it 'accepts valid items' do get '/custom_message/regexp_with_array', names: ['bob'] expect(last_response.status).to eq(200) end it 'accepts nil instead of array' do get '/custom_message/regexp_with_array', names: nil expect(last_response.status).to eq(200) end end end end ================================================ FILE: spec/grape/validations/validators/same_as_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::SameAsValidator do describe '/' do let(:app) do Class.new(Grape::API) do params do requires :password requires :password_confirmation, same_as: :password end post do end end end context 'is the same' do it do post '/', password: '987654', password_confirmation: '987654' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'is not the same' do it do post '/', password: '123456', password_confirmation: 'whatever' expect(last_response.status).to eq(400) expect(last_response.body).to eq('password_confirmation is not the same as password') end end end describe '/custom-message' do let(:app) do Class.new(Grape::API) do params do requires :password requires :password_confirmation, same_as: { value: :password, message: 'not match' } end post '/custom-message' do end end end context 'is the same' do it do post '/custom-message', password: '987654', password_confirmation: '987654' expect(last_response.status).to eq(201) expect(last_response.body).to eq('') end end context 'is not the same' do it do post '/custom-message', password: '123456', password_confirmation: 'whatever' expect(last_response.status).to eq(400) expect(last_response.body).to eq('password_confirmation not match') end end end end ================================================ FILE: spec/grape/validations/validators/values_validator_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations::Validators::ValuesValidator do let(:values_model) do Class.new do class << self def values @values ||= [] [default_values + @values].flatten.uniq end def add_value(value) @values ||= [] @values << value end def excepts @excepts ||= [] [default_excepts + @excepts].flatten.uniq end def add_except(except) @excepts ||= [] @excepts << except end def include?(value) values.include?(value) end def even?(value) value.to_i.even? end private def default_values %w[valid-type1 valid-type2 valid-type3].freeze end def default_excepts %w[invalid-type1 invalid-type2 invalid-type3].freeze end end end end before do stub_const('ValuesModel', values_model) end describe '#bad encoding' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, type: String, values: %w[a b] end get '/bad_encoding' end end context 'when value as bad encoding' do it 'does not raise an error' do expect { get '/bad_encoding', type: "Hello \x80" }.not_to raise_error end end end describe '/custom_message' do let(:app) do Class.new(Grape::API) do default_format :json resources :custom_message do params do requires :type, values: { value: ValuesModel.values, message: 'value does not include in values' } end get '/' do { type: params[:type] } end params do optional :type, values: { value: -> { ValuesModel.values }, message: 'value does not include in values' }, default: 'valid-type2' end get '/lambda' do { type: params[:type] } end end end end it 'allows a valid value for a parameter' do get('/custom_message', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow an invalid value for a parameter' do get('/custom_message', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) end it 'validates against values in a proc' do ValuesModel.add_value('valid-type4') get('/custom_message/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end it 'does not allow an invalid value for a parameter using lambda' do get('/custom_message/lambda', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type value does not include in values' }.to_json) end end describe '/' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: ValuesModel.values end get '/' do { type: params[:type] } end end end it 'allows a valid value for a parameter' do get('/', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow an invalid value for a parameter' do get('/', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end context 'nil value for a parameter' do it 'does not allow for root params scope' do get('/', type: nil) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end it 'does not validate updated values without proc' do app # Instantiate with the existing values. ValuesModel.add_value('valid-type4') get('/', type: 'valid-type4') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/empty' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: [] end get '/empty' end end it 'rejects all values if values is an empty array' do get('/empty', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/optional_with_required_values' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :optional, type: Array do requires :type, values: %w[a b] end end get '/optional_with_required_values' end end it 'allows nil value for a required param in child scope' do get('/optional_with_required_values') expect(last_response.status).to eq 200 end end describe '/optional_with_array_of_string_values' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :optional, type: Array[String], values: %w[a b c] end put '/optional_with_array_of_string_values' end end it 'accepts nil for an optional param with a list of values' do put('/optional_with_array_of_string_values', optional: nil) expect(last_response.status).to eq 200 end end describe '/default/valid' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, values: ValuesModel.values, default: 'valid-type2' end get '/default/valid' do { type: params[:type] } end end end it 'allows a valid default value' do get('/default/valid') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end end describe '/default/hash/valid' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, values: { value: ValuesModel.values }, default: 'valid-type2' end get '/default/hash/valid' do { type: params[:type] } end end end it 'allows a valid default value' do get('/default/hash/valid') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type2' }.to_json) end end describe '/lambda' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, values: -> { ValuesModel.values }, default: 'valid-type2' end get '/lambda' do { type: params[:type] } end end end it 'allows a proc for values' do get('/lambda', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'validates against values in a proc' do ValuesModel.add_value('valid-type4') get('/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end it 'does not allow an invalid value for a parameter using lambda' do get('/lambda', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'evaluates the proc per-request, not at definition time (e.g. for DB-backed values)' do app # instantiate at definition time, before the new value is added ValuesModel.add_value('valid-type4') get('/lambda', type: 'valid-type4') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type4' }.to_json) end end describe '/endless' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, type: Integer, values: 1.. end get '/endless' do { type: params[:type] } end end end it 'validates against values in an endless range' do get('/endless', type: 10) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10 }.to_json) end it 'does not allow an invalid value for a parameter using an endless range' do get('/endless', type: 0) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/lambda_val' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: ->(v) { ValuesModel.include? v } end get '/lambda_val' do { type: params[:type] } end end end it 'allows value using lambda' do get('/lambda_val', type: 'valid-type1') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'does not allow invalid value using lambda' do get('/lambda_val', type: 'invalid-type') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/lambda_int_val' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :number, type: Integer, values: ->(v) { v > 0 } end get '/lambda_int_val' do { number: params[:number] } end end end it 'does not allow non-numeric string value for int value using lambda' do get('/lambda_int_val', number: 'foo') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'number is invalid, number does not have a valid value' }.to_json) end it 'does not allow nil for int value using lambda' do get('/lambda_int_val', number: nil) expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'number does not have a valid value' }.to_json) end it 'allows numeric string for int value using lambda' do get('/lambda_int_val', number: '3') expect(last_response.status).to eq 200 expect(last_response.body).to eq({ number: 3 }.to_json) end end describe '/empty_lambda' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: -> { [] } end get '/empty_lambda' end end it 'validates against an empty array in a proc' do get('/empty_lambda', type: 'any') expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/default_lambda' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample } end get '/default_lambda' do { type: params[:type] } end end end it 'validates default value from proc' do get('/default_lambda') expect(last_response.status).to eq 200 end end describe '/default_and_values_lambda' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample } end get '/default_and_values_lambda' do { type: params[:type] } end end end it 'validates default value from proc against values in a proc' do get('/default_and_values_lambda') expect(last_response.status).to eq 200 end end context 'IncompatibleOptionValues' do it 'raises on an invalid default value from proc' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValuesModel.values.sample}_invalid" } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises on an invalid default value' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises when type is incompatible with values array' do subject = Class.new(Grape::API) expect do subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises when values contains a value that is not a kind of the type' do subject = Class.new(Grape::API) expect do subject.params { requires :type, values: [10.5, 11], type: Integer } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end it 'raises when except contains a value that is not a kind of the type' do subject = Class.new(Grape::API) expect do subject.params { requires :type, except_values: [10.5, 11], type: Integer } end.to raise_error Grape::Exceptions::IncompatibleOptionValues end end describe '/values/optional_boolean' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :type, type: Grape::API::Boolean, desc: 'A boolean', values: [true] end get '/values/optional_boolean' do { type: params[:type] } end end end it 'allows a value from the list' do get('/values/optional_boolean', type: true) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: true }.to_json) end it 'rejects a value which is not in the list' do get('/values/optional_boolean', type: false) expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/values/coercion' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10 end get '/values/coercion' do { type: params[:type] } end end end it 'allows values to be a kind of the coerced type not just an instance of it' do get('/values/coercion', type: 10) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 10 }.to_json) end end describe '/values/array_coercion' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10 end get '/values/array_coercion' do { type: params[:type] } end end end it 'allows values to be a kind of the coerced type in an array' do get('/values/array_coercion', type: [10]) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: [10] }.to_json) end end describe '/allow_blank' do let(:app) do Class.new(Grape::API) do default_format :json params do optional :name, type: String, values: %w[a b], allow_blank: true end get '/allow_blank' end end it 'allows a blank value when the allow_blank option is true' do get 'allow_blank', name: nil expect(last_response.status).to eq(200) get 'allow_blank', name: '' expect(last_response.status).to eq(200) end end context 'with a lambda values' do subject do Class.new(Grape::API) do params do optional :type, type: String, values: -> { [SecureRandom.uuid] }, default: -> { SecureRandom.uuid } end get '/random_values' end end def app subject end before do expect(SecureRandom).to receive(:uuid).and_return('foo').once end it 'only evaluates values dynamically with each request' do get '/random_values', type: 'foo' expect(last_response.status).to eq 200 end it 'chooses default' do get '/random_values' expect(last_response.status).to eq 200 end end context 'with a range of values' do subject(:app) do Class.new(Grape::API) do params do optional :value, type: Float, values: 0.0..10.0 end get '/value' do { value: params[:value] }.to_json end params do optional :values, type: Array[Float], values: 0.0..10.0 end get '/values' do { values: params[:values] }.to_json end end end it 'allows a single value inside of the range' do get('/value', value: 5.2) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ value: 5.2 }.to_json) end it 'allows an array of values inside of the range' do get('/values', values: [8.6, 7.5, 3, 0.9]) expect(last_response.status).to eq 200 expect(last_response.body).to eq({ values: [8.6, 7.5, 3.0, 0.9] }.to_json) end it 'rejects a single value outside the range' do get('/value', value: 'a') expect(last_response.status).to eq 400 expect(last_response.body).to eq('value is invalid, value does not have a valid value') end it 'rejects an array of values if any of them are outside the range' do get('/values', values: [8.6, 75, 3, 0.9]) expect(last_response.status).to eq 400 expect(last_response.body).to eq('values does not have a valid value') end end describe '/mixed/value/except' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, type: Integer, values: 1..5, except_values: [3] end get '/mixed/value/except' do { type: params[:type] } end end end it 'allows value, but not in except' do get '/mixed/value/except', type: 2 expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 2 }.to_json) end it 'rejects except' do get '/mixed/value/except', type: 3 expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type has a value not allowed' }.to_json) end it 'rejects outside except and outside value' do get '/mixed/value/except', type: 10 expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/proc' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: ->(v) { ValuesModel.include? v } end get '/proc' do { type: params[:type] } end end end it 'accepts a single valid value' do get '/proc', type: 'valid-type1' expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: 'valid-type1' }.to_json) end it 'accepts multiple valid values' do get '/proc', type: %w[valid-type1 valid-type3] expect(last_response.status).to eq 200 expect(last_response.body).to eq({ type: %w[valid-type1 valid-type3] }.to_json) end it 'rejects a single invalid value' do get '/proc', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end it 'rejects an invalid value among valid ones' do get '/proc', type: %w[valid-type1 invalid-type1 valid-type3] expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json) end end describe '/proc/message' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :type, values: { value: ->(v) { ValuesModel.include? v }, message: 'failed check' } end get '/proc/message' end end it 'uses supplied message' do get '/proc/message', type: 'invalid-type1' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type failed check' }.to_json) end end describe '/proc/custom_message' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :number, values: { value: ->(v) { ValuesModel.even? v }, message: 'must be even' } end get '/proc/custom_message' do { message: 'success' } end end end it 'accepts a valid value' do get '/proc/custom_message', number: 4 expect(last_response.status).to eq 200 expect(last_response.body).to eq({ message: 'success' }.to_json) end it 'rejects an invalid value' do get '/proc/custom_message', number: 5 expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'number must be even' }.to_json) end end describe '/proc/arity2' do let(:app) do Class.new(Grape::API) do default_format :json params do requires :input_one, :input_two, values: { value: ->(v1, v2) { v1 + v2 > 10 } } end get '/proc/arity2' end end it 'returns an error status code' do get '/proc/arity2', input_one: 2, input_two: 3 expect(last_response.status).to eq 400 end end describe '/values_wrapped_by_with_block' do let(:app) do Class.new(Grape::API) do default_format :json params do with(type: String) do requires :type, values: ValuesModel.values end end get 'values_wrapped_by_with_block' end end it 'rejects an invalid value' do get 'values_wrapped_by_with_block' expect(last_response.status).to eq 400 expect(last_response.body).to eq({ error: 'type is missing, type does not have a valid value' }.to_json) end end end ================================================ FILE: spec/grape/validations_spec.rb ================================================ # frozen_string_literal: true describe Grape::Validations do subject { Class.new(Grape::API) } let(:app) { subject } describe 'params' do context 'optional' do before do subject.params do optional :a_number, regexp: /^[0-9]+$/ optional :attachment, type: File end subject.get '/optional' do 'optional works!' end end it 'validates when params is present' do get '/optional', a_number: 'string' expect(last_response.status).to eq(400) expect(last_response.body).to eq('a_number is invalid') get '/optional', a_number: 45 expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional works!') end it "doesn't validate when param not present" do get '/optional', a_number: nil, attachment: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional works!') end it 'adds to declared parameters' do subject.params do optional :some_param end subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('some_param' => nil) end end context 'optional using Grape::Entity documentation' do def define_optional_using documentation = { field_a: { type: String }, field_b: { type: String } } subject.params do optional :all, using: documentation end end before do define_optional_using subject.get '/optional' do 'optional with using works' end end it 'adds entity documentation to declared params' do define_optional_using subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { field_a: 'field_a', field_b: 'field_b' }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('field_a' => 'field_a', 'field_b' => 'field_b') end it 'works when field_a and field_b are not present' do get '/optional' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end it 'works when field_a is present' do get '/optional', field_a: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end it 'works when field_b is present' do get '/optional', field_b: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with using works') end end context 'required' do before do subject.params do requires :key, type: String end subject.get('/required') { 'required works' } subject.put('/required') { { key: params[:key] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('key is missing') end it "doesn't throw a missing param when param is present" do get '/required', key: 'cool' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.params do requires :some_param end subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { some_param: 'some_param' }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('some_param' => 'some_param') end it 'works when required field is present but nil' do put '/required', { key: nil }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq('key' => nil) end end context 'requires with nested params' do before do subject.params do requires :first_level, type: Hash do optional :second_level, type: Array do requires :value, type: Integer optional :name, type: String optional :third_level, type: Array do requires :value, type: Integer optional :name, type: String optional :fourth_level, type: Array do requires :value, type: Integer optional :name, type: String end end end end end subject.put('/required') { 'required works' } end let(:request_params) do { first_level: { second_level: [ { value: 1, name: 'Lisa' }, { value: 2, name: 'James', third_level: [ { value: 'three', name: 'Sophie' }, { value: 4, name: 'Jenny', fourth_level: [ { name: 'Samuel' }, { value: 6, name: 'Jane' } ] } ] } ] } } end it 'validates correctly in deep nested params' do put '/required', request_params.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'first_level[second_level][1][third_level][0][value] is invalid, ' \ 'first_level[second_level][1][third_level][1][fourth_level][0][value] is missing' ) end end context 'requires :all using Grape::Entity documentation' do def define_requires_all documentation = { required_field: { type: String, required: true, param_type: 'query' }, optional_field: { type: String }, optional_array_field: { type: Array[String], is_array: true } } subject.params do requires :all, except: %i[optional_field optional_array_field], using: documentation end end before do define_requires_all subject.get '/required' do 'required works' end end it 'adds entity documentation to declared params' do define_requires_all subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { required_field: 'required_field', optional_field: 'optional_field', optional_array_field: ['optional_array_field'] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('required_field' => 'required_field', 'optional_field' => 'optional_field', 'optional_array_field' => ['optional_array_field']) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('required_field is missing') end it 'works when required_field is present' do get '/required', required_field: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'requires :none using Grape::Entity documentation' do def define_requires_none documentation = { required_field: { type: String, example: 'Foo' }, optional_field: { type: Integer, format: 'int64' } } subject.params do requires :none, except: :required_field, using: documentation end end before do define_requires_none subject.get '/required' do 'required works' end end it 'adds entity documentation to declared params' do define_requires_none subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { required_field: 'required_field', optional_field: 1 }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('required_field' => 'required_field', 'optional_field' => 1) end it 'errors when required_field is not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('required_field is missing') end it 'works when required_field is present' do get '/required', required_field: 'woof' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'requires :all or :none but except a non-existent field using Grape::Entity documentation' do context 'requires :all' do def define_requires_all documentation = { required_field: { type: String }, optional_field: { type: String } } subject.params do requires :all, except: :non_existent_field, using: documentation end end it 'adds only the entity documentation to declared params, nothing more' do define_requires_all subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { required_field: 'required_field', optional_field: 'optional_field' }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('required_field' => 'required_field', 'optional_field' => 'optional_field') end end context 'requires :none' do def define_requires_none documentation = { required_field: { type: String }, optional_field: { type: String } } subject.params do requires :none, except: :non_existent_field, using: documentation end end it 'adds only the entity documentation to declared params, nothing more' do expect { define_requires_none }.to raise_error(ArgumentError) end end end context 'required with an Array block' do before do subject.params do requires :items, type: Array do requires :key end end subject.get('/required') { 'required works' } subject.put('/required') { { items: params[:items] }.to_json } end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing') end it 'errors when param is not an Array' do get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') get '/required', items: { key: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it "doesn't throw a missing param when param is present" do get '/required', items: [{ key: 'hello' }, { key: 'world' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it "doesn't throw a missing param when param is present but empty" do put '/required', { items: [] }.to_json, 'CONTENT_TYPE' => 'application/json' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq('items' => []) end it 'adds to declared parameters' do subject.format :json subject.params do requires :items, type: Array do requires :key end end subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { items: [key: 'my_key'] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('items' => ['key' => 'my_key']) end end # Ensure there is no leakage between declared Array types and # subsequent Hash types context 'required with an Array and a Hash block' do before do subject.params do requires :cats, type: Array[String], default: [] requires :items, type: Hash do requires :key end end subject.get '/required' do 'required works' end end it 'does not output index [0] for Hash types' do get '/required', cats: ['Garfield'], items: { foo: 'bar' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[key] is missing') end end context 'required with a Hash block' do before do subject.params do requires :items, type: Hash do requires :key end end subject.get '/required' do 'required works' end end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing, items[key] is missing') end it 'errors when nested param not present' do get '/required', items: { foo: 'bar' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[key] is missing') end it 'errors when param is not a Hash' do get '/required', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing') get '/required', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it "doesn't throw a missing param when param is present" do get '/required', items: { key: 'hello' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.params do requires :items, type: Array do requires :key end end subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { items: [key: :my_key] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('items' => ['key' => 'my_key']) end end context 'hash with a required param with validation' do before do subject.params do requires :items, type: Hash do requires :key, type: String, values: %w[a b] end end subject.get '/required' do 'required works' end end it 'errors when param is not a Hash' do get '/required', items: 'not a hash' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] is missing, items[key] is invalid') get '/required', items: [{ key: 'hash in array' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid, items[key] does not have a valid value') end it 'works when all params match' do get '/required', items: { key: 'a' } expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end end context 'group' do before do subject.params do group :items, type: Array do requires :key end end subject.get '/required' do 'required works' end end it 'errors when param not present' do get '/required' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is missing') end it "doesn't throw a missing param when param is present" do get '/required', items: [key: 'hello'] expect(last_response.status).to eq(200) expect(last_response.body).to eq('required works') end it 'adds to declared parameters' do subject.format :json subject.params do group :items, type: Array do requires :key end end subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { items: [key: :my_key] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('items' => ['key' => 'my_key']) end end context 'group params with nested params which has a type' do let(:invalid_items) { { items: '' } } before do subject.params do optional :items, type: Array do optional :key1, type: String optional :key2, type: String end end subject.post '/group_with_nested' do 'group with nested works' end end it 'errors when group param is invalid' do post '/group_with_nested', items: invalid_items expect(last_response.status).to eq(400) end end context 'custom validator for a Hash' do let(:date_range_validator) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params[attr_name][:from] <= params[attr_name][:to] raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") end end end before do stub_const('DateRangeValidator', date_range_validator) described_class.register(DateRangeValidator) subject.params do optional :date_range, date_range: true, type: Hash do requires :from, type: Integer requires :to, type: Integer end end subject.get('/optional') do 'optional works' end subject.params do requires :date_range, date_range: true, type: Hash do requires :from, type: Integer requires :to, type: Integer end end subject.get('/required') do 'required works' end end after do described_class.deregister(:date_range) end context 'which is optional' do it "doesn't throw an error if the validation passes" do get '/optional', date_range: { from: 1, to: 2 } expect(last_response.status).to eq(200) end it 'errors if the validation fails' do get '/optional', date_range: { from: 2, to: 1 } expect(last_response.status).to eq(400) end end context 'which is required' do it "doesn't throw an error if the validation passes" do get '/required', date_range: { from: 1, to: 2 } expect(last_response.status).to eq(200) end it 'errors if the validation fails' do get '/required', date_range: { from: 2, to: 1 } expect(last_response.status).to eq(400) end end end context 'validation within arrays' do before do subject.params do group :children, type: Array do requires :name group :parents, type: Array do requires :name, allow_blank: false end end end subject.get '/within_array' do 'within array works' end end it 'can handle new scopes within child elements' do get '/within_array', children: [ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] }, { name: 'Joe', parents: [{ name: 'Josie' }] } ] expect(last_response.status).to eq(200) expect(last_response.body).to eq('within array works') end it 'errors when a parameter is not present' do get '/within_array', children: [ { name: 'Jim', parents: [{ name: 'Joy' }] }, { name: 'Job', parents: [{}] } ] # NOTE: with body parameters in json or XML or similar this # should actually fail with: children[parents][name] is missing. expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[1][parents] is missing, children[0][parents][1][name] is missing, children[0][parents][1][name] is empty') end it 'errors when a parameter is not present in array within array' do get '/within_array', children: [ { name: 'Jim', parents: [{ name: 'Joy' }] }, { name: 'Job', parents: [{ name: 'Bill' }, { name: '' }] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[1][parents][1][name] is empty') end it 'handle errors for all array elements' do get '/within_array', children: [ { name: 'Jim', parents: [] }, { name: 'Job', parents: [] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'children[0][parents][0][name] is missing, ' \ 'children[1][parents][0][name] is missing' ) end it 'safely handles empty arrays and blank parameters' do # NOTE: with body parameters in json or XML or similar this # should actually return 200, since an empty array is valid. get '/within_array', children: [] expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'children[0][name] is missing, ' \ 'children[0][parents] is missing, ' \ 'children[0][parents] is invalid, ' \ 'children[0][parents][0][name] is missing, ' \ 'children[0][parents][0][name] is empty' ) get '/within_array', children: [name: 'Jay'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing, children[0][parents][0][name] is empty') end it 'errors when param is not an Array' do get '/within_array', children: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid') get '/within_array', children: { name: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('children is invalid') get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is invalid') end end context 'with block param' do before do subject.params do requires :planets, type: Array do requires :name end end subject.get '/req' do 'within array works' end subject.put '/req' do '' end subject.params do group :stars, type: Array do requires :name end end subject.get '/grp' do 'within array works' end subject.put '/grp' do '' end subject.params do requires :name optional :moons, type: Array do requires :name end end subject.get '/opt' do 'within array works' end subject.put '/opt' do '' end end it 'requires defaults to Array type' do get '/req', planets: 'Jupiter, Saturn' expect(last_response.status).to eq(400) expect(last_response.body).to eq('planets is invalid') get '/req', planets: { name: 'Jupiter' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('planets is invalid') get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }] expect(last_response.status).to eq(200) put_with_json '/req', planets: [] expect(last_response.status).to eq(200) end it 'optional defaults to Array type' do get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede' expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid') get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('moons is invalid') get '/opt', name: 'Jupiter', moons: [{ name: 'Io' }, { name: 'Callisto' }] expect(last_response.status).to eq(200) put_with_json '/opt', name: 'Venus' expect(last_response.status).to eq(200) put_with_json '/opt', name: 'Mercury', moons: [] expect(last_response.status).to eq(200) end it 'group defaults to Array type' do get '/grp', stars: 'Sun' expect(last_response.status).to eq(400) expect(last_response.body).to eq('stars is invalid') get '/grp', stars: { name: 'Sun' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('stars is invalid') get '/grp', stars: [{ name: 'Sun' }] expect(last_response.status).to eq(200) put_with_json '/grp', stars: [] expect(last_response.status).to eq(200) end end context 'validation within arrays with JSON' do before do subject.params do group :children, type: Array do requires :name group :parents, type: Array do requires :name end end end subject.put '/within_array' do 'within array works' end end it 'can handle new scopes within child elements' do put_with_json '/within_array', children: [ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] }, { name: 'Joe', parents: [{ name: 'Josie' }] } ] expect(last_response.status).to eq(200) expect(last_response.body).to eq('within array works') end it 'errors when a parameter is not present' do put_with_json '/within_array', children: [ { name: 'Jim', parents: [{}] }, { name: 'Job', parents: [{ name: 'Joy' }] } ] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents][0][name] is missing') end it 'safely handles empty arrays and blank parameters' do put_with_json '/within_array', children: [] expect(last_response.status).to eq(200) put_with_json '/within_array', children: [name: 'Jay'] expect(last_response.status).to eq(400) expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing') end end context 'optional with an Array block' do before do subject.params do optional :items, type: Array do requires :key end end subject.get '/optional_group' do 'optional group works' end end it "doesn't throw a missing param when the group isn't present" do get '/optional_group' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional group works') end it "doesn't throw a missing param when both group and param are given" do get '/optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional group works') end it 'errors when group is present, but required param is not' do get '/optional_group', items: [{ not_key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][key] is missing') end it "errors when param is present but isn't an Array" do get '/optional_group', items: 'hello' expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') get '/optional_group', items: { key: 'foo' } expect(last_response.status).to eq(400) expect(last_response.body).to eq('items is invalid') end it 'adds to declared parameters' do subject.format :json subject.params do optional :items, type: Array do requires :key end end subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { items: [key: :my_key] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('items' => ['key' => 'my_key']) end end context 'nested optional Array blocks' do before do subject.params do optional :items, type: Array do requires :key optional(:optional_subitems, type: Array) { requires :value } requires(:required_subitems, type: Array) { requires :value } end end subject.get('/nested_optional_group') { 'nested optional group works' } end it 'does no internal validations if the outer group is blank' do get '/nested_optional_group' expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'does internal validations if the outer group is present' do get '/nested_optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'handles deep nesting' do get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') end it 'handles validation within arrays' do get '/nested_optional_group', items: [{ key: 'foo' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('nested optional group works') get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('items[0][optional_subitems][0][value] is missing') end it 'adds to declared parameters' do subject.params do optional :items, type: Array do requires :key optional(:optional_subitems, type: Array) { requires :value } requires(:required_subitems, type: Array) { requires :value } end end subject.format :json subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { items: [{ key: :my_key, required_subitems: [value: 'my_value'] }] }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('items' => [{ 'key' => 'my_key', 'optional_subitems' => [], 'required_subitems' => [{ 'value' => 'my_value' }] }]) end context <<~DESC do Issue occurs whenever: * param structure with at least three levels * 1st level item is a required Array that has >1 entry with an optional item present and >1 entry with an optional item missing#{' '} * 2nd level is an optional Array or Hash#{' '} * 3rd level is a required item (can be any type) * additional levels do not effect the issue from occuring DESC it 'example based off actual real world use case' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batches, type: Array do requires :batch_no, type: String end end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: [{ batches: [{ batch_no: 'A1234567' }] }] }, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!') expect(last_response.status).to eq(200) end it 'simplest example using Array -> Array -> Hash -> String' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batch_no, type: String end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: [{ batch_no: 'A1234567' }] }, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!') expect(last_response.status).to eq(200) end it 'simplest example using Array -> Hash -> String' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do requires :batch_no, type: String end end end subject.get '/validate_required_arrays_under_optional_arrays' do 'validate_required_arrays_under_optional_arrays works!' end data = { orders: [ { id: 77, drugs: { batch_no: 'A1234567' } }, { id: 70 } ] } get '/validate_required_arrays_under_optional_arrays', data expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!') expect(last_response.status).to eq(200) end it 'correctly indexes invalida data' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Array do requires :batch_no, type: String requires :quantity, type: Integer end end end subject.get '/correctly_indexes' do 'correctly_indexes works!' end data = { orders: [ { id: 70 }, { id: 77, drugs: [{ batch_no: 'A1234567', quantity: 12 }, { batch_no: 'B222222' }] } ] } get '/correctly_indexes', data expect(last_response.body).to eq('orders[1][drugs][1][quantity] is missing') expect(last_response.status).to eq(400) end context 'multiple levels of optional and requires settings' do before do subject.params do requires :top, type: Array do requires :top_id, type: Integer, allow_blank: false optional :middle_1, type: Array do requires :middle_1_id, type: Integer, allow_blank: false optional :middle_2, type: Array do requires :middle_2_id, type: String, allow_blank: false optional :bottom, type: Array do requires :bottom_id, type: Integer, allow_blank: false end end end end end subject.get '/multi_level' do 'multi_level works!' end end it 'with valid data' do data = { top: [ { top_id: 1, middle_1: [ { middle_1_id: 11 }, { middle_1_id: 12, middle_2: [ { middle_2_id: 121 }, { middle_2_id: 122, bottom: [{ bottom_id: 1221 }] } ] } ] }, { top_id: 2, middle_1: [ { middle_1_id: 21 }, { middle_1_id: 22, middle_2: [ { middle_2_id: 221 } ] } ] }, { top_id: 3, middle_1: [ { middle_1_id: 31 }, { middle_1_id: 32 } ] }, { top_id: 4 } ] } get '/multi_level', data expect(last_response.body).to eq('multi_level works!') expect(last_response.status).to eq(200) end it 'with invalid data' do data = { top: [ { top_id: 1, middle_1: [ { middle_1_id: 11 }, { middle_1_id: 12, middle_2: [ { middle_2_id: 121 }, { middle_2_id: 122, bottom: [{ bottom_id: nil }] } ] } ] }, { top_id: 2, middle_1: [ { middle_1_id: 21 }, { middle_1_id: 22, middle_2: [{ middle_2_id: nil }] } ] }, { top_id: 3, middle_1: [ { middle_1_id: nil }, { middle_1_id: 32 } ] }, { top_id: nil, missing_top_id: 4 } ] } # debugger get '/multi_level', data expect(last_response.body.split(', ')).to contain_exactly( 'top[3][top_id] is empty', 'top[2][middle_1][0][middle_1_id] is empty', 'top[1][middle_1][1][middle_2][0][middle_2_id] is empty', 'top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty' ) expect(last_response.status).to eq(400) end end end it 'exactly_one_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String exactly_one_of :batch_no, :batch_id end end end subject.get '/exactly_one_of' do 'exactly_one_of works!' end data = { orders: [ { id: 77, drugs: { batch_no: 'A1234567' } }, { id: 70 } ] } get '/exactly_one_of', data expect(last_response.body).to eq('exactly_one_of works!') expect(last_response.status).to eq(200) end it 'at_least_one_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String at_least_one_of :batch_no, :batch_id end end end subject.get '/at_least_one_of' do 'at_least_one_of works!' end data = { orders: [ { id: 77, drugs: { batch_no: 'A1234567' } }, { id: 70 } ] } get '/at_least_one_of', data expect(last_response.body).to eq('at_least_one_of works!') expect(last_response.status).to eq(200) end it 'all_or_none_of' do subject.params do requires :orders, type: Array do requires :id, type: Integer optional :drugs, type: Hash do optional :batch_no, type: String optional :batch_id, type: String all_or_none_of :batch_no, :batch_id end end end subject.get '/all_or_none_of' do 'all_or_none_of works!' end data = { orders: [ { id: 77, drugs: { batch_no: 'A1234567', batch_id: '12' } }, { id: 70 } ] } get '/all_or_none_of', data expect(last_response.body).to eq('all_or_none_of works!') expect(last_response.status).to eq(200) end end context 'multiple validation errors' do before do subject.params do requires :yolo requires :swag end subject.get '/two_required' do 'two required works' end end it 'throws the validation errors' do get '/two_required' expect(last_response.status).to eq(400) expect(last_response.body).to match(/yolo is missing/) expect(last_response.body).to match(/swag is missing/) end end context 'custom validation' do let(:custom_validator) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params[attr_name] == 'im custom' raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'is not custom!') end end end before do stub_const('CustomvalidatorValidator', custom_validator) described_class.register(CustomvalidatorValidator) end after do described_class.deregister(:customvalidator) end context 'when using optional with a custom validator' do before do subject.params do optional :custom, customvalidator: true end subject.get '/optional_custom' do 'optional with custom works!' end end it 'validates when param is present' do get '/optional_custom', custom: 'im custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') get '/optional_custom', custom: 'im wrong' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end it "skips validation when parameter isn't present" do get '/optional_custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') end it 'validates with custom validator when param present and incorrect type' do subject.params do optional :custom, type: String, customvalidator: true end get '/optional_custom', custom: 123 expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end end context 'when using requires with a custom validator' do before do subject.params do requires :custom, customvalidator: true end subject.get '/required_custom' do 'required with custom works!' end end it 'validates when param is present' do get '/required_custom', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') get '/required_custom', custom: 'im custom' expect(last_response.status).to eq(200) expect(last_response.body).to eq('required with custom works!') end it 'validates when param is not present' do get '/required_custom' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is missing, custom is not custom!') end context 'nested namespaces' do before do subject.params do requires :custom, customvalidator: true end subject.namespace 'nested' do get 'one' do 'validation failed' end namespace 'nested' do get 'two' do 'validation failed' end end end subject.namespace 'peer' do get 'one' do 'no validation required' end namespace 'nested' do get 'two' do 'no validation required' end end end subject.namespace 'unrelated' do params do requires :name end get 'one' do 'validation required' end namespace 'double' do get 'two' do 'no validation required' end end end end specify 'the parent namespace uses the validator' do get '/nested/one', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end specify 'the nested namespace inherits the custom validator' do get '/nested/nested/two', custom: 'im wrong, validate me' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom!') end specify 'peer namespaces does not have the validator' do get '/peer/one', custom: 'im not validated' expect(last_response.status).to eq(200) expect(last_response.body).to eq('no validation required') end specify 'namespaces nested in peers should also not have the validator' do get '/peer/nested/two', custom: 'im not validated' expect(last_response.status).to eq(200) expect(last_response.body).to eq('no validation required') end specify 'when nested, specifying a route should clear out the validations for deeper nested params' do get '/unrelated/one' expect(last_response.status).to eq(400) get '/unrelated/double/two' expect(last_response.status).to eq(200) end end end context 'when using options on param' do let(:custom_validator_with_options) do Class.new(Grape::Validations::Validators::Base) do def validate_param!(attr_name, params) return if params[attr_name] == @option[:text] raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) end end end before do stub_const('CustomvalidatorWithOptionsValidator', custom_validator_with_options) described_class.register(CustomvalidatorWithOptionsValidator) subject.params do optional :custom, customvalidator_with_options: { text: 'im custom with options', message: 'is not custom with options!' } end subject.get '/optional_custom' do 'optional with custom works!' end end after do described_class.deregister(:customvalidator_with_options) end it 'validates param with custom validator with options' do get '/optional_custom', custom: 'im custom with options' expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional with custom works!') get '/optional_custom', custom: 'im wrong' expect(last_response.status).to eq(400) expect(last_response.body).to eq('custom is not custom with options!') end end end context 'named' do context 'can be included in usual params' do before do shared_params = Module.new do extend Grape::DSL::Helpers::BaseHelper params :period do optional :start_date optional :end_date end end subject.helpers shared_params subject.helpers do params :pagination do optional :page, type: Integer optional :per_page, type: Integer end end end it 'by #use' do subject.format :json subject.params do use :pagination end subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { page: 1, per_page: 10 }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('page' => 1, 'per_page' => 10) end it 'by #use with multiple params' do subject.format :json subject.params do use :pagination, :period end subject.get('/') do declared(params) end env = Rack::MockRequest.env_for('/', method: Rack::GET, params: { page: 1, per_page: 10, start_date: '2025-01-01', end_date: '2026-01-01' }) response = Rack::MockResponse[*subject.call(env)] expect(JSON.parse(response.body)).to eq('page' => 1, 'per_page' => 10, 'start_date' => '2025-01-01', 'end_date' => '2026-01-01') end end context 'with block' do before do subject.helpers do params :order do |options| optional :order, type: Symbol, values: %i[asc desc], default: options[:default_order] optional :order_by, type: Symbol, values: options[:order_by], default: options[:default_order_by] end end subject.format :json subject.params do use :order, default_order: :asc, order_by: %i[name created_at], default_order_by: :created_at end subject.get '/order' do { order: params[:order], order_by: params[:order_by] } end end it 'returns defaults' do get '/order' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :created_at }.to_json) end it 'overrides default value for order' do get '/order?order=desc' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :desc, order_by: :created_at }.to_json) end it 'overrides default value for order_by' do get '/order?order_by=name' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ order: :asc, order_by: :name }.to_json) end it 'fails with invalid value' do get '/order?order=invalid' expect(last_response.status).to eq(400) expect(last_response.body).to eq('{"error":"order does not have a valid value"}') end end end context 'with block and keyword argument' do before do subject.helpers do params :shared_params do |type:| optional :param, default: type end end subject.format :json subject.params do use :shared_params, type: 'value' end subject.get '/shared_params' do { param: params[:param] } end end it 'works' do get '/shared_params' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ param: 'value' }.to_json) end end context 'with block and empty args' do before do subject.helpers do params :shared_params do |empty_args| optional :param, default: empty_args[:some] end end subject.format :json subject.params do use :shared_params end subject.get '/shared_params' do :ok end end it 'works' do get '/shared_params' expect(last_response.status).to eq(200) end end context 'all or none' do context 'optional params' do before do subject.resource :custom_message do params do optional :beer optional :wine optional :juice all_or_none_of :beer, :wine, :juice, message: 'all params are required or none is required' end get '/all_or_none' do 'all_or_none works!' end end end context 'with a custom validation message' do it 'errors when any one is present' do get '/custom_message/all_or_none', beer: 'string' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice all params are required or none is required' end it 'works when all params are present' do get '/custom_message/all_or_none', beer: 'string', wine: 'anotherstring', juice: 'anotheranotherstring' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'all_or_none works!' end it 'works when none are present' do get '/custom_message/all_or_none' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'all_or_none works!' end end end end context 'mutually exclusive' do context 'optional params' do context 'with custom validation message' do it 'errors when two or more are present' do subject.resources :custom_message do params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice, message: 'are mutually exclusive cannot pass both params' end get '/mutually_exclusive' do 'mutually_exclusive works!' end end get '/custom_message/mutually_exclusive', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive cannot pass both params' end end it 'errors when two or more are present' do subject.params do optional :beer optional :wine optional :juice mutually_exclusive :beer, :wine, :juice end subject.get '/mutually_exclusive' do 'mutually_exclusive works!' end get '/mutually_exclusive', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end context 'more than one set of mutually exclusive params' do context 'with a custom validation message' do it 'errors for all sets' do subject.resources :custom_message do params do optional :beer optional :wine mutually_exclusive :beer, :wine, message: 'are mutually exclusive pass only one' optional :nested, type: Hash do optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit, message: 'are mutually exclusive pass only one' end optional :nested2, type: Array do optional :scotch2 optional :aquavit2 mutually_exclusive :scotch2, :aquavit2, message: 'are mutually exclusive pass only one' end end get '/mutually_exclusive' do 'mutually_exclusive works!' end end get '/custom_message/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq( 'beer, wine are mutually exclusive pass only one, nested[scotch], nested[aquavit] are mutually exclusive pass only one, nested2[1][scotch2], nested2[1][aquavit2] are mutually exclusive pass only one' ) end end it 'errors for all sets' do subject.params do optional :beer optional :wine mutually_exclusive :beer, :wine optional :nested, type: Hash do optional :scotch optional :aquavit mutually_exclusive :scotch, :aquavit end optional :nested2, type: Array do optional :scotch2 optional :aquavit2 mutually_exclusive :scotch2, :aquavit2 end end subject.get '/mutually_exclusive' do 'mutually_exclusive works!' end get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive, nested[scotch], nested[aquavit] are mutually exclusive, nested2[1][scotch2], nested2[1][aquavit2] are mutually exclusive' end end context 'in a group' do it 'works when only one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice mutually_exclusive :beer, :wine, :juice end end subject.get '/mutually_exclusive_group' do 'mutually_exclusive_group works!' end get '/mutually_exclusive_group', drink: { beer: 'true' } expect(last_response.status).to eq(200) end it 'errors when more than one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice mutually_exclusive :beer, :wine, :juice end end subject.get '/mutually_exclusive_group' do 'mutually_exclusive_group works!' end get '/mutually_exclusive_group', drink: { beer: 'true', juice: 'true', wine: 'true' } expect(last_response.status).to eq(400) end end context 'mutually exclusive params inside Hash group' do it 'invalidates if request param is invalid type' do subject.params do optional :wine, type: Hash do optional :grape optional :country mutually_exclusive :grape, :country end end subject.post '/mutually_exclusive' do 'mutually_exclusive works!' end post '/mutually_exclusive', wine: '2015 sauvignon' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'wine is invalid' end end end context 'exactly one of' do context 'params' do before do subject.resources :custom_message do params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice, message: 'are missing, exactly one parameter is required' end get '/exactly_one_of' do 'exactly_one_of works!' end end subject.params do optional :beer optional :wine optional :juice exactly_one_of :beer, :wine, :juice end subject.get '/exactly_one_of' do 'exactly_one_of works!' end end context 'with a custom validation message' do it 'errors when none are present' do get '/custom_message/exactly_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter is required' end it 'succeeds when one is present' do get '/custom_message/exactly_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'exactly_one_of works!' end it 'errors when two or more are present' do get '/custom_message/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are missing, exactly one parameter is required' end end it 'errors when none are present' do get '/exactly_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter must be provided' end it 'succeeds when one is present' do get '/exactly_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'exactly_one_of works!' end it 'errors when two or more are present' do get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine are mutually exclusive' end end context 'nested params' do before do subject.params do requires :nested, type: Hash do optional :beer_nested optional :wine_nested optional :juice_nested exactly_one_of :beer_nested, :wine_nested, :juice_nested end optional :nested2, type: Array do optional :beer_nested2 optional :wine_nested2 optional :juice_nested2 exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2 end end subject.get '/exactly_one_of_nested' do 'exactly_one_of works!' end end it 'errors when none are present' do get '/exactly_one_of_nested' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'nested is missing, nested[beer_nested], nested[wine_nested], nested[juice_nested] are missing, exactly one parameter must be provided' end it 'succeeds when one is present' do get '/exactly_one_of_nested', nested: { beer_nested: 'string' } expect(last_response.status).to eq(200) expect(last_response.body).to eq 'exactly_one_of works!' end it 'errors when two or more are present' do get '/exactly_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'anotherstring' }] expect(last_response.status).to eq(400) expect(last_response.body).to eq 'nested2[0][beer_nested2], nested2[0][wine_nested2] are mutually exclusive' end end end context 'at least one of' do context 'params' do before do subject.resources :custom_message do params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice, message: 'are missing, please specify at least one param' end get '/at_least_one_of' do 'at_least_one_of works!' end end subject.params do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end subject.get '/at_least_one_of' do 'at_least_one_of works!' end end context 'with a custom validation message' do it 'errors when none are present' do get '/custom_message/at_least_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, please specify at least one param' end it 'does not error when one is present' do get '/custom_message/at_least_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end it 'does not error when two are present' do get '/custom_message/at_least_one_of', beer: 'string', wine: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end it 'errors when none are present' do get '/at_least_one_of' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'beer, wine, juice are missing, at least one parameter must be provided' end it 'does not error when one is present' do get '/at_least_one_of', beer: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end it 'does not error when two are present' do get '/at_least_one_of', beer: 'string', wine: 'string' expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end context 'nested params' do before do subject.params do requires :nested, type: Hash do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end optional :nested2, type: Array do optional :beer optional :wine optional :juice at_least_one_of :beer, :wine, :juice end end subject.get '/at_least_one_of_nested' do 'at_least_one_of works!' end end it 'errors when none are present' do get '/at_least_one_of_nested' expect(last_response.status).to eq(400) expect(last_response.body).to eq 'nested is missing, nested[beer], nested[wine], nested[juice] are missing, at least one parameter must be provided' end it 'does not error when one is present' do get '/at_least_one_of_nested', nested: { beer: 'string' }, nested2: [{ beer: 'string' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end it 'does not error when two are present' do get '/at_least_one_of_nested', nested: { beer: 'string', wine: 'string' }, nested2: [{ beer: 'string', wine: 'string' }] expect(last_response.status).to eq(200) expect(last_response.body).to eq 'at_least_one_of works!' end end end context 'in a group' do it 'works when only one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { beer: 'true' } expect(last_response.status).to eq(200) end it 'errors when no parameter from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: {} expect(last_response.status).to eq(400) end it 'errors when more than one from the set is present' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { beer: 'true', juice: 'true', wine: 'true' } expect(last_response.status).to eq(400) end it 'does not falsely think the param is there if it is provided outside the block' do subject.params do group :drink, type: Hash do optional :wine optional :beer optional :juice exactly_one_of :beer, :wine, :juice end end subject.get '/exactly_one_of_group' do 'exactly_one_of_group works!' end get '/exactly_one_of_group', drink: { foo: 'bar' }, beer: 'true' expect(last_response.status).to eq(400) end end # Ensure there is no leakage of indices between requests context 'required with a hash inside an array' do before do subject.params do requires :items, type: Array do requires :item, type: Hash do requires :name, type: String end end end subject.post '/required' do 'required works' end end let(:valid_item) { { item: { name: 'foo' } } } let(:params) do { items: [ valid_item, valid_item, {} ] } end it 'makes sure the error message is independent of the previous request' do post_with_json '/required', {} expect(last_response).to be_bad_request expect(last_response.body).to eq('items is missing, items[item][name] is missing') post_with_json '/required', params expect(last_response).to be_bad_request expect(last_response.body).to eq('items[2][item] is missing, items[2][item][name] is missing') post_with_json '/required', {} expect(last_response).to be_bad_request expect(last_response.body).to eq('items is missing, items[item][name] is missing') end end end describe 'require_validator' do subject { described_class.require_validator(short_name) } context 'when found' do let(:short_name) { :presence } it { is_expected.to be(Grape::Validations::Validators::PresenceValidator) } end context 'when not found' do let(:short_name) { :test } it 'raises an error' do expect { subject }.to raise_error(Grape::Exceptions::UnknownValidator) end end end end ================================================ FILE: spec/integration/dry_validation/dry_validation_spec.rb ================================================ # frozen_string_literal: true describe 'Dry::Schema', if: defined?(Dry::Schema) do describe 'Grape::DSL::Validations' do subject { app } let(:app) do Class.new do extend Grape::DSL::Validations extend Grape::DSL::Settings end end describe '.contract' do it 'saves the schema instance' do expect(subject.contract(Dry::Schema.Params)).to be_a Grape::Validations::ContractScope end it 'errors without params or block' do expect { subject.contract }.to raise_error(ArgumentError) end end end describe 'Grape::Validations::ContractScope' do let(:validated_params) { {} } let(:app) do vp = validated_params Class.new(Grape::API) do after_validation do vp.replace(params) end end end context 'with simple schema, pre-defined' do let(:contract) do Dry::Schema.Params do required(:number).filled(:integer) end end before do app.contract(contract) app.post('/required') end it 'coerces the parameter value one level deep' do post '/required', number: '1' expect(last_response).to be_created expect(validated_params).to eq('number' => 1) end it 'shows expected validation error' do post '/required' expect(last_response).to be_bad_request expect(last_response.body).to eq('number is missing') end end context 'with contract class' do let(:contract) do Class.new(Dry::Validation::Contract) do params do required(:number).filled(:integer) required(:name).filled(:string) end rule(:number) do key.failure('is too high') if value > 5 end end end before do app.contract(contract) app.post('/required') end it 'coerces the parameter' do post '/required', number: '1', name: '2' expect(last_response).to be_created expect(validated_params).to eq('number' => 1, 'name' => '2') end it 'shows expected validation error' do post '/required', number: '6' expect(last_response).to be_bad_request expect(last_response.body).to eq('name is missing, number is too high') end end context 'with nested schema' do before do app.contract do required(:home).hash do required(:address).hash do required(:number).filled(:integer) end end required(:turns).array(:integer) end app.post('/required') end it 'keeps unknown parameters' do post '/required', home: { address: { number: '1', street: 'Baker' } }, turns: %w[2 3] expect(last_response).to be_created expected = { 'home' => { 'address' => { 'number' => 1, 'street' => 'Baker' } }, 'turns' => [2, 3] } expect(validated_params).to eq(expected) end it 'shows expected validation error' do post '/required', home: { address: { something: 'else' } } expect(last_response).to be_bad_request expect(last_response.body).to eq('home[address][number] is missing, turns is missing') end end context 'with mixed validation sources' do before do app.resource :foos do route_param :foo_id, type: Integer do contract do required(:number).filled(:integer) end post('/required') end end end it 'combines the coercions' do post '/foos/123/required', number: '1' expect(last_response).to be_created expected = { 'foo_id' => 123, 'number' => 1 } expect(validated_params).to eq(expected) end it 'shows validation error for missing' do post '/foos/123/required' expect(last_response).to be_bad_request expect(last_response.body).to eq('number is missing') end it 'includes keys from all sources into declared' do declared_params = nil app.after_validation do declared_params = declared(params) end post '/foos/123/required', number: '1', string: '2' expect(last_response).to be_created expected = { 'foo_id' => 123, 'number' => 1 } expect(validated_params).to eq(expected.merge('string' => '2')) expect(declared_params).to eq(expected) end end context 'with schema config validate_keys=true' do it 'validates the whole params hash' do app.resource :foos do route_param :foo_id do contract do config.validate_keys = true required(:number).filled(:integer) required(:foo_id).filled(:integer) end post('/required') end end post '/foos/123/required', number: '1' expect(last_response).to be_created expected = { 'foo_id' => 123, 'number' => 1 } expect(validated_params).to eq(expected) end it 'fails validation for any parameters not in schema' do app.resource :foos do route_param :foo_id, type: Integer do contract do config.validate_keys = true required(:number).filled(:integer) end post('/required') end end post '/foos/123/required', number: '1' expect(last_response).to be_bad_request expect(last_response.body).to eq('foo_id is not allowed') end end end end ================================================ FILE: spec/integration/grape_entity/entity_spec.rb ================================================ # frozen_string_literal: true require 'rack/contrib/jsonp' describe 'Grape::Entity', if: defined?(Grape::Entity) do describe '#present' do subject { Class.new(Grape::API) } let(:app) { subject } before do stub_const('TestObject', Class.new) stub_const('FakeCollection', Class.new do def first TestObject.new end end) end it 'sets the object as the body if no options are provided' do inner_body = nil subject.get '/example' do present({ abc: 'def' }) inner_body = body end get '/example' expect(inner_body).to eql(abc: 'def') end it 'pulls a representation from the class options if it exists' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subject.represent Object, with: entity subject.get '/example' do present Object.new end get '/example' expect(last_response.body).to eq('Hiya') end it 'pulls a representation from the class options if the presented object is a collection of objects' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subject.represent TestObject, with: entity subject.get '/example' do present [TestObject.new] end subject.get '/example2' do present FakeCollection.new end get '/example' expect(last_response.body).to eq('Hiya') get '/example2' expect(last_response.body).to eq('Hiya') end it 'pulls a representation from the class ancestor if it exists' do entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Hiya') subclass = Class.new(Object) subject.represent Object, with: entity subject.get '/example' do present subclass.new end get '/example' expect(last_response.body).to eq('Hiya') end it 'automatically uses Klass::Entity if that exists' do some_model = Class.new entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity subject.get '/example' do present some_model.new end get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'automatically uses Klass::Entity based on the first object in the collection being presented' do some_model = Class.new entity = Class.new(Grape::Entity) allow(entity).to receive(:represent).and_return('Auto-detect!') some_model.const_set :Entity, entity subject.get '/example' do present [some_model.new] end get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'does not run autodetection for Entity when explicitly provided' do entity = Class.new(Grape::Entity) some_array = [] subject.get '/example' do present some_array, with: entity end expect(some_array).not_to receive(:first) get '/example' end it 'does not use #first method on ActiveRecord::Relation to prevent needless sql query' do entity = Class.new(Grape::Entity) some_relation = Class.new some_model = Class.new allow(entity).to receive(:represent).and_return('Auto-detect!') allow(some_relation).to receive(:first) allow(some_relation).to receive(:klass).and_return(some_model) some_model.const_set :Entity, entity subject.get '/example' do present some_relation end expect(some_relation).not_to receive(:first) get '/example' expect(last_response.body).to eq('Auto-detect!') end it 'autodetection does not use Entity if it is not a presenter' do some_model = Class.new entity = Class.new some_model.class.const_set :Entity, entity subject.get '/example' do present some_model end get '/example' expect(entity).not_to receive(:represent) end it 'adds a root key to the output if one is given' do inner_body = nil subject.get '/example' do present({ abc: 'def' }, root: :root) inner_body = body end get '/example' expect(inner_body).to eql(root: { abc: 'def' }) end %i[json serializable_hash].each do |format| it "presents with #{format}" do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id subject.format format subject.get '/example' do c = Class.new do attr_reader :id def initialize(id) @id = id end end present c.new(1), with: entity end get '/example' expect(last_response).to be_successful expect(last_response.body).to eq('{"example":{"id":1}}') end it "presents with #{format} collection" do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :id subject.format format subject.get '/examples' do c = Class.new do attr_reader :id def initialize(id) @id = id end end examples = [c.new(1), c.new(2)] present examples, with: entity end get '/examples' expect(last_response).to be_successful expect(last_response.body).to eq('{"examples":[{"id":1},{"id":2}]}') end end it 'presents with xml' do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name subject.format :xml subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example' expect(last_response).to be_successful expect(last_response.content_type).to eq('application/xml') expect(last_response.body).to eq <<~XML johnnyiller XML end it 'presents with json' do entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name subject.format :json subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example' expect(last_response).to be_successful expect(last_response.content_type).to eq('application/json') expect(last_response.body).to eq('{"example":{"name":"johnnyiller"}}') end it 'presents with jsonp utilising Rack::JSONP' do subject.use Rack::JSONP entity = Class.new(Grape::Entity) entity.root 'examples', 'example' entity.expose :name # Rack::JSONP expects a standard JSON response in UTF-8 format subject.format :json subject.formatter :json, lambda { |object, _| object.to_json.encode('utf-8') } subject.get '/example' do c = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end present c.new(name: 'johnnyiller'), with: entity end get '/example?callback=abcDef' expect(last_response).to be_successful expect(last_response.content_type).to eq('application/javascript') expect(last_response.body).to include 'abcDef({"example":{"name":"johnnyiller"}})' end context 'present with multiple entities' do it 'present with multiple entities using optional symbol' do user = Class.new do attr_reader :name def initialize(args) @name = args[:name] || 'no name set' end end user1 = user.new(name: 'user1') user2 = user.new(name: 'user2') entity = Class.new(Grape::Entity) entity.expose :name subject.format :json subject.get '/example' do present :page, 1 present :user1, user1, with: entity present :user2, user2, with: entity end get '/example' expect_response_json = { 'page' => 1, 'user1' => { 'name' => 'user1' }, 'user2' => { 'name' => 'user2' } } expect(JSON(last_response.body)).to eq(expect_response_json) end end end describe 'Grape::Middleware::Error' do let(:error_entity) do Class.new(Grape::Entity) do expose :code expose :static def static 'static text' end end end let(:options) { { default_message: 'Aww, hamburgers.' } } let(:error_app) do Class.new do class << self attr_accessor :error, :format def call(_env) throw :error, error end end end end let(:app) do opts = options Rack::Builder.app do use Spec::Support::EndpointFaker use Grape::Middleware::Error, **opts run ErrApp end end before do stub_const('ErrApp', error_app) stub_const('ErrorEntity', error_entity) end context 'with http code' do it 'presents an error message' do ErrApp.error = { message: { code: 200, with: ErrorEntity } } get '/' expect(last_response.body).to eq({ code: 200, static: 'static text' }.to_json) end end end describe 'error_presenter' do subject { last_response } let(:error_presenter) do Class.new(Grape::Entity) do expose :code expose :static def static 'some static text' end end end before do stub_const('ErrorPresenter', error_presenter) get '/exception' end context 'when using http_codes' do let(:app) do Class.new(Grape::API) do desc 'some desc', http_codes: [[408, 'Unauthorized', ErrorPresenter]] get '/exception' do error!({ code: 408 }, 408) end end end it 'is used as presenter' do expect(subject).to be_request_timeout expect(subject.body).to eql({ code: 408, static: 'some static text' }.to_json) end end context 'when using with' do let(:app) do Class.new(Grape::API) do get '/exception' do error!({ code: 408, with: ErrorPresenter }, 408) end end end it 'presented with' do expect(subject).to be_request_timeout expect(subject.body).to eql({ code: 408, static: 'some static text' }.to_json) end end end end ================================================ FILE: spec/integration/hashie/hashie_spec.rb ================================================ # frozen_string_literal: true describe 'Hashie', if: defined?(Hashie) do subject { app } let(:app) { Class.new(Grape::API) } describe 'Grape::ParamsBuilder::HashieMash' do describe 'in an endpoint' do describe '#params' do before do subject.params do build_with :hashie_mash end subject.get do params.class end end it 'is of type Hashie::Mash' do get '/' expect(last_response).to be_successful expect(last_response.body).to eq('Hashie::Mash') end end end describe 'in an api' do before do subject.build_with :hashie_mash end describe '#params' do before do subject.get do params.class end end it 'is Hashie::Mash' do get '/' expect(last_response).to be_successful expect(last_response.body).to eq('Hashie::Mash') end end context 'in a nested namespace api' do before do subject.namespace :foo do get do params.class end end end it 'is Hashie::Mash' do get '/foo' expect(last_response).to be_successful expect(last_response.body).to eq('Hashie::Mash') end end it 'is indifferent to key or symbol access' do subject.params do build_with :hashie_mash requires :a, type: String end subject.get '/foo' do [params[:a], params['a']] end get '/foo', a: 'bar' expect(last_response).to be_successful expect(last_response.body).to eq('["bar", "bar"]') end it 'does not overwrite route_param with a regular param if they have same name' do subject.namespace :route_param do route_param :foo do get { params.to_json } end end get '/route_param/bar', foo: 'baz' expect(last_response).to be_successful expect(last_response.body).to eq('{"foo":"bar"}') end it 'does not overwrite route_param with a defined regular param if they have same name' do subject.namespace :route_param do params do build_with :hashie_mash requires :foo, type: String end route_param :foo do get do [params[:foo], params['foo']] end end end get '/route_param/bar', foo: 'baz' expect(last_response).to be_successful expect(last_response.body).to eq('["bar", "bar"]') end end end describe 'Grape::Request' do let(:default_method) { Rack::GET } let(:default_params) { {} } let(:default_options) do { method: method, params: params } end let(:default_env) do Rack::MockRequest.env_for('/', options) end let(:method) { default_method } let(:params) { default_params } let(:options) { default_options } let(:env) { default_env } let(:request) { Grape::Request.new(env) } describe '#params' do let(:params) do { a: '123', b: 'xyz' } end it 'by default returns stringified parameter keys' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new('a' => '123', 'b' => 'xyz')) end context 'when build_params_with: Grape::Extensions::Hash::ParamBuilder is specified' do let(:request) { Grape::Request.new(env, build_params_with: :hash) } it 'returns symbolized params' do expect(request.params).to eq(a: '123', b: 'xyz') end end describe 'with grape.routing_args' do let(:options) do default_options.merge('grape.routing_args' => routing_args) end let(:routing_args) do { version: '123', route_info: '456', c: 'ccc' } end it 'cuts version and route_info' do expect(request.params).to eq(ActiveSupport::HashWithIndifferentAccess.new(a: '123', b: 'xyz', c: 'ccc')) end end end describe 'when the build_params_with is set to Hashie' do subject(:request_params) { Grape::Request.new(env, build_params_with: :hashie_mash).params } context 'when the API includes a specific param builder' do it { is_expected.to be_a(Hashie::Mash) } end end end describe 'Grape::Validations::Validators::CoerceValidator' do context 'when params is Hashie::Mash' do context 'for primitive collections' do before do subject.params do build_with :hashie_mash optional :a, types: [String, Array[String]] optional :b, types: [Array[Integer], Array[String]] optional :c, type: Array[Integer, String] optional :d, types: [Integer, String, Set[Integer, String]] end subject.get '/' do ( params.a || params.b || params.c || params.d ).inspect end end it 'allows singular form declaration' do get '/', a: 'one way' expect(last_response).to be_successful expect(last_response.body).to eq('"one way"') get '/', a: %w[the other] expect(last_response).to be_successful expect(last_response.body).to eq('#') get '/', a: { a: 1, b: 2 } expect(last_response).to be_bad_request expect(last_response.body).to eq('a is invalid') get '/', a: [1, 2, 3] expect(last_response).to be_successful expect(last_response.body).to eq('#') end it 'allows multiple collection types' do get '/', b: [1, 2, 3] expect(last_response).to be_successful expect(last_response.body).to eq('#') get '/', b: %w[1 2 3] expect(last_response).to be_successful expect(last_response.body).to eq('#') get '/', b: [1, true, 'three'] expect(last_response).to be_successful expect(last_response.body).to eq('#') end it 'allows collections with multiple types' do get '/', c: [1, '2', true, 'three'] expect(last_response).to be_successful expect(last_response.body).to eq('#') get '/', d: '1' expect(last_response).to be_successful expect(last_response.body).to eq('1') get '/', d: 'one' expect(last_response).to be_successful expect(last_response.body).to eq('"one"') get '/', d: %w[1 two] expect(last_response).to be_successful json_set = JSON.parse([1, 'two'].to_set.to_json) expect(last_response.body).to eq(json_set) end end end end describe 'Grape::Endpoint' do before do subject.format :json subject.params do requires :first optional :second optional :third, default: 'third-default' optional :multiple_types, types: [Integer, String] optional :nested, type: Hash do optional :fourth optional :fifth optional :nested_two, type: Hash do optional :sixth optional :nested_three, type: Hash do optional :seventh end end optional :nested_arr, type: Array do optional :eighth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end optional :arr, type: Array do optional :nineth end optional :empty_arr, type: Array optional :empty_typed_arr, type: Array[String] optional :empty_hash, type: Hash optional :empty_hash_two, type: Hash optional :empty_set, type: Set optional :empty_typed_set, type: Set[String] end end context 'when params are not built with default class' do it 'returns an object that corresponds with the params class - hashie mash' do subject.params do build_with :hashie_mash end subject.get '/declared' do d = declared(params, include_missing: true) { declared_class: d.class.to_s } end get '/declared?first=present' expect(JSON.parse(last_response.body)['declared_class']).to eq('Hashie::Mash') end end end end ================================================ FILE: spec/integration/multi_json/json_spec.rb ================================================ # frozen_string_literal: true # grape_entity depends on multi-json and it breaks the test. describe Grape::Json, if: defined?(MultiJson) && !defined?(Grape::Entity) do subject { described_class } it { is_expected.to eq(MultiJson) } end ================================================ FILE: spec/integration/multi_xml/xml_spec.rb ================================================ # frozen_string_literal: true describe Grape::Xml, if: defined?(MultiXml) do subject { described_class } it { is_expected.to eq(MultiXml) } end ================================================ FILE: spec/integration/rails/mounting_spec.rb ================================================ # frozen_string_literal: true describe 'Rails', if: defined?(Rails) do context 'rails mounted' do let(:api) do Class.new(Grape::API) do lint! get('/test_grape') { 'rails mounted' } end end let(:app) do require 'rails' require 'action_controller/railtie' # https://github.com/rails/rails/issues/51784 # same error as described if not redefining the following ActiveSupport::Dependencies.autoload_paths = [] ActiveSupport::Dependencies.autoload_once_paths = [] Class.new(Rails::Application) do config.eager_load = false config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" config.api_only = true config.consider_all_requests_local = true config.hosts << 'example.org' routes.append do mount GrapeApi => '/' get 'up', to: lambda { |_env| [200, {}, ['hello world']] } end end end before do stub_const('GrapeApi', api) app.initialize! end it 'cascades' do get '/test_grape' expect(last_response).to be_successful expect(last_response.body).to eq('rails mounted') get '/up' expect(last_response).to be_successful expect(last_response.body).to eq('hello world') end end end ================================================ FILE: spec/integration/rails/railtie_spec.rb ================================================ # frozen_string_literal: true if defined?(Rails) describe Grape::Railtie do describe '.railtie' do subject { test_app.deprecators[:grape] } let(:test_app) do # https://github.com/rails/rails/issues/51784 # same error as described if not redefining the following ActiveSupport::Dependencies.autoload_paths = [] ActiveSupport::Dependencies.autoload_once_paths = [] Class.new(Rails::Application) do config.eager_load = false config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" end end before { test_app.initialize! } it { is_expected.to be(Grape.deprecator) } end end end ================================================ FILE: spec/shared/versioning_examples.rb ================================================ # frozen_string_literal: true shared_examples_for 'versioning' do it 'sets the API version' do subject.format :txt subject.version 'v1', **macro_options subject.get :hello do "Version: #{request.env[Grape::Env::API_VERSION]}" end versioned_get '/hello', 'v1', macro_options expect(last_response.body).to eql 'Version: v1' end it 'adds the prefix before the API version' do subject.format :txt subject.prefix 'api' subject.version 'v1', **macro_options subject.get :hello do "Version: #{request.env[Grape::Env::API_VERSION]}" end versioned_get '/hello', 'v1', macro_options.merge(prefix: 'api') expect(last_response.body).to eql 'Version: v1' end it 'is able to specify version as a nesting' do subject.version 'v2', **macro_options subject.get '/awesome' do 'Radical' end subject.version 'v1', **macro_options do get '/legacy' do 'Totally' end end versioned_get '/awesome', 'v1', macro_options expect(last_response.status).to be 404 versioned_get '/awesome', 'v2', macro_options expect(last_response.status).to be 200 versioned_get '/legacy', 'v1', macro_options expect(last_response.status).to be 200 versioned_get '/legacy', 'v2', macro_options expect(last_response.status).to be 404 end it 'is able to specify multiple versions' do subject.version 'v1', 'v2', **macro_options subject.get 'awesome' do 'I exist' end versioned_get '/awesome', 'v1', macro_options expect(last_response.status).to be 200 versioned_get '/awesome', 'v2', macro_options expect(last_response.status).to be 200 versioned_get '/awesome', 'v3', macro_options expect(last_response.status).to be 404 end context 'with different versions for the same endpoint' do context 'without a prefix' do it 'allows the same endpoint to be implemented' do subject.format :txt subject.version 'v2', **macro_options subject.get 'version' do request.env[Grape::Env::API_VERSION] end subject.version 'v1', **macro_options do get 'version' do "version #{request.env[Grape::Env::API_VERSION]}" end end versioned_get '/version', 'v2', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') versioned_get '/version', 'v1', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('version v1') end end context 'with a prefix' do it 'allows the same endpoint to be implemented' do subject.format :txt subject.prefix 'api' subject.version 'v2', **macro_options subject.get 'version' do request.env[Grape::Env::API_VERSION] end subject.version 'v1', **macro_options do get 'version' do "version #{request.env[Grape::Env::API_VERSION]}" end end versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('version v1') versioned_get '/version', 'v2', macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') end end end context 'with before block defined within a version block' do it 'calls before block that is defined within the version block' do subject.format :txt subject.prefix 'api' subject.version 'v2', **macro_options do before do @output ||= 'v2-' end get 'version' do @output += 'version' end end subject.version 'v1', **macro_options do before do @output ||= 'v1-' end get 'version' do @output += 'version' end end versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v1-version') versioned_get '/version', 'v2', macro_options.merge(prefix: subject.prefix) expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2-version') end end it 'does not overwrite version parameter with API version' do subject.format :txt subject.version 'v1', **macro_options subject.params { requires :version } subject.get :api_version_with_version_param do params[:version] end versioned_get '/api_version_with_version_param?version=1', 'v1', macro_options expect(last_response.body).to eql '1' end context 'with catch-all' do let(:options) { macro_options } let(:v1) do klass = Class.new(Grape::API) klass.version 'v1', **options klass.get 'version' do 'v1' end klass end let(:v2) do klass = Class.new(Grape::API) klass.version 'v2', **options klass.get 'version' do 'v2' end klass end before do subject.format :txt subject.mount v1 subject.mount v2 subject.route :any, '*path' do params[:path] end end context 'v1' do it 'finds endpoint' do versioned_get '/version', 'v1', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v1') end it 'finds catch all' do versioned_get '/whatever', 'v1', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end end context 'v2' do it 'finds endpoint' do versioned_get '/version', 'v2', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('v2') end it 'finds catch all' do versioned_get '/whatever', 'v2', macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require 'simplecov' require 'rubygems' require 'bundler' Bundler.require :default, :test Grape.deprecator.behavior = :raise %w[support].each do |dir| Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].each do |file| require file end end Grape.config.lint = true # lint all apis by default Grape::Util::Registry.include(Deregister) # The default value for this setting is true in a standard Rails app, # so it should be set to true here as well to reflect that. I18n.enforce_available_locales = true RSpec.configure do |config| config.include Rack::Test::Methods config.include Spec::Support::Helpers config.raise_errors_for_deprecations! config.filter_run_when_matching :focus config.before(:all) { Grape::Util::InheritableSetting.reset_global! } config.before { Grape::Util::InheritableSetting.reset_global! } # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' end ================================================ FILE: spec/support/basic_auth_encode_helpers.rb ================================================ # frozen_string_literal: true module Spec module Support module Helpers def encode_basic_auth(username, password) "Basic #{Base64.encode64("#{username}:#{password}")}" end end end end ================================================ FILE: spec/support/chunked_response.rb ================================================ # frozen_string_literal: true # this is a copy of Rack::Chunked which has been removed in rack > 3.0 class ChunkedResponse class Body TERM = "\r\n" TAIL = "0#{TERM}".freeze # Store the response body to be chunked. def initialize(body) @body = body end # For each element yielded by the response body, yield # the element in chunked encoding. def each(&) term = TERM @body.each do |chunk| size = chunk.bytesize next if size == 0 yield [size.to_s(16), term, chunk.b, term].join end yield TAIL yield_trailers(&) yield term end # Close the response body if the response body supports it. def close @body.close if @body.respond_to?(:close) end private # Do nothing as this class does not support trailer headers. def yield_trailers; end end class TrailerBody < Body private # Yield strings for each trailer header. def yield_trailers @body.trailers.each_pair do |k, v| yield "#{k}: #{v}\r\n" end end end def initialize(app) @app = app end def call(env) status, headers, body = response = @app.call(env) if !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[Rack::CONTENT_LENGTH] && !headers['Transfer-Encoding'] headers['Transfer-Encoding'] = 'chunked' response[2] = if headers['trailer'] TrailerBody.new(body) else Body.new(body) end end response end end ================================================ FILE: spec/support/content_type_helpers.rb ================================================ # frozen_string_literal: true module Spec module Support module Helpers %w[put patch post delete].each do |method| define_method :"#{method}_with_json" do |uri, params = {}, env = {}, &block| params = params.to_json env['CONTENT_TYPE'] ||= 'application/json' __send__(method, uri, params, env, &block) end end end end end ================================================ FILE: spec/support/cookie_jar.rb ================================================ # frozen_string_literal: true require 'uri' module Spec module Support # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie class CookieJar attr_reader :attributes def initialize(raw) @attributes = raw.split(/;\s*/).flat_map.with_index do |attribute, i| attribute, value = attribute.split('=', 2) if i.zero? [['name', attribute], ['value', unescape(value)]] else [[attribute.downcase, parse_value(attribute, value)]] end end.to_h.freeze end def to_h @attributes.dup end def to_s @attributes.to_s end private def unescape(value) URI.decode_www_form_component(value, Encoding::UTF_8) end def parse_value(attribute, value) case attribute when 'expires' Time.parse(value) when 'max-age' value.to_i when 'secure', 'httponly', 'partitioned' true else unescape(value) end end end end end module Rack class MockResponse def cookie_jar @cookie_jar ||= Array(headers[Rack::SET_COOKIE]).flat_map { |h| h.split("\n") }.map { |c| Spec::Support::CookieJar.new(c).to_h } end end end ================================================ FILE: spec/support/deprecated_warning_handlers.rb ================================================ # frozen_string_literal: true Warning[:deprecated] = true module DeprecatedWarningHandler class DeprecationWarning < StandardError; end DEPRECATION_REGEX = /is deprecated/ def warn(message) return super unless message.match?(DEPRECATION_REGEX) exception = DeprecationWarning.new(message) exception.set_backtrace(caller) raise exception end end Warning.singleton_class.prepend(DeprecatedWarningHandler) ================================================ FILE: spec/support/deregister.rb ================================================ # frozen_string_literal: true module Deregister def deregister(key) registry.delete(key) end end ================================================ FILE: spec/support/endpoint_faker.rb ================================================ # frozen_string_literal: true module Spec module Support class EndpointFaker class FakerAPI < Grape::API get('/') end def initialize(app, endpoint = FakerAPI.endpoints.first) @app = app @endpoint = endpoint end def call(env) @endpoint.instance_exec do @request = Grape::Request.new(env.dup) end @app.call(env.merge(Grape::Env::API_ENDPOINT => @endpoint)) end end end end ================================================ FILE: spec/support/file_streamer.rb ================================================ # frozen_string_literal: true class FileStreamer def initialize(file_path) @file_path = file_path end def each(&blk) File.open(@file_path, 'rb') do |file| file.each(10, &blk) end end end ================================================ FILE: spec/support/integer_helpers.rb ================================================ # frozen_string_literal: true module Spec module Support module Helpers INTEGER_CLASS_NAME = 0.class.to_s.freeze def integer_class_name INTEGER_CLASS_NAME end end end end ================================================ FILE: spec/support/versioned_helpers.rb ================================================ # frozen_string_literal: true # Versioning module Spec module Support module Helpers # Returns the path with options[:version] prefixed if options[:using] is :path. # Returns normal path otherwise. def versioned_path(options) case options[:using] when :path File.join('/', options[:prefix] || '', options[:version], options[:path]) when :param, :header, :accept_version_header File.join('/', options[:prefix] || '', options[:path]) else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") end end def versioned_headers(options) case options[:using] when :path, :param {} when :header { 'HTTP_ACCEPT' => [ "application/vnd.#{options[:vendor]}-#{options[:version]}", options[:format] ].compact.join('+') } when :accept_version_header { 'HTTP_ACCEPT_VERSION' => options[:version].to_s } else raise ArgumentError.new("unknown versioning strategy: #{options[:using]}") end end def versioned_get(path, version_name, version_options) path = versioned_path(version_options.merge(version: version_name, path: path)) headers = versioned_headers(version_options.merge(version: version_name)) params = {} params = { version_options[:parameter] => version_name } if version_options[:using] == :param get path, params, headers end end end end