diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..6768fc5a6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "rubygems/grape" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 000000000..040beda21 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,21 @@ +--- +name: danger +on: + pull_request: + types: [opened, reopened, edited, synchronize] +jobs: + danger: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v1 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true + - name: Run Danger + run: | + # the token is public, this is ok + TOKEN='b8b19daa0ade737762c' + TOKEN+='f35edcb328642d371ce86' + DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..44a7541f9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,91 @@ +--- +name: test +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" +jobs: + lint: + name: RuboCop + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - name: Run RuboCop + run: bundle exec rubocop + test: + strategy: + fail-fast: false + matrix: + ruby: + - 2.5 + - 2.6 + - 2.7 + - 3.0 + gemfile: + - Gemfile + - gemfiles/rack1.gemfile + - gemfiles/rack2.gemfile + - gemfiles/rack2_2.gemfile + - gemfiles/rack_edge.gemfile + - gemfiles/rails_5.gemfile + - gemfiles/rails_6.gemfile + - gemfiles/rails_6_1.gemfile + experimental: [false] + include: + - ruby: 3.0 + gemfile: 'gemfiles/multi_json.gemfile' + experimental: false + - ruby: 3.0 + gemfile: 'gemfiles/multi_xml.gemfile' + experimental: false + - ruby: 2.7 + gemfile: 'gemfiles/multi_json.gemfile' + experimental: false + - ruby: 2.7 + gemfile: 'gemfiles/multi_xml.gemfile' + experimental: false + - ruby: 2.7 + gemfile: 'gemfiles/rails_edge.gemfile' + experimental: false + - ruby: "ruby-head" + experimental: true + - ruby: "truffleruby-head" + experimental: true + - ruby: "jruby-head" + experimental: true + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.experimental }} + env: + BUNDLE_GEMFILE: ${{ matrix.gemfile }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Run tests + run: bundle exec rake spec + + - name: Run tests (spec/integration/eager_load) + if: ${{ matrix.gemfile == 'Gemfile' }} + run: bundle exec rspec spec/integration/eager_load + + - name: Run tests (spec/integration/multi_json) + if: ${{ matrix.gemfile == 'gemfiles/multi_json.gemfile' }} + run: bundle exec rspec spec/integration/multi_json + + - name: Run tests (spec/integration/multi_xml) + if: ${{ matrix.gemfile == 'gemfiles/multi_xml.gemfile' }} + run: bundle exec rspec spec/integration/multi_xml diff --git a/.gitignore b/.gitignore index e139fdf5b..9e152a5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ pkg .rvmrc .ruby-version .ruby-gemset +.rspec_status .bundle .byebug_history dist diff --git a/.rspec b/.rspec index d3ad5a94f..87d9ba441 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,3 @@ --color --format=documentation +--order=rand diff --git a/.rubocop.yml b/.rubocop.yml index 9e43b4527..ee4a91c64 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,13 +1,14 @@ AllCops: - TargetRubyVersion: 2.1 - Include: - - Dangerfile - - gemfiles/*.gemfile - + NewCops: enable + TargetRubyVersion: 2.4 + SuggestExtensions: false Exclude: - vendor/**/* - bin/**/* +require: + - rubocop-performance + inherit_from: .rubocop_todo.yml Style/Documentation: @@ -19,8 +20,14 @@ Style/MultilineIfModifier: Style/RaiseArgs: Enabled: false -Lint/UnneededDisable: - Enabled: false +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true Metrics/BlockLength: Exclude: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8052a8a6d..4320b3937 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,128 +1,457 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-12-10 01:15:22 +0900 using RuboCop version 0.51.0. +# on 2020-12-26 22:10:33 UTC using RuboCop version 1.7.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: 5 +# Cop supports --auto-correct. +Layout/ClosingHeredocIndentation: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + +# Offense count: 73 +# Cop supports --auto-correct. +Layout/EmptyLineAfterGuardClause: + Enabled: false + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines. +Layout/EmptyLineBetweenDefs: + Exclude: + - 'spec/grape/api_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_brackets +Layout/FirstArrayElementIndentation: + Exclude: + - 'spec/grape/validations_spec.rb' + +# Offense count: 27 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'grape.gemspec' + - 'lib/grape/validations/params_scope.rb' + - 'lib/grape/validations/types/primitive_coercer.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + # Offense count: 7 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: +Layout/HeredocIndentation: Exclude: - 'lib/grape/router/route.rb' - 'spec/grape/api_spec.rb' - 'spec/grape/entity_spec.rb' +# Offense count: 9 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineArrayBraceLayout: + Exclude: + - 'spec/grape/validations_spec.rb' + +# Offense count: 13 +# Cop supports --auto-correct. +Layout/SpaceBeforeBrackets: + Exclude: + - 'spec/grape/api_remount_spec.rb' + - 'spec/grape/dsl/desc_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/exceptions/invalid_accept_header_spec.rb' + +# Offense count: 71 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideHashLiteralBraces: + Exclude: + - 'spec/grape/validations_spec.rb' + # Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'spec/grape/validations_spec.rb' + +# Offense count: 1 Lint/AmbiguousBlockAssociation: Exclude: - - 'lib/grape/middleware/stack.rb' - 'spec/grape/dsl/routing_spec.rb' +# Offense count: 54 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Enabled: false + +# Offense count: 5 +# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. +Lint/DuplicateBranch: + Exclude: + - 'lib/grape/extensions/deep_symbolize_hash.rb' + - 'spec/support/versioned_helpers.rb' + +# Offense count: 85 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Enabled: false + +# Offense count: 6 +# Configuration parameters: AllowComments. +Lint/EmptyClass: + Exclude: + - 'lib/grape/dsl/parameters.rb' + - 'lib/grape/validations/types.rb' + - 'spec/grape/api_spec.rb' + - 'spec/grape/entity_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + +# Offense count: 9 +Lint/MissingSuper: + Exclude: + - 'lib/grape/api.rb' + - 'lib/grape/api/instance.rb' + - 'lib/grape/exceptions/base.rb' + - 'lib/grape/exceptions/validation_array_errors.rb' + - 'lib/grape/namespace.rb' + - 'lib/grape/path.rb' + - 'lib/grape/router/pattern.rb' + - 'lib/grape/validations/validators/base.rb' + # Offense count: 1 -Lint/RescueWithoutErrorClass: +# Cop supports --auto-correct. +Lint/NonDeterministicRequireOrder: + Exclude: + - 'spec/spec_helper.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Lint/ToJSON: + Exclude: + - 'spec/grape/middleware/formatter_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowComments. +Lint/UselessMethodDefinition: Exclude: - 'lib/grape/validations/validators/coerce.rb' -# Offense count: 49 +# Offense count: 42 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 44 + Max: 47 -# Offense count: 282 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 6 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine Metrics/BlockLength: - Max: 3125 + Max: 182 -# Offense count: 9 -# Configuration parameters: CountComments. +# Offense count: 11 +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 288 + Max: 304 -# Offense count: 32 +# Offense count: 30 +# Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: - Max: 14 - -# Offense count: 1159 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 215 + Max: 17 -# Offense count: 57 -# Configuration parameters: CountComments. +# Offense count: 71 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: - Max: 33 + Max: 32 -# Offense count: 11 -# Configuration parameters: CountComments. +# Offense count: 12 +# Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 212 + Max: 220 -# Offense count: 21 +# Offense count: 1 +# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. +Metrics/ParameterLists: + Exclude: + - 'lib/grape/middleware/error.rb' + +# Offense count: 27 +# Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: - Max: 14 + Max: 18 # Offense count: 4 -# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Naming/FileName: +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: Exclude: - - 'Appraisals' - - 'Gemfile' - - 'Guardfile' - - 'Rakefile' + - 'lib/grape/api/instance.rb' + - 'lib/grape/config.rb' + - 'lib/grape/middleware/base.rb' + - 'spec/grape/integration/rack_spec.rb' -# Offense count: 1 -# Configuration parameters: Blacklist. -# Blacklist: END, (?-mix:EO[A-Z]{1}) -Naming/HeredocDelimiterNaming: +# Offense count: 5 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to +Naming/MethodParameterName: Exclude: - - 'lib/grape/router/route.rb' + - 'lib/grape/endpoint.rb' + - 'lib/grape/middleware/error.rb' + - 'lib/grape/middleware/stack.rb' + - 'spec/grape/api_spec.rb' + +# Offense count: 18 +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 +Naming/VariableNumber: + Exclude: + - 'spec/grape/dsl/settings_spec.rb' + - 'spec/grape/exceptions/validation_errors_spec.rb' + - 'spec/grape/validations_spec.rb' # Offense count: 3 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect. -Performance/HashEachMethods: +Performance/BigDecimalWithNumericArgument: Exclude: - - 'lib/grape/api.rb' - - 'lib/grape/middleware/versioner/header.rb' + - 'spec/grape/validations/types/primitive_coercer_spec.rb' -# Offense count: 1 +# Offense count: 21 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. -# SupportedStyles: line_count_based, semantic, braces_for_chaining -# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object -# FunctionalMethods: let, let!, subject, watch -# IgnoredMethods: lambda, proc, it -Style/BlockDelimiters: +Performance/BlockGivenWithExplicitBlock: + Exclude: + - 'lib/grape/api/instance.rb' + - 'lib/grape/dsl/desc.rb' + - 'lib/grape/dsl/helpers.rb' + - 'lib/grape/dsl/middleware.rb' + - 'lib/grape/dsl/parameters.rb' + - 'lib/grape/dsl/request_response.rb' + - 'lib/grape/dsl/routing.rb' + - 'lib/grape/dsl/settings.rb' + - 'lib/grape/endpoint.rb' + - 'lib/grape/validations/params_scope.rb' + +# Offense count: 2 +# Configuration parameters: MinSize. +Performance/CollectionLiteralInLoop: Exclude: + - 'spec/grape/api_spec.rb' - 'spec/grape/middleware/formatter_spec.rb' +# Offense count: 3 +# Cop supports --auto-correct. +Performance/InefficientHashSearch: + Exclude: + - 'spec/grape/validations/validators/values_spec.rb' + # Offense count: 1 -Style/CommentedKeyword: +Performance/MethodObjectAsBlock: Exclude: - - 'spec/grape/validations_spec.rb' + - 'lib/grape/middleware/stack.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: separated, grouped +Style/AccessorGrouping: + Exclude: + - 'lib/grape/api/instance.rb' + - 'lib/grape/exceptions/validation.rb' + - 'lib/grape/util/inheritable_setting.rb' + - 'spec/grape/middleware/error_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Style/CaseLikeIf: + Exclude: + - 'lib/grape/util/lazy_value.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' # Offense count: 2 -# Configuration parameters: SupportedStyles. -# SupportedStyles: annotated, template +# Cop supports --auto-correct. +# Configuration parameters: IgnoredMethods. +# IgnoredMethods: ==, equal?, eql? +Style/ClassEqualityComparison: + Exclude: + - 'lib/grape/validations/types/dry_type_coercer.rb' + - 'lib/grape/validations/validators/coerce.rb' + +# Offense count: 1 +Style/CombinableLoops: + Exclude: + - 'spec/grape/endpoint_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: Keywords. +# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE +Style/CommentAnnotation: + Exclude: + - 'spec/grape/api_spec.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +Style/EmptyLambdaParameter: + Exclude: + - 'spec/grape/dsl/callbacks_spec.rb' + - 'spec/grape/dsl/middleware_spec.rb' + - 'spec/grape/dsl/routing_spec.rb' + - 'spec/grape/middleware/auth/dsl_spec.rb' + - 'spec/grape/middleware/stack_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/ExpandPathArguments: + Exclude: + - 'grape.gemspec' + - 'lib/grape.rb' + - 'spec/grape/validations/validators/coerce_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/ExplicitBlockArgument: + Exclude: + - 'lib/grape/middleware/stack.rb' + +# Offense count: 2 +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed. +# SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: template -# Offense count: 2 -Style/IdenticalConditionalBranches: +# Offense count: 1 +# Cop supports --auto-correct. +Style/GlobalStdStream: Exclude: + - 'benchmark/remounting.rb' + +# Offense count: 19 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Exclude: + - 'lib/grape/api/instance.rb' - 'lib/grape/dsl/desc.rb' + - 'lib/grape/dsl/request_response.rb' + - 'lib/grape/dsl/settings.rb' + - 'lib/grape/endpoint.rb' + - 'lib/grape/error_formatter/json.rb' + - 'lib/grape/error_formatter/xml.rb' + - 'lib/grape/middleware/formatter.rb' + - 'lib/grape/middleware/versioner/accept_version_header.rb' + - 'lib/grape/validations/params_scope.rb' # Offense count: 1 -Style/MethodMissing: +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IgnoredMethods. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'lib/grape/middleware/formatter.rb' + +# Offense count: 12 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/grape/api.rb' + - 'lib/grape/dsl/inside_route.rb' + - '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' + +# Offense count: 18 +# Cop supports --auto-correct. +Style/RedundantRegexpEscape: + Exclude: + - 'lib/grape/middleware/versioner/header.rb' + - 'lib/grape/middleware/versioner/parse_media_type_patch.rb' + - 'spec/grape/api/routes_with_requirements_spec.rb' + - 'spec/grape/api_spec.rb' + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/grape/api/instance.rb' + - 'lib/grape/dsl/desc.rb' + - 'lib/grape/dsl/inside_route.rb' + - 'lib/grape/dsl/request_response.rb' + - 'lib/grape/endpoint.rb' + - 'lib/grape/middleware/versioner/accept_version_header.rb' + - 'lib/grape/middleware/versioner/header.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: AllowModifier. +Style/SoleNestedConditional: Exclude: - - 'lib/grape/router/attribute_translator.rb' + - 'lib/grape/api/instance.rb' + - 'lib/grape/middleware/versioner/accept_version_header.rb' + - 'lib/grape/validations/params_scope.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +Style/StringConcatenation: + Exclude: + - 'benchmark/large_model.rb' + - 'lib/grape/dsl/inside_route.rb' + - 'lib/grape/router/pattern.rb' + - 'spec/grape/validations/validators/values_spec.rb' + - 'spec/shared/versioning_examples.rb' + - 'spec/support/basic_auth_encode_helpers.rb' + +# Offense count: 32 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'spec/grape/validations_spec.rb' # Offense count: 1 -Style/MultipleComparison: +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'spec/grape/validations/single_attribute_iterator_spec.rb' + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: Exclude: - - 'lib/grape/validations/types/custom_type_coercer.rb' + - 'spec/grape/validations/validators/except_values_spec.rb' + - 'spec/grape/validations/validators/values_spec.rb' + +# Offense count: 132 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 215 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e6a01b439..000000000 --- a/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -language: ruby - -sudo: false - -matrix: - include: - - rvm: 2.4.2 - script: - - bundle exec danger - - rvm: 2.4.2 - gemfile: Gemfile - - rvm: 2.4.2 - gemfile: gemfiles/rack_edge.gemfile - - rvm: 2.4.2 - gemfile: gemfiles/rack_1.5.2.gemfile - - rvm: 2.4.2 - gemfile: gemfiles/rails_edge.gemfile - - rvm: 2.4.2 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.4.2 - gemfile: gemfiles/multi_json.gemfile - script: - - bundle exec rake - - bundle exec rspec spec/integration/multi_json - - rvm: 2.4.2 - gemfile: gemfiles/multi_xml.gemfile - script: - - bundle exec rake - - bundle exec rspec spec/integration/multi_xml - - rvm: 2.3.5 - gemfile: Gemfile - - rvm: 2.3.5 - gemfile: gemfiles/rack_edge.gemfile - - rvm: 2.3.5 - gemfile: gemfiles/rack_1.5.2.gemfile - - rvm: 2.3.5 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.2.8 - gemfile: Gemfile - - rvm: 2.2.8 - gemfile: gemfiles/rack_1.5.2.gemfile - - rvm: 2.2.8 - gemfile: gemfiles/rails_5.gemfile - - rvm: 2.2.8 - gemfile: gemfiles/rails_4.gemfile - - rvm: 2.2.8 - gemfile: gemfiles/rails_3.gemfile - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - allow_failures: - - rvm: ruby-head - - rvm: jruby-head - - rvm: rbx-3 - -bundler_args: --without development diff --git a/.yardopts b/.yardopts new file mode 100644 index 000000000..51701cd72 --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +--asset grape.png diff --git a/Appraisals b/Appraisals index 8c7cd680c..1613cfa03 100644 --- a/Appraisals +++ b/Appraisals @@ -1,18 +1,15 @@ -appraise 'rails-3' do - gem 'rails', '3.2.19' - gem 'rack-cache', '<= 1.2' # Pin as next rack-cache version (1.3) removes Ruby1.9 support -end +# frozen_string_literal: true -appraise 'rails-4' do - gem 'rails', '4.1.6' +appraise 'rails-5' do + gem 'rails', '~> 5.2' end -appraise 'rails-5' do - gem 'rails', '5.0.0' +appraise 'rails-6' do + gem 'rails', '~> 6.0' end -appraise 'rack-1.5.2' do - gem 'rack', '1.5.2' +appraise 'rails-6-1' do + gem 'rails', '~> 6.1' end appraise 'rails-edge' do @@ -30,3 +27,11 @@ end appraise 'multi_xml' do gem 'multi_xml', require: 'multi_xml' end + +appraise 'rack1' do + gem 'rack', '~> 1.0' +end + +appraise 'rack2' do + gem 'rack', '~> 2.0' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index fb52340e8..489ef8aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,245 @@ -### 1.1.0 (8/4/2018) +### 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 -* [#1759](https://github.com/ruby-grape/grape/pull/1759): Instrument serialization as `'format_response.grape'` - [@zvkemp](https://github.com/zvkemp). +* [#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). @@ -13,7 +247,7 @@ * [#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 (4/23/2018) +### 1.0.3 (2018/4/23) #### Fixes @@ -26,7 +260,7 @@ * [#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 (1/10/2018) +### 1.0.2 (2018/1/10) #### Features @@ -44,7 +278,7 @@ * [#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 (9/8/2017) +### 1.0.1 (2017/9/8) #### Features @@ -58,7 +292,7 @@ * [#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 (7/3/2017) +### 1.0.0 (2017/7/3) #### Features @@ -77,7 +311,7 @@ * [#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 (4/12/2017) +### 0.19.2 (2017/4/12) #### Features @@ -98,7 +332,7 @@ * [#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 (1/9/2017) +### 0.19.1 (2017/1/9) #### Features @@ -110,7 +344,7 @@ * [#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 (12/18/2016) +### 0.19.0 (2016/12/18) #### Features @@ -126,7 +360,7 @@ * [#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 (10/7/2016) +### 0.18.0 (2016/10/7) #### Features @@ -143,7 +377,7 @@ * [#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 (7/29/2016) +### 0.17.0 (2016/7/29) #### Features @@ -170,7 +404,7 @@ * [#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 (4/12/2016) +### 0.16.2 (2016/4/12) #### Features @@ -183,7 +417,7 @@ * [#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 (4/3/2016) +### 0.16.1 (2016/4/3) #### Features @@ -198,7 +432,7 @@ * [#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 (3/8/2016) +### 0.15.0 (2016/3/8) #### Features @@ -225,7 +459,7 @@ * [#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 (12/07/2015) +### 0.14.0 (2015/12/07) #### Features @@ -252,7 +486,7 @@ * [#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 (8/10/2015) +### 0.13.0 (2015/8/10) #### Features @@ -273,7 +507,7 @@ * [#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 (6/18/2015) +### 0.12.0 (2015/6/18) #### Features @@ -299,7 +533,7 @@ * [#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 (2/23/2015) +### 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). @@ -316,12 +550,12 @@ * [#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 (12/28/2014) +### 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 (12/19/2014) +### 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). @@ -344,7 +578,7 @@ * [#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 (8/27/2014) +### 0.9.0 (2014/8/27) #### Features @@ -362,7 +596,7 @@ * [#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 (7/10/2014) +### 0.8.0 (2014/7/10) #### Features @@ -382,7 +616,7 @@ * [#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 (4/2/2014) +### 0.7.0 (2014/4/2) #### Features @@ -411,7 +645,7 @@ * [#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 explicitely passed - [@yaneq](https://github.com/yaneq). +* [#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). @@ -421,7 +655,7 @@ * [#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 (10/19/2013) +### 0.6.1 (2013/10/19) #### Features @@ -437,7 +671,7 @@ * Implemented Rubocop, a Ruby code static code analyzer - [@dblock](https://github.com/dblock). -### 0.6.0 (9/16/2013) +### 0.6.0 (2013/9/16) #### Features @@ -455,7 +689,7 @@ * [#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 (6/14/2013) +### 0.5.0 (2013/6/14) #### Features @@ -481,11 +715,11 @@ * [#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 (4/1/2013) +### 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 (3/17/2013) +### 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). @@ -499,15 +733,15 @@ * [#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 (2/28/2013) +### 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 (2/25/2013) +### 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 (02/21/2013) +### 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). @@ -526,12 +760,12 @@ * [#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 (01/11/2013) +### 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 (01/10/2013) +### 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). @@ -540,7 +774,7 @@ * [#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 (01/06/2013) +### 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). @@ -558,7 +792,7 @@ * [#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 (24/12/2012) +### 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). @@ -571,7 +805,7 @@ * [#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 (12/10/2012) +### 0.2.2 (2012/12/10) #### Features @@ -593,7 +827,7 @@ * [#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 (7/11/2012) +### 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). @@ -608,7 +842,7 @@ * [#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 (3/28/2012) +### 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). @@ -628,28 +862,28 @@ * 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 (6/14/2011) +### 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 (4/8/2011) +### 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 (1/10/2011) +### 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 (11/14/2010) +### 0.1.1 (2010/11/14) * Endpoints properly reset between each request - [@mbleigh](https://github.com/mbleigh). -### 0.1.0 (11/13/2010) +### 0.1.0 (2010/11/13) * Initial public release - [@mbleigh](https://github.com/mbleigh). diff --git a/Dangerfile b/Dangerfile index 5ff5d8e71..527dbb8b7 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,2 +1,4 @@ +# frozen_string_literal: true + danger.import_dangerfile(gem: 'ruby-grape-danger') -toc.check +toc.check! diff --git a/Gemfile b/Gemfile index 9ea7a5058..8e32048c4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # when changing this file, run appraisal install ; rubocop -a gemfiles/*.gemfile -source 'https://rubygems.org' +source('https://rubygems.org') gemspec @@ -8,12 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,13 +26,16 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false +end + +platforms :jruby do + gem 'racc' end diff --git a/Guardfile b/Guardfile index ad1b72546..f771d0349 100644 --- a/Guardfile +++ b/Guardfile @@ -1,3 +1,5 @@ +# 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" } diff --git a/LICENSE b/LICENSE index c0b20e593..e689bdce6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2018 Michael Bleigh, Intridea Inc. and Contributors. +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 diff --git a/README.md b/README.md index 4cc8f6c45..f74f17f74 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ ![grape logo](grape.png) [![Gem Version](https://badge.fury.io/rb/grape.svg)](http://badge.fury.io/rb/grape) -[![Build Status](https://travis-ci.org/ruby-grape/grape.svg?branch=master)](https://travis-ci.org/ruby-grape/grape) -[![Dependency Status](https://gemnasium.com/ruby-grape/grape.svg)](https://gemnasium.com/ruby-grape/grape) +[![Build Status](https://github.com/ruby-grape/grape/workflows/test/badge.svg?branch=master)](https://github.com/ruby-grape/grape/actions) [![Code Climate](https://codeclimate.com/github/ruby-grape/grape.svg)](https://codeclimate.com/github/ruby-grape/grape) [![Coverage Status](https://coveralls.io/repos/github/ruby-grape/grape/badge.svg?branch=master)](https://coveralls.io/github/ruby-grape/grape?branch=master) -[![Inline docs](http://inch-ci.org/github/ruby-grape/grape.svg)](http://inch-ci.org/github/ruby-grape/grape) -[![git.legal](https://git.legal/projects/1364/badge.svg "Number of libraries approved")](https://git.legal/projects/1364) +[![Inline docs](https://inch-ci.org/github/ruby-grape/grape.svg)](https://inch-ci.org/github/ruby-grape/grape) [![Join the chat at https://gitter.im/ruby-grape/grape](https://badges.gitter.im/ruby-grape/grape.svg)](https://gitter.im/ruby-grape/grape?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Table of Contents @@ -14,20 +12,29 @@ - [What is Grape?](#what-is-grape) - [Stable Release](#stable-release) - [Project Resources](#project-resources) +- [Grape for Enterprise](#grape-for-enterprise) - [Installation](#installation) - [Basic Usage](#basic-usage) - [Mounting](#mounting) + - [All](#all) - [Rack](#rack) - [ActiveRecord without Rails](#activerecord-without-rails) + - [Rails 4](#rails-4) + - [Rails 5+](#rails-5) - [Alongside Sinatra (or other frameworks)](#alongside-sinatra-or-other-frameworks) - [Rails](#rails) + - [Rails < 5.2](#rails--52) + - [Rails 6.0](#rails-60) - [Modules](#modules) +- [Remounting](#remounting) + - [Mount Configuration](#mount-configuration) - [Versioning](#versioning) - [Path](#path) - [Header](#header) - [Accept-Version Header](#accept-version-header) - [Param](#param) - [Describing Methods](#describing-methods) +- [Configuration](#configuration) - [Parameters](#parameters) - [Params Class](#params-class) - [Declared](#declared) @@ -38,38 +45,43 @@ - [Integer/Fixnum and Coercions](#integerfixnum-and-coercions) - [Custom Types and Coercions](#custom-types-and-coercions) - [Multipart File Parameters](#multipart-file-parameters) - - [First-Class `JSON` Types](#first-class-json-types) + - [First-Class JSON Types](#first-class-json-types) - [Multiple Allowed Types](#multiple-allowed-types) - [Validation of Nested Parameters](#validation-of-nested-parameters) - [Dependent Parameters](#dependent-parameters) - [Group Options](#group-options) - - [Alias](#alias) + - [Renaming](#renaming) - [Built-in Validators](#built-in-validators) - - [`allow_blank`](#allow_blank) - - [`values`](#values) - - [`except_values`](#except_values) - - [`regexp`](#regexp) - - [`mutually_exclusive`](#mutually_exclusive) - - [`exactly_one_of`](#exactly_one_of) - - [`at_least_one_of`](#at_least_one_of) - - [`all_or_none_of`](#all_or_none_of) - - [Nested `mutually_exclusive`, `exactly_one_of`, `at_least_one_of`, `all_or_none_of`](#nested-mutually_exclusive-exactly_one_of-at_least_one_of-all_or_none_of) + - [allow_blank](#allow_blank) + - [values](#values) + - [except_values](#except_values) + - [same_as](#same_as) + - [regexp](#regexp) + - [mutually_exclusive](#mutually_exclusive) + - [exactly_one_of](#exactly_one_of) + - [at_least_one_of](#at_least_one_of) + - [all_or_none_of](#all_or_none_of) + - [Nested mutually_exclusive, exactly_one_of, at_least_one_of, all_or_none_of](#nested-mutually_exclusive-exactly_one_of-at_least_one_of-all_or_none_of) - [Namespace Validation and Coercion](#namespace-validation-and-coercion) - [Custom Validators](#custom-validators) - [Validation Errors](#validation-errors) - [I18n](#i18n) - [Custom Validation messages](#custom-validation-messages) - - [`presence`, `allow_blank`, `values`, `regexp`](#presence-allow_blank-values-regexp) - - [`all_or_none_of`](#all_or_none_of-1) - - [`mutually_exclusive`](#mutually_exclusive-1) - - [`exactly_one_of`](#exactly_one_of-1) - - [`at_least_one_of`](#at_least_one_of-1) - - [`Coerce`](#coerce) - - [`With Lambdas`](#with-lambdas) - - [`Pass symbols for i18n translations`](#pass-symbols-for-i18n-translations) + - [presence, allow_blank, values, regexp](#presence-allow_blank-values-regexp) + - [same_as](#same_as-1) + - [all_or_none_of](#all_or_none_of-1) + - [mutually_exclusive](#mutually_exclusive-1) + - [exactly_one_of](#exactly_one_of-1) + - [at_least_one_of](#at_least_one_of-1) + - [Coerce](#coerce) + - [With Lambdas](#with-lambdas) + - [Pass symbols for i18n translations](#pass-symbols-for-i18n-translations) - [Overriding Attribute Names](#overriding-attribute-names) - [With Default](#with-default) - [Headers](#headers) + - [Request](#request) + - [Header Case Handling](#header-case-handling) + - [Response](#response) - [Routes](#routes) - [Helpers](#helpers) - [Path Helpers](#path-helpers) @@ -105,7 +117,7 @@ - [Register custom middleware for authentication](#register-custom-middleware-for-authentication) - [Describing and Inspecting an API](#describing-and-inspecting-an-api) - [Current Route and Endpoint](#current-route-and-endpoint) -- [Before and After](#before-and-after) +- [Before, After and Finally](#before-after-and-finally) - [Anchoring](#anchoring) - [Using Custom Middleware](#using-custom-middleware) - [Grape Middleware](#grape-middleware) @@ -132,6 +144,7 @@ - [format_response.grape](#format_responsegrape) - [Monitoring Products](#monitoring-products) - [Contributing to Grape](#contributing-to-grape) +- [Security](#security) - [License](#license) - [Copyright](#copyright) @@ -145,7 +158,7 @@ content negotiation, versioning and much more. ## Stable Release -You're reading the documentation for the stable release of Grape, **1.1.0**. +You're reading the documentation for the stable release of Grape, **v1.5.3**. Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. ## Project Resources @@ -155,8 +168,18 @@ Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version. * Need help? Try [Grape Google Group](http://groups.google.com/group/ruby-grape) or [Gitter](https://gitter.im/ruby-grape/grape) * [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. + +In 2020, we plan to use the money towards gathering Grape contributors for dinner in New York City. + ## Installation +Ruby 2.4 or newer is required. + Grape is available as a gem, to install it just install the gem: gem install grape @@ -204,7 +227,7 @@ module Twitter desc 'Return a status.' params do - requires :id, type: Integer, desc: 'Status id.' + requires :id, type: Integer, desc: 'Status ID.' end route_param :id do get do @@ -252,6 +275,17 @@ end ## Mounting +### All + + +By default Grape will compile the routes on the first route, 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 @@ -261,6 +295,13 @@ with `rackup`: 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 @@ -277,13 +318,21 @@ Grape will also automatically respond to HEAD and OPTIONS for all GET, and just If you want to use ActiveRecord within Grape, you will need to make sure that ActiveRecord's connection pool is handled correctly. +#### Rails 4 + The easiest way to achieve that is by using ActiveRecord's `ConnectionManagement` middleware in your `config.ru` before mounting Grape, e.g.: ```ruby use ActiveRecord::ConnectionAdapters::ConnectionManagement +``` -run Twitter::API +#### Rails 5+ + +Use [otr-activerecord](https://github.com/jhollinger/otr-activerecord) as follows: + +```ruby +use OTR::ActiveRecord::ConnectionManagement ``` ### Alongside Sinatra (or other frameworks) @@ -310,13 +359,24 @@ class Web < Sinatra::Base end use Rack::Session::Cookie -run Rack::Cascade.new [API, Web] +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 => '/' +``` + +#### Rails < 5.2 + Modify `application.rb`: ```ruby @@ -324,14 +384,18 @@ config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] ``` -Modify `config/routes`: +See [below](#reloading-api-changes-in-development) for additional code that enables reloading of API changes in development. + +#### Rails 6.0 + +For Rails versions greater than 6.0.0.beta2, `Zeitwerk` autoloader is the default for CRuby. By default `Zeitwerk` 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 -mount Twitter::API => '/' +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'API' +end ``` -See [below](#reloading-api-changes-in-development) for additional code that enables reloading of API changes in development. - ### Modules You can mount multiple API implementations inside another one. These don't have to be @@ -365,6 +429,131 @@ class Twitter::API < Grape::API 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: mounted { configuration[:entity] || API::Entities::Status }.documentation + end + params do + requires :all, using: mounted { configuration[:entity] || API::Entities::Status }.documentation + end + get '/statuses' do + statuses = Status.all + type = current_user.admin? ? :full : :default + present statuses, with: mounted { configuration[:entity] || API::Entities::Status }, type: type + end +end + +class V1 < Grape::API + version 'v1' + mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::Status } } +end + +class V2 < Grape::API + version 'v2' + mount BasicAPI, with: { entity: mounted { configuration[:entity] || API::Enitities::V2::Status } } +end +``` + ## Versioning There are four strategies in which clients can reach your API's endpoints: `:path`, @@ -445,10 +634,13 @@ version 'v1', using: :param, parameter: 'v' ## Describing Methods -You can add a description to API methods and namespaces. +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 @@ -462,7 +654,13 @@ desc 'Returns your public timeline.' do 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) @@ -475,6 +673,43 @@ end * `failure`: (former http_codes) A definition of the used failure HTTP Codes and Entities * `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 = Grape::Extensions::Hashie::Mash::ParamBuilder +end +``` + +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 @@ -553,13 +788,20 @@ params do end ``` +Or globally with the [Configuration](#configuration) `Grape.configure.param_builder`. + In the example above, `params["color"]` will return `nil` since `params` is a plain `Hash`. Available parameter builders are `Grape::Extensions::Hash::ParamBuilder`, `Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder` and `Grape::Extensions::Hashie::Mash::ParamBuilder`. ### Declared -Grape allows you to access only the parameters that have been declared by your `params` block. It filters out the params that have been passed, but are not allowed. Consider the following API endpoint: +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. + +Consider the following API endpoint: ````ruby format :json @@ -592,9 +834,9 @@ Once we add parameters requirements, grape will start returning only the declare format :json params do - requires :user, type: Hash do - requires :first_name, type: String - requires :last_name, type: String + optional :user, type: Hash do + optional :first_name, type: String + optional :last_name, type: String end end @@ -622,6 +864,44 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d } ```` +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. @@ -680,8 +960,10 @@ By default `declared(params)` includes parameters that have `nil` values. If you format :json params do - requires :first_name, type: String - optional :last_name, type: String + requires :user, type: Hash do + requires :first_name, type: String + optional :last_name, type: String + end end post 'users/signup' do @@ -834,13 +1116,13 @@ params do end ``` -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. - 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'] @@ -900,7 +1182,8 @@ 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 raise an exception to indicate the value was invalid. E.g., +type, or return an instance of `Grape::Types::InvalidValue` which optionally +accepts a message to be returned in the response. ```ruby class Color @@ -910,8 +1193,9 @@ class Color end def self.parse(value) - fail 'Invalid color' unless %w(blue red green).include?(value) - new(value) + return new(value) if %w[blue red green]).include?(value) + + Grape::Types::InvalidValue.new('Unsupported color') end end @@ -934,7 +1218,7 @@ parameter, and the return value must match the given `type`. ```ruby params do - requires :passwd, type: String, coerce_with: Base64.method(:decode) + 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 @@ -943,6 +1227,7 @@ params do 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`. @@ -1109,6 +1394,18 @@ params do 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 @@ -1137,9 +1434,9 @@ params do end ``` -### Alias +### Renaming -You can set an alias for parameters using `as`, which can be useful when refactoring existing APIs: +You can rename parameters using `as`, which can be useful when refactoring existing APIs: ```ruby resource :users do @@ -1248,6 +1545,17 @@ params do 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 +``` + #### `regexp` Parameters can be restricted to match a specific regular expression with the `:regexp` option. If the value @@ -1485,7 +1793,7 @@ params do end ``` -Every validation will have it's own instance of the validator, which means that the validator can have a state. +Every validation will have its own instance of the validator, which means that the validator can have a state. ### Validation Errors @@ -1546,6 +1854,8 @@ end 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 Validation messages Grape supports custom validation messages for parameter-related and coerce-related error messages. @@ -1558,6 +1868,15 @@ params do end ``` +#### `same_as` + +```ruby +params do + requires :password + requires :password_confirmation, same_as: { value: :password, message: 'not match' } +end +``` + #### `all_or_none_of` ```ruby @@ -1587,7 +1906,7 @@ 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"} + 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 ``` @@ -1606,7 +1925,7 @@ end ```ruby params do - requires :int, type: {value: Integer, message: "type cast is invalid" } + requires :int, type: { value: Integer, message: "type cast is invalid" } end ``` @@ -1668,6 +1987,7 @@ end ## Headers +### Request Request headers are available through the `headers` helper or from `env` in their original form. ```ruby @@ -1682,13 +2002,30 @@ get do 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 capitalized kebab case. +- In the `env` collection they appear in all uppercase, in snake case, and prefixed with 'HTTP_'. + +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. +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.' @@ -1696,6 +2033,56 @@ 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. @@ -2028,6 +2415,12 @@ instead of a message. 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 @@ -2182,7 +2575,7 @@ You can also rescue all exceptions with a code block and handle the Rack respons ```ruby class Twitter::API < Grape::API rescue_from :all do |e| - Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }).finish + Rack::Response.new([ e.message ], 500, { 'Content-type' => 'text/error' }) end end ``` @@ -2253,9 +2646,9 @@ class Twitter::API < Grape::API end ``` -The `rescue_from` block must return a `Rack::Response` object, call `error!` or re-raise an exception. +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). -The `with` keyword is available as `rescue_from` options, it can be passed method name or Proc object. +Alternately, use the `with` option in `rescue_from` to specify a method or a `proc`. ```ruby class Twitter::API < Grape::API @@ -2271,6 +2664,17 @@ class Twitter::API < Grape::API 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 @@ -2293,7 +2697,7 @@ class Twitter::API < Grape::API end ``` -Here `'inner'` will be result of handling occured `ArgumentError`. +Here `'inner'` will be result of handling occurred `ArgumentError`. #### Unrescuable Exceptions @@ -2824,17 +3228,19 @@ end Use `body false` to return `204 No Content` without any data or content-type. -You can also set the response to a file with `file`. +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 - file '/path/to/file' + sendfile '/path/to/file' end end ``` -If you want a file to be streamed using Rack::Chunked, use `stream`. +To stream a file in chunks use `stream` ```ruby class API < Grape::API @@ -2844,6 +3250,26 @@ class API < Grape::API 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 and Digest Auth @@ -2855,14 +3281,13 @@ applies to the current namespace and any children, but not parents. ```ruby http_basic do |username, password| # verify user's password here - { 'test' => 'password1' }[username] == password + # IMPORTANT: make sure you use a comparison method which isn't prone to a timing attack end ``` ```ruby http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username| # lookup the user's password here - { 'user1' => 'password1' }[username] end ``` @@ -2893,7 +3318,9 @@ end ``` -Use [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2) for OAuth2 support. +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 @@ -2962,19 +3389,22 @@ class ApiLogger < Grape::Middleware::Base end ``` -## Before and After +## 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` -5. _the API call_ -6. `after` +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. @@ -2994,6 +3424,14 @@ before do 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: @@ -3196,6 +3634,8 @@ class API < Grape::API 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. @@ -3504,6 +3944,7 @@ Grape integrates with following third-party tools: * **Librato Metrics** - [grape-librato](https://github.com/seanmoon/grape-librato) 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) ## Contributing to Grape @@ -3512,10 +3953,14 @@ features and discuss issues. See [CONTRIBUTING](CONTRIBUTING.md). +## Security + +See [SECURITY](SECURITY.md) for details. + ## License -MIT License. See LICENSE for details. +MIT License. See [LICENSE](LICENSE) for details. ## Copyright -Copyright (c) 2010-2018 Michael Bleigh, Intridea Inc. and Contributors. +Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors. diff --git a/RELEASING.md b/RELEASING.md index 4a5a7209e..1d40997ce 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -36,7 +36,7 @@ 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 (9/16/2013) +#### 0.6.0 (2013/9/16) ``` Remove the line with "Your contribution here.", since there will be no more contributions to this release. @@ -74,7 +74,13 @@ The current stable release is [0.6.0](https://github.com/ruby-grape/grape/blob/v Add the next release to [CHANGELOG.md](CHANGELOG.md). ``` -#### 0.6.1 (Next) +### 0.6.1 (Next) + +#### Features + +* Your contribution here. + +#### Fixes * Your contribution here. ``` @@ -83,7 +89,7 @@ Bump the minor version in lib/grape/version.rb. ```ruby module Grape - VERSION = '0.6.1' + VERSION = '0.6.1'.freeze end ``` diff --git a/Rakefile b/Rakefile index 2beb99875..2d79797f7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,12 @@ -require 'rubygems' -require 'bundler' -Bundler.setup :default, :test, :development +# frozen_string_literal: true + +require('rubygems') +require('bundler') +Bundler.setup(:default, :test, :development) Bundler::GemHelper.install_tasks -require 'rspec/core/rake_task' +require('rspec/core/rake_task') RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.exclude_pattern = 'spec/integration/**/*_spec.rb' @@ -15,11 +17,11 @@ RSpec::Core::RakeTask.new(:rcov) do |spec| spec.rcov = true end -task :spec +task(:spec) -require 'rainbow/ext/string' unless String.respond_to?(:color) +require('rainbow/ext/string') unless String.respond_to?(:color) -require 'rubocop/rake_task' +require('rubocop/rake_task') RuboCop::RakeTask.new -task default: %i[rubocop spec] +task(default: %i[rubocop spec]) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..6a8cc7d6a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +## Supported Versions + +Version 1.2.0 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. + diff --git a/UPGRADING.md b/UPGRADING.md index d63fa23e6..c8813e32f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,424 @@ Upgrading Grape =============== + +### 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 such 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 always been a special case when dealing with types especially with the following structures: + +- Array +- Hash +- Set + +The behavior for these structures has change through out 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 + +#### 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 @@ -70,8 +488,7 @@ 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. +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. @@ -147,9 +564,7 @@ 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. +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. @@ -170,8 +585,7 @@ 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. +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 @@ -349,18 +763,14 @@ 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 +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. +`#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. @@ -394,10 +804,7 @@ 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: +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 diff --git a/benchmark/compile_many_routes.rb b/benchmark/compile_many_routes.rb new file mode 100644 index 000000000..9fa858cf3 --- /dev/null +++ b/benchmark/compile_many_routes.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +require 'grape' +require 'benchmark/ips' +require 'grape/eager_load' + +Grape.eager_load! + +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 diff --git a/benchmark/large_model.rb b/benchmark/large_model.rb new file mode 100644 index 000000000..48827528d --- /dev/null +++ b/benchmark/large_model.rb @@ -0,0 +1,249 @@ +# 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: ->(value) { value.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: '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, {}) } diff --git a/benchmark/nested_params.rb b/benchmark/nested_params.rb new file mode 100644 index 000000000..2523939b8 --- /dev/null +++ b/benchmark/nested_params.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +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: '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 diff --git a/benchmark/remounting.rb b/benchmark/remounting.rb new file mode 100644 index 000000000..5c565b1d3 --- /dev/null +++ b/benchmark/remounting.rb @@ -0,0 +1,49 @@ +# 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: '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 diff --git a/benchmark/resource/vrp_example.json b/benchmark/resource/vrp_example.json new file mode 100644 index 000000000..d9c9cd5bc --- /dev/null +++ b/benchmark/resource/vrp_example.json @@ -0,0 +1 @@ +{"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}}}} diff --git a/benchmark/simple.rb b/benchmark/simple.rb index 022cebe8b..053c33351 100644 --- a/benchmark/simple.rb +++ b/benchmark/simple.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'grape' require 'benchmark/ips' diff --git a/benchmark/simple_with_type_coercer.rb b/benchmark/simple_with_type_coercer.rb deleted file mode 100644 index 2a1c408d3..000000000 --- a/benchmark/simple_with_type_coercer.rb +++ /dev/null @@ -1,22 +0,0 @@ -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'grape' -require 'benchmark/ips' - -api = Class.new(Grape::API) do - prefix :api - version 'v1', using: :path - params do - requires :param, type: Array[String] - end - get '/' do - 'hello' - end -end - -env = Rack::MockRequest.env_for('/api/v1?param=value', method: 'GET') - -Benchmark.ips do |ips| - ips.report('simple_with_type_coercer') do - api.call(env) - end -end diff --git a/gemfiles/multi_json.gemfile b/gemfiles/multi_json.gemfile index af5797e5c..726c8be19 100644 --- a/gemfiles/multi_json.gemfile +++ b/gemfiles/multi_json.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' @@ -8,12 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/multi_xml.gemfile b/gemfiles/multi_xml.gemfile index b323389cf..72d4e3f30 100644 --- a/gemfiles/multi_xml.gemfile +++ b/gemfiles/multi_xml.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' @@ -8,12 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rails_4.gemfile b/gemfiles/rack1.gemfile similarity index 61% rename from gemfiles/rails_4.gemfile rename to gemfiles/rack1.gemfile index 7a4d248c1..bb72144a0 100644 --- a/gemfiles/rails_4.gemfile +++ b/gemfiles/rack1.gemfile @@ -1,19 +1,24 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' -gem 'rails', '4.1.6' +gem 'rack', '~> 1.0' group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rack_1.5.2.gemfile b/gemfiles/rack2.gemfile similarity index 61% rename from gemfiles/rack_1.5.2.gemfile rename to gemfiles/rack2.gemfile index a9b955f01..6522140d9 100644 --- a/gemfiles/rack_1.5.2.gemfile +++ b/gemfiles/rack2.gemfile @@ -1,19 +1,24 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' -gem 'rack', '1.5.2' +gem 'rack', '~> 2.0' group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rails_3.gemfile b/gemfiles/rack2_2.gemfile similarity index 61% rename from gemfiles/rails_3.gemfile rename to gemfiles/rack2_2.gemfile index 5c70608a9..88cfa240d 100644 --- a/gemfiles/rails_3.gemfile +++ b/gemfiles/rack2_2.gemfile @@ -1,20 +1,24 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' -gem 'rails', '3.2.19' -gem 'rack-cache', '<= 1.2' +gem 'rack', '~> 2.2' group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -22,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rack_edge.gemfile b/gemfiles/rack_edge.gemfile index ee2b96774..25b275b6c 100644 --- a/gemfiles/rack_edge.gemfile +++ b/gemfiles/rack_edge.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' @@ -8,12 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rails_5.gemfile b/gemfiles/rails_5.gemfile index 2b59c1add..7dc94e695 100644 --- a/gemfiles/rails_5.gemfile +++ b/gemfiles/rails_5.gemfile @@ -1,19 +1,24 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' -gem 'rails', '5.0.0' +gem 'rails', '~> 5.2' group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile new file mode 100644 index 000000000..5db9b049d --- /dev/null +++ b/gemfiles/rails_6.gemfile @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rails', '~> 6.0' + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false +end + +group :development do + gem 'appraisal' + gem 'benchmark-ips' + gem 'benchmark-memory' + gem 'guard' + gem 'guard-rspec' + gem 'guard-rubocop' +end + +group :test do + gem 'cookiejar' + gem 'coveralls_reborn' + gem 'grape-entity', '~> 0.6' + gem 'maruku' + gem 'mime-types' + gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-test', '~> 1.1.0' + gem 'rspec', '~> 3.0' + gem 'ruby-grape-danger', '~> 0.2.0', require: false +end + +gemspec path: '../' diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile new file mode 100644 index 000000000..d27f9ed02 --- /dev/null +++ b/gemfiles/rails_6_1.gemfile @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source 'https://rubygems.org' + +gem 'rails', '~> 6.1' + +group :development, :test do + gem 'bundler' + gem 'hashie' + gem 'rake' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false +end + +group :development do + gem 'appraisal' + gem 'benchmark-ips' + gem 'benchmark-memory' + gem 'guard' + gem 'guard-rspec' + gem 'guard-rubocop' +end + +group :test do + gem 'cookiejar' + gem 'coveralls_reborn' + gem 'grape-entity', '~> 0.6' + gem 'maruku' + gem 'mime-types' + gem 'rack-jsonp', require: 'rack/jsonp' + gem 'rack-test', '~> 1.1.0' + gem 'rspec', '~> 3.0' + gem 'ruby-grape-danger', '~> 0.2.0', require: false +end + +gemspec path: '../' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile index bce0e1942..5d179f078 100644 --- a/gemfiles/rails_edge.gemfile +++ b/gemfiles/rails_edge.gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by Appraisal source 'https://rubygems.org' @@ -8,12 +10,15 @@ group :development, :test do gem 'bundler' gem 'hashie' gem 'rake' - gem 'rubocop', '0.51.0' + gem 'rubocop', '1.7.0' + gem 'rubocop-ast', '1.3.0' + gem 'rubocop-performance', '1.9.1', require: false end group :development do gem 'appraisal' gem 'benchmark-ips' + gem 'benchmark-memory' gem 'guard' gem 'guard-rspec' gem 'guard-rubocop' @@ -21,15 +26,14 @@ end group :test do gem 'cookiejar' - gem 'coveralls', '~> 0.8.17', require: false - gem 'danger-toc', '~> 0.1.2' + gem 'coveralls_reborn' gem 'grape-entity', '~> 0.6' gem 'maruku' gem 'mime-types' gem 'rack-jsonp', require: 'rack/jsonp' - gem 'rack-test', '~> 0.6.3' + gem 'rack-test', '~> 1.1.0' gem 'rspec', '~> 3.0' - gem 'ruby-grape-danger', '~> 0.1.0', require: false + gem 'ruby-grape-danger', '~> 0.2.0', require: false end gemspec path: '../' diff --git a/grape.gemspec b/grape.gemspec index a483dd8b7..6ba65bb33 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) require 'grape/version' @@ -11,15 +13,24 @@ Gem::Specification.new do |s| 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}" + } s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'builder' + s.add_runtime_dependency 'dry-types', '>= 1.1' s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0' s.add_runtime_dependency 'rack', '>= 1.3.0' s.add_runtime_dependency 'rack-accept' - s.add_runtime_dependency 'virtus', '>= 1.0.0' - s.files = Dir['**/*'].keep_if { |file| File.file?(file) } + s.files = %w[CHANGELOG.md CONTRIBUTING.md README.md grape.png UPGRADING.md LICENSE] + s.files += %w[grape.gemspec] + s.files += Dir['lib/**/*'] s.test_files = Dir['spec/**/*'] s.require_paths = ['lib'] + s.required_ruby_version = '>= 2.4.0' end diff --git a/lib/grape.rb b/lib/grape.rb index 0ee168a6d..818c8f6bc 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'logger' require 'rack' require 'rack/builder' @@ -10,6 +12,7 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/except' @@ -18,9 +21,6 @@ require 'active_support/dependencies/autoload' require 'active_support/notifications' require 'i18n' -require 'thread' - -require 'virtus' I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__) @@ -34,7 +34,6 @@ module Grape autoload :Namespace autoload :Path - autoload :Cookies autoload :Validations autoload :ErrorFormatter @@ -55,105 +54,125 @@ module Http module Exceptions extend ::ActiveSupport::Autoload - autoload :Base - autoload :Validation - autoload :ValidationArrayErrors - autoload :ValidationErrors - autoload :MissingVendorOption - autoload :MissingMimeType - autoload :MissingOption - autoload :InvalidFormatter - autoload :InvalidVersionerOption - autoload :UnknownValidator - autoload :UnknownOptions - autoload :UnknownParameter - autoload :InvalidWithOptionForRepresent - autoload :IncompatibleOptionValues - autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' - autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' - autoload :InvalidMessageBody - autoload :InvalidAcceptHeader - autoload :InvalidVersionHeader - autoload :MethodNotAllowed + eager_autoload do + autoload :Base + autoload :Validation + autoload :ValidationArrayErrors + autoload :ValidationErrors + autoload :MissingVendorOption + autoload :MissingMimeType + autoload :MissingOption + autoload :InvalidFormatter + autoload :InvalidVersionerOption + autoload :UnknownValidator + autoload :UnknownOptions + autoload :UnknownParameter + autoload :InvalidWithOptionForRepresent + autoload :IncompatibleOptionValues + autoload :MissingGroupTypeError, 'grape/exceptions/missing_group_type' + autoload :UnsupportedGroupTypeError, 'grape/exceptions/unsupported_group_type' + autoload :InvalidMessageBody + autoload :InvalidAcceptHeader + autoload :InvalidVersionHeader + autoload :MethodNotAllowed + autoload :InvalidResponse + autoload :EmptyMessageBody + end end module Extensions extend ::ActiveSupport::Autoload - - autoload :DeepMergeableHash - autoload :DeepSymbolizeHash - autoload :DeepHashWithIndifferentAccess - autoload :Hash - + eager_autoload do + autoload :DeepMergeableHash + autoload :DeepSymbolizeHash + autoload :Hash + end module ActiveSupport extend ::ActiveSupport::Autoload - - autoload :HashWithIndifferentAccess + eager_autoload do + autoload :HashWithIndifferentAccess + end end module Hashie extend ::ActiveSupport::Autoload - - autoload :Mash + eager_autoload do + autoload :Mash + end end end module Middleware extend ::ActiveSupport::Autoload - autoload :Base - autoload :Versioner - autoload :Formatter - autoload :Error - autoload :Globals - autoload :Stack + eager_autoload do + autoload :Base + autoload :Versioner + autoload :Formatter + autoload :Error + autoload :Globals + autoload :Stack + autoload :Helpers + end module Auth extend ::ActiveSupport::Autoload - autoload :Base - autoload :DSL - autoload :StrategyInfo - autoload :Strategies + eager_autoload do + autoload :Base + autoload :DSL + autoload :StrategyInfo + autoload :Strategies + end end module Versioner extend ::ActiveSupport::Autoload - autoload :Path - autoload :Header - autoload :Param - autoload :AcceptVersionHeader + eager_autoload do + autoload :Path + autoload :Header + autoload :Param + autoload :AcceptVersionHeader + end end end module Util extend ::ActiveSupport::Autoload - autoload :InheritableValues - autoload :StackableValues - autoload :ReverseStackableValues - autoload :InheritableSetting - autoload :StrictHashConfiguration - autoload :Registrable + eager_autoload do + autoload :InheritableValues + autoload :StackableValues + autoload :ReverseStackableValues + autoload :InheritableSetting + autoload :StrictHashConfiguration + autoload :Registrable + end end module ErrorFormatter extend ::ActiveSupport::Autoload - autoload :Base - autoload :Json - autoload :Txt - autoload :Xml + eager_autoload do + autoload :Base + autoload :Json + autoload :Txt + autoload :Xml + end end module Formatter extend ::ActiveSupport::Autoload - autoload :Json - autoload :SerializableHash - autoload :Txt - autoload :Xml + eager_autoload do + autoload :Json + autoload :SerializableHash + autoload :Txt + autoload :Xml + end end module Parser extend ::ActiveSupport::Autoload - autoload :Json - autoload :Xml + eager_autoload do + autoload :Json + autoload :Xml + end end module DSL @@ -177,26 +196,39 @@ module DSL class API extend ::ActiveSupport::Autoload - autoload :Helpers + eager_autoload do + autoload :Helpers + end end module Presenters extend ::ActiveSupport::Autoload - autoload :Presenter + eager_autoload do + autoload :Presenter + end end - module ServeFile + module ServeStream extend ::ActiveSupport::Autoload - autoload :FileResponse - autoload :FileBody - autoload :SendfileResponse + eager_autoload do + autoload :FileBody + autoload :SendfileResponse + autoload :StreamResponse + end end end -require 'grape/util/content_types' +require 'grape/config' +require 'grape/content_types' + +require 'grape/util/lazy_value' +require 'grape/util/lazy_block' +require 'grape/util/endpoint_configuration' require 'grape/validations/validators/base' require 'grape/validations/attributes_iterator' +require 'grape/validations/single_attribute_iterator' +require 'grape/validations/multiple_attributes_iterator' require 'grape/validations/validators/allow_blank' require 'grape/validations/validators/as' require 'grape/validations/validators/at_least_one_of' @@ -206,6 +238,7 @@ module ServeFile require 'grape/validations/validators/mutual_exclusion' require 'grape/validations/validators/presence' require 'grape/validations/validators/regexp' +require 'grape/validations/validators/same_as' require 'grape/validations/validators/values' require 'grape/validations/validators/except_values' require 'grape/validations/params_scope' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 70114859d..11bcb6a44 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -1,233 +1,196 @@ +# frozen_string_literal: true + require 'grape/router' +require 'grape/api/instance' 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 - include Grape::DSL::API + # Class methods that we want to call on the API rather than on the API object + NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze class << self - attr_reader :instance + attr_accessor :base_instance, :instances - # A class-level lock to ensure the API is not compiled by multiple - # threads simultaneously within the same process. - LOCK = Mutex.new + # Rather than initializing an object of type Grape::API, create an object of type Instance + def new(*args, &block) + base_instance.new(*args, &block) + end - # Clears all defined routes, endpoints, etc., on this API. - def reset! - reset_endpoints! - reset_routes! - reset_validations! + # 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, base_instance_parent = Grape::API::Instance) + api.initial_setup(base_instance_parent) + api.override_all_methods! + make_inheritable(api) end - # Parses the API's definition and compiles it into an instance of - # Grape::API. - def compile - @instance ||= new + # 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 = Set.new + @base_parent = base_instance_parent + @base_instance = mount_instance end - # Wipe the compiled API so we can recompile after changes were made. - def change! - @instance = nil + # Redefines all methods so that are forwarded to add_setup and be recorded + def override_all_methods! + (base_instance.methods - NON_OVERRIDABLE).each do |method_override| + define_singleton_method(method_override) do |*args, &block| + add_setup(method_override, *args, &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 # 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) - LOCK.synchronize { compile } unless instance - call!(env) + # NOTE: This will only be called on an API directly mounted on RACK + def call(*args, &block) + instance_for_rack.call(*args, &block) end - # A non-synchronized version of ::call. - def call!(env) - instance.call(env) + # Allows an API to itself be inheritable: + def make_inheritable(api) + # When a child API inherits from a parent API. + def api.inherited(child_api) + # The instances of the child API inherit from the instances of the parent API + Grape::API.inherited(child_api, base_instance) + end end - # (see #cascade?) - def cascade(value = nil) - if value.nil? - inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true + # Alleviates problems with autoloading by tring to search for the constant + def const_missing(*args) + if base_instance.const_defined?(*args) + base_instance.const_get(*args) else - namespace_inheritable(:cascade, value) + super end end - # see Grape::Router#recognize_path - def recognize_path(path) - LOCK.synchronize { compile } unless instance - instance.router.recognize_path(path) + # 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(**opts) + instance = Class.new(@base_parent) + instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {}) + instance.base = self + replay_setup_on(instance) + instance + 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 - protected + def respond_to?(method, include_private = false) + super(method, include_private) || base_instance.respond_to?(method, include_private) + end - def prepare_routes - endpoints.map(&:routes).flatten + def respond_to_missing?(method, include_private = false) + base_instance.respond_to?(method, include_private) 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.reject!(&:nil?) - if blocks.any? - instance_eval(&block) if block_given? - blocks.each { |b| instance_eval(&b) } - reset_validations! + def method_missing(method, *args, &block) + # If there's a missing method, it may be defined on the base_instance instead. + if respond_to_missing?(method) + base_instance.send(method, *args, &block) else - instance_eval(&block) + super end end - def inherited(subclass) - subclass.reset! - subclass.logger = logger.clone + def compile! + require 'grape/eager_load' + instance_for_rack.compile! # See API::Instance.compile! end - def inherit_settings(other_settings) - top_level_setting.inherit_from other_settings.point_in_time_copy + private - # 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! + def instance_for_rack + if never_mounted? + base_instance + else + mounted_instances.first end - - reset_routes! 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) + # Adds a new stage to the set up require to get a Grape::API up and running + def add_setup(method, *args, &block) + setup_step = { method: method, args: args, block: block } + @setup += [setup_step] + last_response = nil + @instances.each do |instance| + last_response = replay_step_on(instance, setup_step) + end + last_response end - @router.compile! - @router.freeze - end - - # Handle a request. See Rack documentation for what `env` is. - def call(env) - result = @router.call(env) - result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade? - result - 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? - return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade) - return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.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 - routes_map = {} - - self.class.endpoints.each do |endpoint| - routes = endpoint.routes - routes.each do |route| - # using the :any shorthand produces [nil] for route methods, substitute all manually - route_key = route.pattern.to_regexp - routes_map[route_key] ||= {} - route_settings = routes_map[route_key] - route_settings[:pattern] = route.pattern - route_settings[:requirements] = route.requirements - route_settings[:path] = route.origin - route_settings[:methods] ||= [] - route_settings[:methods] << route.request_method - route_settings[:endpoint] = route.app - - # using the :any shorthand produces [nil] for route methods, substitute all manually - route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*') + def replay_step_on(instance, setup_step) + return if skip_immediate_run?(instance, setup_step[:args]) + args = evaluate_arguments(instance.configuration, *setup_step[:args]) + response = instance.send(setup_step[:method], *args, &setup_step[:block]) + if skip_immediate_run?(instance, [response]) + response + else + evaluate_arguments(instance.configuration, response).first end end - # The paths we collected are prepared (cf. Path#prepare), so they - # contain already versioning information when using path versioning. - # Disable versioning so adding a route won't prepend versioning - # informations again. - without_root_prefix do - without_versioning do - routes_map.each do |_, config| - methods = config[:methods] - allowed_methods = methods.dup - - unless self.class.namespace_inheritable(:do_not_route_head) - allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET) - end - - allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ') + # Skips steps that contain arguments to be lazily executed (on re-mount time) + def skip_immediate_run?(instance, args) + instance.base_instance? && + (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) }) + end - unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) - config[:endpoint].options[:options_route_enabled] = true - end + def any_lazy?(args) + args.any? { |argument| argument.respond_to?(:lazy?) && argument.lazy? } + end - attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) - generate_not_allowed_method(config[:pattern], attributes) + def evaluate_arguments(configuration, *args) + args.map do |argument| + if argument.respond_to?(:lazy?) && argument.lazy? + 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 - end - - # Generate a route that returns an HTTP 405 response for a user defined - # path on methods not specified - def generate_not_allowed_method(pattern, allowed_methods: [], **attributes) - not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods - not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options) - return if not_allowed_methods.empty? - - @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes) - end - - # Allows definition of endpoints that ignore the versioning configuration - # used by the rest of your API. - def without_versioning(&_block) - old_version = self.class.namespace_inheritable(:version) - old_version_options = self.class.namespace_inheritable(:version_options) - - self.class.namespace_inheritable_to_nil(:version) - self.class.namespace_inheritable_to_nil(:version_options) - - yield - - self.class.namespace_inheritable(:version, old_version) - self.class.namespace_inheritable(:version_options, old_version_options) - end - - # Allows definition of endpoints that ignore the root prefix used by the - # rest of your API. - def without_root_prefix(&_block) - old_prefix = self.class.namespace_inheritable(:root_prefix) - - self.class.namespace_inheritable_to_nil(:root_prefix) - - yield + def never_mounted? + mounted_instances.empty? + end - self.class.namespace_inheritable(:root_prefix, old_prefix) + def mounted_instances + instances - [base_instance] + end end end end diff --git a/lib/grape/api/helpers.rb b/lib/grape/api/helpers.rb index 7fd69e7db..00da38f86 100644 --- a/lib/grape/api/helpers.rb +++ b/lib/grape/api/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape class API module Helpers diff --git a/lib/grape/api/instance.rb b/lib/grape/api/instance.rb new file mode 100644 index 000000000..e8f8d85fb --- /dev/null +++ b/lib/grape/api/instance.rb @@ -0,0 +1,283 @@ +# frozen_string_literal: true + +require 'grape/router' + +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 + include Grape::DSL::API + + class << self + attr_reader :instance + attr_reader :base + attr_accessor :configuration + + def given(conditional_option, &block) + evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given? + end + + def mounted(&block) + evaluate_as_instance_with_configuration(block, lazy: true) + end + + def base=(grape_api) + @base = grape_api + grape_api.instances << self + end + + def to_s + (base && base.to_s) || super + 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 + + # Parses the API's definition and compiles it into an instance of + # Grape::API. + def compile + @instance ||= new + end + + # Wipe the compiled API so we can recompile after changes were made. + def change! + @instance = nil + 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! + call!(env) + end + + # A non-synchronized version of ::call. + def call!(env) + instance.call(env) + end + + # (see #cascade?) + def cascade(value = nil) + if value.nil? + inheritable_setting.namespace_inheritable.key?(:cascade) ? !namespace_inheritable(:cascade).nil? : true + else + namespace_inheritable(:cascade, value) + end + end + + def compile! + return if instance + LOCK.synchronize { compile unless instance } + end + + # see Grape::Router#recognize_path + def recognize_path(path) + compile! + instance.router.recognize_path(path) + end + + protected + + def prepare_routes + endpoints.map(&:routes).flatten + 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.reject!(&:nil?) + if blocks.any? + evaluate_as_instance_with_configuration(block) if block_given? + 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::LazyBlock.new do |configuration| + value_for_configuration = configuration + if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy? + self.configuration = value_for_configuration.evaluate + end + 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 + + def inherited(subclass) + subclass.reset! + subclass.logger = logger.clone + end + + 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 + 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) + result = @router.call(env) + result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade? + result + 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? + return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(:cascade) + return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.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 + versioned_route_configs = collect_route_config_per_pattern + # The paths we collected are prepared (cf. Path#prepare), so they + # contain already versioning information when using path versioning. + # Disable versioning so adding a route won't prepend versioning + # informations again. + without_root_prefix do + without_versioning do + versioned_route_configs.each do |config| + allowed_methods = config[:methods].dup + + unless self.class.namespace_inheritable(:do_not_route_head) + allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET) + end + + allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods) + + unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS) + config[:endpoint].options[:options_route_enabled] = true + end + + attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header) + generate_not_allowed_method(config[:pattern], **attributes) + end + end + end + end + + def collect_route_config_per_pattern + all_routes = self.class.endpoints.map(&:routes).flatten + routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp } + + # Build the configuration based on the first endpoint and the collection of methods supported. + routes_by_regexp.values.map do |routes| + last_route = routes.last # Most of the configuration is taken from the last endpoint + matching_wildchar = routes.any? { |route| route.request_method == '*' } + { + options: {}, + pattern: last_route.pattern, + requirements: last_route.requirements, + path: last_route.origin, + endpoint: last_route.app, + methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method) + } + end + end + + # Generate a route that returns an HTTP 405 response for a user defined + # path on methods not specified + def generate_not_allowed_method(pattern, allowed_methods: [], **attributes) + supported_methods = + if self.class.namespace_inheritable(:do_not_route_options) + Grape::Http::Headers::SUPPORTED_METHODS + else + Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS + end + not_allowed_methods = supported_methods - allowed_methods + return if not_allowed_methods.empty? + @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes) + end + + # Allows definition of endpoints that ignore the versioning configuration + # used by the rest of your API. + def without_versioning(&_block) + old_version = self.class.namespace_inheritable(:version) + old_version_options = self.class.namespace_inheritable(:version_options) + + self.class.namespace_inheritable_to_nil(:version) + self.class.namespace_inheritable_to_nil(:version_options) + + yield + + self.class.namespace_inheritable(:version, old_version) + self.class.namespace_inheritable(:version_options, old_version_options) + end + + # Allows definition of endpoints that ignore the root prefix used by the + # rest of your API. + def without_root_prefix(&_block) + old_prefix = self.class.namespace_inheritable(:root_prefix) + + self.class.namespace_inheritable_to_nil(:root_prefix) + + yield + + self.class.namespace_inheritable(:root_prefix, old_prefix) + end + end + end +end diff --git a/lib/grape/config.rb b/lib/grape/config.rb new file mode 100644 index 000000000..6d76c06b1 --- /dev/null +++ b/lib/grape/config.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Grape + module Config + class Configuration + ATTRIBUTES = %i[ + param_builder + ].freeze + + attr_accessor(*ATTRIBUTES) + + def initialize + reset + end + + def reset + self.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + end + end + + def self.extended(base) + def base.configure + block_given? ? yield(config) : config + end + + def base.config + @configuration ||= Grape::Config::Configuration.new + end + end + end +end + +Grape.extend Grape::Config +Grape.config.reset diff --git a/lib/grape/content_types.rb b/lib/grape/content_types.rb new file mode 100644 index 000000000..2c19f9731 --- /dev/null +++ b/lib/grape/content_types.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'grape/util/registrable' + +module Grape + module ContentTypes + extend Util::Registrable + + # Content types are listed in order of preference. + CONTENT_TYPES = { + xml: 'application/xml', + serializable_hash: 'application/json', + json: 'application/json', + binary: 'application/octet-stream', + txt: 'text/plain' + }.freeze + + class << self + def content_types_for_settings(settings) + return if settings.blank? + + settings.each_with_object({}) { |value, result| result.merge!(value) } + end + + def content_types_for(from_settings) + if from_settings.present? + from_settings + else + Grape::ContentTypes::CONTENT_TYPES.merge(default_elements) + end + end + end + end +end diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 48a8389bc..6e37c6d32 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape class Cookies def initialize diff --git a/lib/grape/dsl/api.rb b/lib/grape/dsl/api.rb index 1883170e9..0543cac54 100644 --- a/lib/grape/dsl/api.rb +++ b/lib/grape/dsl/api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape diff --git a/lib/grape/dsl/callbacks.rb b/lib/grape/dsl/callbacks.rb index ede063c16..03827684d 100644 --- a/lib/grape/dsl/callbacks.rb +++ b/lib/grape/dsl/callbacks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -43,6 +45,26 @@ def after_validation(&block) def after(&block) namespace_stackable(:afters, block) end + + # Allows you to specify a something that will always be executed after a call + # API call. Unlike the `after` block, this code will run even on + # unsuccesful requests. + # @example + # class ExampleAPI < Grape::API + # before do + # ApiLogger.start + # end + # finally do + # ApiLogger.close + # end + # end + # + # This will make sure that the ApiLogger is opened and closed around every + # request + # @param ensured_block [Proc] The block to be executed after every api_call + def finally(&block) + namespace_stackable(:finallies, block) + end end end end diff --git a/lib/grape/dsl/configuration.rb b/lib/grape/dsl/configuration.rb index 3ee20f062..f33af3225 100644 --- a/lib/grape/dsl/configuration.rb +++ b/lib/grape/dsl/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape diff --git a/lib/grape/dsl/desc.rb b/lib/grape/dsl/desc.rb index e758bf2c9..8e94750c7 100644 --- a/lib/grape/dsl/desc.rb +++ b/lib/grape/dsl/desc.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module DSL module Desc @@ -9,6 +11,7 @@ module Desc # @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 @@ -16,7 +19,16 @@ module Desc # @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. @@ -39,7 +51,17 @@ module Desc # def desc(description, options = {}, &config_block) if block_given? - config_class = desc_container + endpoint_configuration = if defined?(configuration) + # When the instance is mounted - the configuration is executed on mount time + if configuration.respond_to?(:evaluate) + configuration.evaluate + # Within `given` or `mounted blocks` the configuration is already evaluated + elsif configuration.is_a?(Hash) + configuration + end + end + endpoint_configuration ||= {} + config_class = desc_container(endpoint_configuration) config_class.configure do description description @@ -59,13 +81,12 @@ def desc(description, options = {}, &config_block) end def description_field(field, value = nil) + description = route_setting(:description) if value - description = route_setting(:description) description ||= route_setting(:description, {}) description[field] = value - else - description = route_setting(:description) - description[field] if description + elsif description + description[field] end end @@ -75,17 +96,30 @@ def unset_description_field(field) end # Returns an object which configures itself via an instance-context DSL. - def desc_container + def desc_container(endpoint_configuration) Module.new do include Grape::Util::StrictHashConfiguration.module( + :summary, :description, :detail, :params, :entity, :http_codes, :named, - :headers + :body_name, + :headers, + :hidden, + :deprecated, + :is_array, + :nickname, + :produces, + :consumes, + :security, + :tags ) + config_context.define_singleton_method(:configuration) do + endpoint_configuration + end def config_context.success(*args) entity(*args) diff --git a/lib/grape/dsl/headers.rb b/lib/grape/dsl/headers.rb index cf7849352..c3c7bc3e8 100644 --- a/lib/grape/dsl/headers.rb +++ b/lib/grape/dsl/headers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module DSL module Headers diff --git a/lib/grape/dsl/helpers.rb b/lib/grape/dsl/helpers.rb index d58e2cf02..d461b34e3 100644 --- a/lib/grape/dsl/helpers.rb +++ b/lib/grape/dsl/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -65,7 +67,7 @@ def include_all_in_scope def define_boolean_in_mod(mod) return if defined? mod::Boolean - mod.const_set('Boolean', Virtus::Attribute::Boolean) + mod.const_set('Boolean', Grape::API::Boolean) end def inject_api_helpers_to_mod(mod, &_block) @@ -79,6 +81,7 @@ def inject_api_helpers_to_mod(mod, &_block) # to provide some API-specific functionality. module BaseHelper attr_accessor :api + def params(name, &block) @named_params ||= {} @named_params[name] = block @@ -92,7 +95,7 @@ def api_changed(new_api) protected def process_named_params - return unless @named_params && @named_params.any? + return unless instance_variable_defined?(:@named_params) && @named_params && @named_params.any? api.namespace_stackable(:named_params, @named_params) end end diff --git a/lib/grape/dsl/inside_route.rb b/lib/grape/dsl/inside_route.rb index b4672bb97..d12107199 100644 --- a/lib/grape/dsl/inside_route.rb +++ b/lib/grape/dsl/inside_route.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' require 'grape/dsl/headers' @@ -26,82 +28,92 @@ def self.post_filter_methods(type) # Methods which should not be available in filters until the before filter # has completed module PostBeforeFilter - def declared(passed_params, options = {}, declared_params = nil) + def declared(passed_params, options = {}, declared_params = nil, params_nested_path = []) options = options.reverse_merge(include_missing: true, include_parent_namespaces: true) - declared_params ||= optioned_declared_params(options) + declared_params ||= optioned_declared_params(**options) if passed_params.is_a?(Array) - declared_array(passed_params, options, declared_params) + declared_array(passed_params, options, declared_params, params_nested_path) else - declared_hash(passed_params, options, declared_params) + declared_hash(passed_params, options, declared_params, params_nested_path) end end private - def declared_array(passed_params, options, declared_params) + def declared_array(passed_params, options, declared_params, params_nested_path) passed_params.map do |passed_param| - declared(passed_param || {}, options, declared_params) + declared(passed_param || {}, options, declared_params, params_nested_path) end end - def declared_hash(passed_params, options, declared_params) + def declared_hash(passed_params, options, declared_params, params_nested_path) declared_params.each_with_object(passed_params.class.new) do |declared_param, memo| if declared_param.is_a?(Hash) declared_param.each_pair do |declared_parent_param, declared_children_params| + params_nested_path_dup = params_nested_path.dup + params_nested_path_dup << declared_parent_param.to_s next unless options[:include_missing] || passed_params.key?(declared_parent_param) passed_children_params = passed_params[declared_parent_param] || passed_params.class.new memo_key = optioned_param_key(declared_parent_param, options) - memo[memo_key] = handle_passed_param(declared_parent_param, passed_children_params) do - declared(passed_children_params, options, declared_children_params) + memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do + declared(passed_children_params, options, declared_children_params, params_nested_path_dup) end end else # If it is not a Hash then it does not have children. # Find its value or set it to nil. - has_alias = route_setting(:aliased_params) && route_setting(:aliased_params).find { |current| current[declared_param] } - param_alias = has_alias[declared_param] if has_alias + has_renaming = route_setting(:renamed_params) && route_setting(:renamed_params).find { |current| current[declared_param] } + param_renaming = has_renaming[declared_param] if has_renaming + + next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming)) - next unless options[:include_missing] || passed_params.key?(declared_param) || (param_alias && passed_params.key?(param_alias)) + memo_key = optioned_param_key(param_renaming || declared_param, options) + passed_param = passed_params[param_renaming || declared_param] - if param_alias - memo[optioned_param_key(param_alias, options)] = passed_params[param_alias] - else - memo[optioned_param_key(declared_param, options)] = 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) do + passed_param end end end end - def handle_passed_param(declared_param, passed_children_params, &_block) - should_be_empty_array?(declared_param, passed_children_params) ? [] : yield - end + def handle_passed_param(params_nested_path, has_passed_children = false, &_block) + return yield if has_passed_children - def should_be_empty_array?(declared_param, passed_children_params) - declared_param_is_array?(declared_param) && passed_children_params.empty? - end + key = params_nested_path[0] + key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1 - def declared_param_is_array?(declared_param) - route_options_params[declared_param.to_s] && route_options_params[declared_param.to_s][:type] == 'Array' - end + route_options_params = options[:route_options][:params] || {} + type = route_options_params.dig(key, :type) + has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) } - def route_options_params - options[:route_options][:params] || {} + if type == 'Hash' && !has_children + {} + elsif type == 'Array' || type&.start_with?('[') && !type&.include?(',') + [] + elsif type == 'Set' || type&.start_with?('# "contents of file" - def file(value = nil) + def sendfile(value = nil) if value.is_a?(String) - file_body = Grape::ServeFile::FileBody.new(value) - @file = Grape::ServeFile::FileResponse.new(file_body) + file_body = Grape::ServeStream::FileBody.new(value) + @stream = Grape::ServeStream::StreamResponse.new(file_body) elsif !value.is_a?(NilClass) - warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.' - @file = Grape::ServeFile::FileResponse.new(value) + raise ArgumentError, 'Argument must be a file path' else - @file + stream end end @@ -283,10 +314,21 @@ def file(value = nil) # * 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 'Content-Length', nil header 'Transfer-Encoding', nil header 'Cache-Control', 'no-cache' # Skips ETag generation (reading the response up front) - file(value) + 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 @@ -322,11 +364,12 @@ def present(*args) end representation = { root => representation } if root + if key - representation = (@body || {}).merge(key => representation) - elsif entity_class.present? && @body + 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) + representation = body.merge(representation) end body representation @@ -356,7 +399,7 @@ def entity_class_for_obj(object, options) entity_class = options.delete(:with) if entity_class.nil? - # entity class not explicitely defined, auto-detect from relation#klass or first object in the collection + # 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 @@ -378,7 +421,7 @@ def entity_class_for_obj(object, options) def entity_representation_for(entity_class, object, options) embeds = { env: env } embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION] - entity_class.represent(object, embeds.merge(options)) + entity_class.represent(object, **embeds.merge(options)) end end end diff --git a/lib/grape/dsl/logger.rb b/lib/grape/dsl/logger.rb index 40cccdaac..516cdcd7d 100644 --- a/lib/grape/dsl/logger.rb +++ b/lib/grape/dsl/logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module DSL module Logger diff --git a/lib/grape/dsl/middleware.rb b/lib/grape/dsl/middleware.rb index 7026f8995..f07f310b1 100644 --- a/lib/grape/dsl/middleware.rb +++ b/lib/grape/dsl/middleware.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -21,6 +23,13 @@ def use(middleware_class, *args, &block) namespace_stackable(:middleware, arr) end + def insert(*args, &block) + arr = [:insert, *args] + arr << block if block_given? + + namespace_stackable(:middleware, arr) + end + def insert_before(*args, &block) arr = [:insert_before, *args] arr << block if block_given? diff --git a/lib/grape/dsl/parameters.rb b/lib/grape/dsl/parameters.rb index bb3196b06..84a0fa574 100644 --- a/lib/grape/dsl/parameters.rb +++ b/lib/grape/dsl/parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -70,7 +72,7 @@ def use(*names) # Require one or more parameters for the current endpoint. # - # @param attrs list of parameter names, or, if :using is + # @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 @@ -125,13 +127,13 @@ def requires(*attrs, &block) opts = attrs.extract_options!.clone opts[:presence] = { value: true, message: opts[:message] } - opts = @group.merge(opts) if @group + opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group if opts[:using] require_required_and_optional_fields(attrs.first, opts) else validate_attributes(attrs, opts, &block) - block_given? ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as)) + block_given? ? new_scope(orig_attrs, &block) : push_declared_params(attrs, **opts.slice(:as)) end end @@ -144,7 +146,7 @@ def optional(*attrs, &block) opts = attrs.extract_options!.clone type = opts[:type] - opts = @group.merge(opts) if @group + opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group # check type for optional parameter group if attrs && block_given? @@ -157,7 +159,7 @@ def optional(*attrs, &block) else validate_attributes(attrs, opts, &block) - block_given? ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as)) + block_given? ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, **opts.slice(:as)) end end @@ -211,22 +213,31 @@ def given(*attrs, &block) # block yet. # @return [Boolean] whether the parameter has been defined def declared_param?(param) - # @declared_params also includes hashes of options and such, but those - # won't be flattened out. - @declared_params.flatten.any? do |declared_param| - first_hash_key_or_param(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| + first_hash_key_or_param(declared_param) == param + end end end alias group requires - def map_params(params, element) + class EmptyOptionalValue; end + + def map_params(params, element, is_array = false) if params.is_a?(Array) params.map do |el| - map_params(el, element) + map_params(el, element, true) end elsif params.is_a?(Hash) - params[element] || {} + params[element] || (@optional && is_array ? EmptyOptionalValue : {}) + elsif params == EmptyOptionalValue + EmptyOptionalValue else {} end @@ -236,8 +247,8 @@ def map_params(params, element) # @return hash of parameters relevant for the current scope # @api private def params(params) - params = @parent.params(params) if @parent - params = map_params(params, @element) if @element + params = @parent.params(params) if instance_variable_defined?(:@parent) && @parent + params = map_params(params, @element) if instance_variable_defined?(:@element) && @element params end diff --git a/lib/grape/dsl/request_response.rb b/lib/grape/dsl/request_response.rb index 654a31cdb..cbb1db912 100644 --- a/lib/grape/dsl/request_response.rb +++ b/lib/grape/dsl/request_response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -20,7 +22,7 @@ def format(new_format = nil) if new_format namespace_inheritable(:format, new_format.to_sym) # define the default error formatters - namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(new_format, {})) + namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(new_format, **{})) # define a single mime type mime_type = content_types[new_format.to_sym] raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type @@ -43,7 +45,7 @@ def parser(content_type, new_parser) # Specify a default error formatter. def default_error_formatter(new_formatter_name = nil) if new_formatter_name - new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name, {}) + new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name, **{}) namespace_inheritable(:default_error_formatter, new_formatter) else namespace_inheritable(:default_error_formatter) diff --git a/lib/grape/dsl/routing.rb b/lib/grape/dsl/routing.rb index fcde3dbda..7f1e78c08 100644 --- a/lib/grape/dsl/routing.rb +++ b/lib/grape/dsl/routing.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -49,7 +51,7 @@ def version(*args, &block) end end - @versions.last unless @versions.nil? + @versions.last if instance_variable_defined?(:@versions) && @versions end # Define a root URL prefix for your entire API. @@ -77,9 +79,14 @@ def do_not_route_options! namespace_inheritable(:do_not_route_options, true) end - def mount(mounts) + 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.shift[:with] : {} + mount({ app.mount_instance(configuration: opts_with) => path }) + next + end in_setting = inheritable_setting if app.respond_to?(:inheritable_setting, true) @@ -136,16 +143,16 @@ def route(methods, paths = ['/'], route_options = {}, &block) reset_validations! end - %w[get post put head delete options patch].each do |meth| - define_method meth do |*args, &block| + Grape::Http::Headers::SUPPORTED_METHODS.each do |supported_method| + define_method supported_method.downcase do |*args, &block| options = args.extract_options! paths = args.first || ['/'] - route(meth.upcase, paths, options, &block) + route(supported_method, paths, options, &block) end end # Declare a "namespace", which prefixes all subordinate routes with its - # name. Any endpoints within a namespace, or group, resource, segment, + # 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. # @@ -157,14 +164,14 @@ def route(methods, paths = ['/'], route_options = {}, &block) # end # end def namespace(space = nil, options = {}, &block) + @namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description + if space || block_given? within_namespace do previous_namespace_description = @namespace_description @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {}) nest(block) do - if space - namespace_stackable(:namespace, Namespace.new(space, options)) - end + namespace_stackable(:namespace, Namespace.new(space, **options)) if space end @namespace_description = previous_namespace_description end @@ -193,7 +200,7 @@ def reset_endpoints! @endpoints = [] end - # Thie method allows you to quickly define a parameter route segment + # 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. diff --git a/lib/grape/dsl/settings.rb b/lib/grape/dsl/settings.rb index 9fe24f83a..706f8adb6 100644 --- a/lib/grape/dsl/settings.rb +++ b/lib/grape/dsl/settings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -169,7 +171,11 @@ def within_namespace(&_block) # the superclass's :inheritable_setting. def build_top_level_setting Grape::Util::InheritableSetting.new.tap do |setting| - if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API + # 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. + if defined?(superclass) && superclass.respond_to?(:inheritable_setting) && superclass != Grape::API::Instance setting.inherit_from superclass.inheritable_setting end end diff --git a/lib/grape/dsl/validations.rb b/lib/grape/dsl/validations.rb index 56208fac4..d2d354fb1 100644 --- a/lib/grape/dsl/validations.rb +++ b/lib/grape/dsl/validations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/concern' module Grape @@ -8,7 +10,24 @@ module Validations include Grape::DSL::Configuration module ClassMethods - # Clears all defined parameters and validations. + # 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! unset_namespace_stackable :declared_params unset_namespace_stackable :validations @@ -27,10 +46,11 @@ def document_attribute(names, opts) setting = description_field(:params) setting ||= description_field(:params, {}) Array(names).each do |name| - setting[name[:full_name].to_s] ||= {} - setting[name[:full_name].to_s].merge!(opts) + full_name = name[:full_name].to_s + setting[full_name] ||= {} + setting[full_name].merge!(opts) - namespace_stackable(:params, name[:full_name].to_s => opts) + namespace_stackable(:params, full_name => opts) end end end diff --git a/lib/grape/eager_load.rb b/lib/grape/eager_load.rb new file mode 100644 index 000000000..ef7bc3ec7 --- /dev/null +++ b/lib/grape/eager_load.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +Grape.eager_load! +Grape::Http.eager_load! +Grape::Exceptions.eager_load! +Grape::Extensions.eager_load! +Grape::Extensions::ActiveSupport.eager_load! +Grape::Extensions::Hashie.eager_load! +Grape::Middleware.eager_load! +Grape::Middleware::Auth.eager_load! +Grape::Middleware::Versioner.eager_load! +Grape::Util.eager_load! +Grape::ErrorFormatter.eager_load! +Grape::Formatter.eager_load! +Grape::Parser.eager_load! +Grape::DSL.eager_load! +Grape::API.eager_load! +Grape::Presenters.eager_load! +Grape::ServeStream.eager_load! +Rack::Head # AutoLoads the Rack::Head diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index aacf0472a..068e1ca0a 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape # An Endpoint is the proxy scope in which all routing # blocks are executed. In other words, any methods @@ -78,7 +80,10 @@ def initialize(new_settings, options = {}, &block) self.inheritable_setting = new_settings.point_in_time_copy - route_setting(:saved_declared_params, namespace_stackable(:declared_params)) + # 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 + route_setting(:declared_params, namespace_stackable(:declared_params).flatten) route_setting(:saved_validations, namespace_stackable(:validations)) namespace_stackable(:representations, []) unless namespace_stackable(:representations) @@ -97,7 +102,7 @@ def initialize(new_settings, options = {}, &block) @block = nil @status = nil - @file = nil + @stream = nil @body = nil @proc = nil @@ -114,7 +119,6 @@ def inherit_settings(namespace_stackable) parent_declared_params = namespace_stackable[:declared_params] if parent_declared_params - inheritable_setting.route[:declared_params] ||= [] inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) end @@ -154,8 +158,8 @@ def mount_in(router) methods << Grape::Http::Headers::HEAD end methods.each do |method| - unless route.request_method.to_s.upcase == method - route = Grape::Router::Route.new(method, route.origin, route.attributes.to_h) + unless route.request_method == method + route = Grape::Router::Route.new(method, route.origin, **route.attributes.to_h) end router.append(route.apply(self)) end @@ -167,8 +171,8 @@ def to_routes route_options = prepare_default_route_attributes map_routes do |method, path| path = prepare_path(path) - params = merge_route_options(route_options.merge(suffix: path.suffix)) - route = Router::Route.new(method, path.path, params) + params = merge_route_options(**route_options.merge(suffix: path.suffix)) + route = Router::Route.new(method, path.path, **params) route.apply(self) end.flatten end @@ -188,7 +192,7 @@ def prepare_default_route_attributes requirements: prepare_routes_requirements, prefix: namespace_inheritable(:root_prefix), anchor: options[:route_options].fetch(:anchor, true), - settings: inheritable_setting.route.except(:saved_declared_params, :saved_validations), + settings: inheritable_setting.route.except(:declared_params, :saved_validations), forward_match: options[:forward_match] } end @@ -200,7 +204,7 @@ def prepare_version end def merge_route_options(**default) - options[:route_options].clone.reverse_merge(**default) + options[:route_options].clone.merge!(**default) end def map_routes @@ -245,32 +249,37 @@ def run @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with)) @params = @request.params @headers = @request.headers + begin + cookies.read(@request) + self.class.run_before_each(self) + run_filters befores, :before + + if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) + raise Grape::Exceptions::MethodNotAllowed.new(header.merge('Allow' => allowed_methods)) unless options? + header 'Allow', allowed_methods + response_object = '' + status 204 + else + run_filters before_validations, :before_validation + run_validators validations, request + remove_renamed_params + run_filters after_validations, :after_validation + response_object = execute + end - cookies.read(@request) - self.class.run_before_each(self) - run_filters befores, :before + run_filters afters, :after + cookies.write(header) - if (allowed_methods = env[Grape::Env::GRAPE_ALLOWED_METHODS]) - raise Grape::Exceptions::MethodNotAllowed, header.merge('Allow' => allowed_methods) unless options? - header 'Allow', allowed_methods - response_object = '' - status 204 - else - run_filters before_validations, :before_validation - run_validators validations, request - run_filters after_validations, :after_validation - response_object = @block ? @block.call(self) : nil - end + # status verifies body presence when DELETE + @body ||= response_object - run_filters afters, :after - cookies.write(header) + # The body commonly is an Array of Strings, the application instance itself, or a Stream-like object + response_object = stream || [body] - # status verifies body presence when DELETE - @body ||= response_object - - # The Body commonly is an Array of Strings, the application instance itself, or a File-like object - response_object = file || [body] - [status, header, response_object] + [status, header, response_object] + ensure + run_filters finallies, :finally + end end end @@ -319,7 +328,18 @@ def build_helpers Module.new { helpers.each { |mod_to_include| include mod_to_include } } end - private :build_stack, :build_helpers + def remove_renamed_params + return unless route_setting(:renamed_params) + route_setting(:renamed_params).flat_map(&:keys).each do |renamed_param| + @params.delete(renamed_param) + end + end + + private :build_stack, :build_helpers, :remove_renamed_params + + def execute + @block ? @block.call(self) : nil + end def helpers lazy_initialize! && @helpers @@ -341,7 +361,7 @@ def lazy_initialize! def run_validators(validator_factories, request) validation_errors = [] - validators = validator_factories.map(&:create_validator) + validators = validator_factories.map { |options| Grape::Validations::ValidatorFactory.create_validator(**options) } ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do validators.each do |validator| @@ -351,13 +371,13 @@ def run_validators(validator_factories, request) validation_errors << e break if validator.fail_fast? rescue Grape::Exceptions::ValidationArrayErrors => e - validation_errors += e.errors + validation_errors.concat e.errors break if validator.fail_fast? end end end - validation_errors.any? && raise(Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header) + validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header)) end def run_filters(filters, type = :other) @@ -384,6 +404,10 @@ def afters namespace_stackable(:afters) || [] end + def finallies + namespace_stackable(:finallies) || [] + end + def validations route_setting(:saved_validations) || [] end diff --git a/lib/grape/error_formatter.rb b/lib/grape/error_formatter.rb index f63e8e510..4d76fe296 100644 --- a/lib/grape/error_formatter.rb +++ b/lib/grape/error_formatter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module ErrorFormatter extend Util::Registrable @@ -13,8 +15,8 @@ def builtin_formatters } end - def formatters(options) - builtin_formatters.merge(default_elements).merge(options[:error_formatters] || {}) + def formatters(**options) + builtin_formatters.merge(default_elements).merge!(options[:error_formatters] || {}) end def formatter_for(api_format, **options) diff --git a/lib/grape/error_formatter/base.rb b/lib/grape/error_formatter/base.rb index 60103a600..f0c802a45 100644 --- a/lib/grape/error_formatter/base.rb +++ b/lib/grape/error_formatter/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module ErrorFormatter module Base diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index d3ebff765..6c160e099 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module ErrorFormatter module Json diff --git a/lib/grape/error_formatter/txt.rb b/lib/grape/error_formatter/txt.rb index b58e03152..76d3cb3f1 100644 --- a/lib/grape/error_formatter/txt.rb +++ b/lib/grape/error_formatter/txt.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module ErrorFormatter module Txt diff --git a/lib/grape/error_formatter/xml.rb b/lib/grape/error_formatter/xml.rb index 73ebf09c8..39ea87387 100644 --- a/lib/grape/error_formatter/xml.rb +++ b/lib/grape/error_formatter/xml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module ErrorFormatter module Xml diff --git a/lib/grape/exceptions/base.rb b/lib/grape/exceptions/base.rb index d54fe6efe..9eeba9301 100644 --- a/lib/grape/exceptions/base.rb +++ b/lib/grape/exceptions/base.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Grape module Exceptions class Base < StandardError - BASE_MESSAGES_KEY = 'grape.errors.messages'.freeze - BASE_ATTRIBUTES_KEY = 'grape.errors.attributes'.freeze + BASE_MESSAGES_KEY = 'grape.errors.messages' + BASE_ATTRIBUTES_KEY = 'grape.errors.attributes' FALLBACK_LOCALE = :en attr_reader :status, :message, :headers @@ -28,7 +30,7 @@ def compose_message(key, **attributes) @problem = problem(key, **attributes) @summary = summary(key, **attributes) @resolution = resolution(key, **attributes) - [['Problem', @problem], ['Summary', @summary], ['Resolution', @resolution]].reduce('') do |message, detail_array| + [['Problem', @problem], ['Summary', @summary], ['Resolution', @resolution]].each_with_object(+'') do |detail_array, message| message << "\n#{detail_array[0]}:\n #{detail_array[1]}" unless detail_array[1].blank? message end @@ -37,16 +39,16 @@ def compose_message(key, **attributes) end end - def problem(key, attributes) - translate_message("#{key}.problem".to_sym, attributes) + def problem(key, **attributes) + translate_message("#{key}.problem".to_sym, **attributes) end - def summary(key, attributes) - translate_message("#{key}.summary".to_sym, attributes) + def summary(key, **attributes) + translate_message("#{key}.summary".to_sym, **attributes) end - def resolution(key, attributes) - translate_message("#{key}.resolution".to_sym, attributes) + def resolution(key, **attributes) + translate_message("#{key}.resolution".to_sym, **attributes) end def translate_attributes(keys, **options) @@ -55,10 +57,6 @@ def translate_attributes(keys, **options) end.join(', ') end - def translate_attribute(key, **options) - translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options) - end - def translate_message(key, **options) case key when Symbol @@ -74,7 +72,15 @@ def translate(key, **options) options = options.dup options[:default] &&= options[:default].to_s message = ::I18n.translate(key, **options) - message.present? ? message : ::I18n.translate(key, locale: FALLBACK_LOCALE, **options) + message.present? ? message : fallback_message(key, **options) + end + + def fallback_message(key, **options) + if ::I18n.enforce_available_locales && !::I18n.available_locales.include?(FALLBACK_LOCALE) + key + else + ::I18n.translate(key, locale: FALLBACK_LOCALE, **options) + end end end end diff --git a/lib/grape/exceptions/empty_message_body.rb b/lib/grape/exceptions/empty_message_body.rb new file mode 100644 index 000000000..c4fd43176 --- /dev/null +++ b/lib/grape/exceptions/empty_message_body.rb @@ -0,0 +1,11 @@ +# 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 diff --git a/lib/grape/exceptions/incompatible_option_values.rb b/lib/grape/exceptions/incompatible_option_values.rb index 971bdeab1..aabe8d5eb 100644 --- a/lib/grape/exceptions/incompatible_option_values.rb +++ b/lib/grape/exceptions/incompatible_option_values.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class IncompatibleOptionValues < Base diff --git a/lib/grape/exceptions/invalid_accept_header.rb b/lib/grape/exceptions/invalid_accept_header.rb index faafbb439..fd98849c0 100644 --- a/lib/grape/exceptions/invalid_accept_header.rb +++ b/lib/grape/exceptions/invalid_accept_header.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidAcceptHeader < Base diff --git a/lib/grape/exceptions/invalid_formatter.rb b/lib/grape/exceptions/invalid_formatter.rb index d3e5ac9e0..8c8a82f09 100644 --- a/lib/grape/exceptions/invalid_formatter.rb +++ b/lib/grape/exceptions/invalid_formatter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidFormatter < Base diff --git a/lib/grape/exceptions/invalid_message_body.rb b/lib/grape/exceptions/invalid_message_body.rb index 9e4b6eec3..ac9c2efbf 100644 --- a/lib/grape/exceptions/invalid_message_body.rb +++ b/lib/grape/exceptions/invalid_message_body.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidMessageBody < Base diff --git a/lib/grape/exceptions/invalid_response.rb b/lib/grape/exceptions/invalid_response.rb new file mode 100644 index 000000000..197f8399f --- /dev/null +++ b/lib/grape/exceptions/invalid_response.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Grape + module Exceptions + class InvalidResponse < Base + def initialize + super(message: compose_message(:invalid_response)) + end + end + end +end diff --git a/lib/grape/exceptions/invalid_version_header.rb b/lib/grape/exceptions/invalid_version_header.rb index b9273bb46..cb01ec3d1 100644 --- a/lib/grape/exceptions/invalid_version_header.rb +++ b/lib/grape/exceptions/invalid_version_header.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidVersionHeader < Base diff --git a/lib/grape/exceptions/invalid_versioner_option.rb b/lib/grape/exceptions/invalid_versioner_option.rb index ea3ca0755..9411370b0 100644 --- a/lib/grape/exceptions/invalid_versioner_option.rb +++ b/lib/grape/exceptions/invalid_versioner_option.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidVersionerOption < Base diff --git a/lib/grape/exceptions/invalid_with_option_for_represent.rb b/lib/grape/exceptions/invalid_with_option_for_represent.rb index 04d380fb3..d6a275fb9 100644 --- a/lib/grape/exceptions/invalid_with_option_for_represent.rb +++ b/lib/grape/exceptions/invalid_with_option_for_represent.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class InvalidWithOptionForRepresent < Base diff --git a/lib/grape/exceptions/method_not_allowed.rb b/lib/grape/exceptions/method_not_allowed.rb index 91fedd611..5777e4c29 100644 --- a/lib/grape/exceptions/method_not_allowed.rb +++ b/lib/grape/exceptions/method_not_allowed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class MethodNotAllowed < Base diff --git a/lib/grape/exceptions/missing_group_type.rb b/lib/grape/exceptions/missing_group_type.rb index 717e2412e..398113ff8 100644 --- a/lib/grape/exceptions/missing_group_type.rb +++ b/lib/grape/exceptions/missing_group_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class MissingGroupTypeError < Base diff --git a/lib/grape/exceptions/missing_mime_type.rb b/lib/grape/exceptions/missing_mime_type.rb index 3fc296def..5bf43a1dc 100644 --- a/lib/grape/exceptions/missing_mime_type.rb +++ b/lib/grape/exceptions/missing_mime_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class MissingMimeType < Base diff --git a/lib/grape/exceptions/missing_option.rb b/lib/grape/exceptions/missing_option.rb index e37e4c368..ec35b96e8 100644 --- a/lib/grape/exceptions/missing_option.rb +++ b/lib/grape/exceptions/missing_option.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class MissingOption < Base diff --git a/lib/grape/exceptions/missing_vendor_option.rb b/lib/grape/exceptions/missing_vendor_option.rb index ae5750fec..46e04885f 100644 --- a/lib/grape/exceptions/missing_vendor_option.rb +++ b/lib/grape/exceptions/missing_vendor_option.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class MissingVendorOption < Base diff --git a/lib/grape/exceptions/unknown_options.rb b/lib/grape/exceptions/unknown_options.rb index 59937f52b..0512a8778 100644 --- a/lib/grape/exceptions/unknown_options.rb +++ b/lib/grape/exceptions/unknown_options.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class UnknownOptions < Base diff --git a/lib/grape/exceptions/unknown_parameter.rb b/lib/grape/exceptions/unknown_parameter.rb index 76fada189..0168ff39d 100644 --- a/lib/grape/exceptions/unknown_parameter.rb +++ b/lib/grape/exceptions/unknown_parameter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class UnknownParameter < Base diff --git a/lib/grape/exceptions/unknown_validator.rb b/lib/grape/exceptions/unknown_validator.rb index ddebcde86..e856f2b36 100644 --- a/lib/grape/exceptions/unknown_validator.rb +++ b/lib/grape/exceptions/unknown_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class UnknownValidator < Base diff --git a/lib/grape/exceptions/unsupported_group_type.rb b/lib/grape/exceptions/unsupported_group_type.rb index 9602e7331..9cbc7aac2 100644 --- a/lib/grape/exceptions/unsupported_group_type.rb +++ b/lib/grape/exceptions/unsupported_group_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class UnsupportedGroupTypeError < Base diff --git a/lib/grape/exceptions/validation.rb b/lib/grape/exceptions/validation.rb index c4aba6a15..6b98112ab 100644 --- a/lib/grape/exceptions/validation.rb +++ b/lib/grape/exceptions/validation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/exceptions/base' module Grape @@ -12,10 +14,10 @@ def initialize(params:, message: nil, **args) @message_key = message if message.is_a?(Symbol) args[:message] = translate_message(message) end - super(args) + super(**args) end - # remove all the unnecessary stuff from Grape::Exceptions::Base like status + # 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 diff --git a/lib/grape/exceptions/validation_array_errors.rb b/lib/grape/exceptions/validation_array_errors.rb index 9e672b90a..d596c8877 100644 --- a/lib/grape/exceptions/validation_array_errors.rb +++ b/lib/grape/exceptions/validation_array_errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Exceptions class ValidationArrayErrors < Base diff --git a/lib/grape/exceptions/validation_errors.rb b/lib/grape/exceptions/validation_errors.rb index 92bf5d590..20f7dd2bf 100644 --- a/lib/grape/exceptions/validation_errors.rb +++ b/lib/grape/exceptions/validation_errors.rb @@ -1,8 +1,13 @@ +# frozen_string_literal: true + require 'grape/exceptions/base' module Grape module Exceptions class ValidationErrors < Grape::Exceptions::Base + ERRORS_FORMAT_KEY = 'grape.errors.format' + DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}' + include Enumerable attr_reader :errors @@ -34,23 +39,21 @@ def as_json(**_opts) end end - def to_json(**_opts) + def to_json(*_opts) as_json.to_json end def full_messages - map { |attributes, error| full_message(attributes, error) }.uniq - end - - private - - def full_message(attributes, error) - I18n.t( - 'grape.errors.format'.to_sym, - default: '%{attributes} %{message}', - attributes: attributes.count == 1 ? translate_attribute(attributes.first) : translate_attributes(attributes), - message: error.message - ) + messages = map do |attributes, error| + I18n.t( + ERRORS_FORMAT_KEY, + default: DEFAULT_ERRORS_FORMAT, + attributes: translate_attributes(attributes), + message: error.message + ) + end + messages.uniq! + messages end end end diff --git a/lib/grape/extensions/active_support/hash_with_indifferent_access.rb b/lib/grape/extensions/active_support/hash_with_indifferent_access.rb index c6bf2ca1a..d412c91dd 100644 --- a/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +++ b/lib/grape/extensions/active_support/hash_with_indifferent_access.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Extensions module ActiveSupport @@ -14,10 +16,9 @@ def params_builder end def build_params - params = ::ActiveSupport::HashWithIndifferentAccess[rack_params] + params = ::ActiveSupport::HashWithIndifferentAccess.new(rack_params) params.deep_merge!(grape_routing_args) if env[Grape::Env::GRAPE_ROUTING_ARGS] - # TODO: remove, in Rails 4 or later ::ActiveSupport::HashWithIndifferentAccess converts nested Hashes into indifferent access ones - DeepHashWithIndifferentAccess.deep_hash_with_indifferent_access(params) + params end end end diff --git a/lib/grape/extensions/deep_hash_with_indifferent_access.rb b/lib/grape/extensions/deep_hash_with_indifferent_access.rb deleted file mode 100644 index e18601f8d..000000000 --- a/lib/grape/extensions/deep_hash_with_indifferent_access.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Grape - module Extensions - module DeepHashWithIndifferentAccess - def self.deep_hash_with_indifferent_access(object) - case object - when ::Hash - object.inject(::ActiveSupport::HashWithIndifferentAccess.new) do |new_hash, (key, value)| - new_hash.merge!(key => deep_hash_with_indifferent_access(value)) - end - when ::Array - object.map { |element| deep_hash_with_indifferent_access(element) } - else - object - end - end - end - end -end diff --git a/lib/grape/extensions/deep_mergeable_hash.rb b/lib/grape/extensions/deep_mergeable_hash.rb index 8f9903d62..825bbe892 100644 --- a/lib/grape/extensions/deep_mergeable_hash.rb +++ b/lib/grape/extensions/deep_mergeable_hash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Extensions class DeepMergeableHash < ::Hash diff --git a/lib/grape/extensions/deep_symbolize_hash.rb b/lib/grape/extensions/deep_symbolize_hash.rb index 310a658af..6c131a97b 100644 --- a/lib/grape/extensions/deep_symbolize_hash.rb +++ b/lib/grape/extensions/deep_symbolize_hash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Extensions module DeepSymbolizeHash diff --git a/lib/grape/extensions/hash.rb b/lib/grape/extensions/hash.rb index 77d075763..990dddfbc 100644 --- a/lib/grape/extensions/hash.rb +++ b/lib/grape/extensions/hash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Extensions module Hash diff --git a/lib/grape/extensions/hashie/mash.rb b/lib/grape/extensions/hashie/mash.rb index 6afa106d9..545e71952 100644 --- a/lib/grape/extensions/hashie/mash.rb +++ b/lib/grape/extensions/hashie/mash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Extensions module Hashie diff --git a/lib/grape/formatter.rb b/lib/grape/formatter.rb index 5047e722a..4a84f0e2b 100644 --- a/lib/grape/formatter.rb +++ b/lib/grape/formatter.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module Grape module Formatter extend Util::Registrable class << self - def builtin_formmaters + def builtin_formatters @builtin_formatters ||= { json: Grape::Formatter::Json, jsonapi: Grape::Formatter::Json, @@ -13,8 +15,8 @@ def builtin_formmaters } end - def formatters(options) - builtin_formmaters.merge(default_elements).merge(options[:formatters] || {}) + def formatters(**options) + builtin_formatters.merge(default_elements).merge!(options[:formatters] || {}) end def formatter_for(api_format, **options) diff --git a/lib/grape/formatter/json.rb b/lib/grape/formatter/json.rb index 5f96918b2..753468a3c 100644 --- a/lib/grape/formatter/json.rb +++ b/lib/grape/formatter/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Formatter module Json diff --git a/lib/grape/formatter/serializable_hash.rb b/lib/grape/formatter/serializable_hash.rb index c31fcebb0..5b6256d59 100644 --- a/lib/grape/formatter/serializable_hash.rb +++ b/lib/grape/formatter/serializable_hash.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Formatter module SerializableHash diff --git a/lib/grape/formatter/txt.rb b/lib/grape/formatter/txt.rb index 175944ede..1f1f9ef2f 100644 --- a/lib/grape/formatter/txt.rb +++ b/lib/grape/formatter/txt.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Formatter module Txt diff --git a/lib/grape/formatter/xml.rb b/lib/grape/formatter/xml.rb index 2e6853920..9db91d015 100644 --- a/lib/grape/formatter/xml.rb +++ b/lib/grape/formatter/xml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Formatter module Xml diff --git a/lib/grape/http/headers.rb b/lib/grape/http/headers.rb index c1e587470..564d97ff8 100644 --- a/lib/grape/http/headers.rb +++ b/lib/grape/http/headers.rb @@ -1,29 +1,61 @@ +# frozen_string_literal: true + +require 'grape/util/lazy_object' + module Grape module Http module Headers # https://github.com/rack/rack/blob/master/lib/rack.rb - HTTP_VERSION = 'HTTP_VERSION'.freeze - PATH_INFO = 'PATH_INFO'.freeze - REQUEST_METHOD = 'REQUEST_METHOD'.freeze - QUERY_STRING = 'QUERY_STRING'.freeze - CONTENT_TYPE = 'Content-Type'.freeze - - GET = 'GET'.freeze - POST = 'POST'.freeze - PUT = 'PUT'.freeze - PATCH = 'PATCH'.freeze - DELETE = 'DELETE'.freeze - HEAD = 'HEAD'.freeze - OPTIONS = 'OPTIONS'.freeze + HTTP_VERSION = 'HTTP_VERSION' + PATH_INFO = 'PATH_INFO' + REQUEST_METHOD = 'REQUEST_METHOD' + QUERY_STRING = 'QUERY_STRING' + CONTENT_TYPE = 'Content-Type' + + GET = 'GET' + POST = 'POST' + PUT = 'PUT' + PATCH = 'PATCH' + DELETE = 'DELETE' + HEAD = 'HEAD' + OPTIONS = 'OPTIONS' SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze + SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze } + + HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION' + X_CASCADE = 'X-Cascade' + HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING' + HTTP_ACCEPT = 'HTTP_ACCEPT' + + FORMAT = 'format' - HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'.freeze - X_CASCADE = 'X-Cascade'.freeze - HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze - HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze + HTTP_HEADERS = Grape::Util::LazyObject.new do + common_http_headers = %w[ + Version + Host + Connection + Cache-Control + Dnt + Upgrade-Insecure-Requests + User-Agent + Sec-Fetch-Dest + Accept + Sec-Fetch-Site + Sec-Fetch-Mode + Sec-Fetch-User + Accept-Encoding + Accept-Language + Cookie + ].freeze + common_http_headers.each_with_object({}) do |header, response| + response["HTTP_#{header.upcase.tr('-', '_')}"] = header + end.freeze + end - FORMAT = 'format'.freeze + def self.find_supported_method(route_method) + Grape::Http::Headers::SUPPORTED_METHODS.detect { |supported_method| supported_method.casecmp(route_method).zero? } + end end end end diff --git a/lib/grape/locale/en.yml b/lib/grape/locale/en.yml index b78de1ed4..036eb93b8 100644 --- a/lib/grape/locale/en.yml +++ b/lib/grape/locale/en.yml @@ -9,6 +9,7 @@ en: blank: 'is empty' values: 'does not have a valid value' except_values: 'has a value not allowed' + same_as: 'is not the same as %{parameter}' missing_vendor_option: problem: 'missing :vendor option.' summary: 'when version using header, you must specify :vendor option. ' @@ -43,10 +44,11 @@ en: "when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request's 'body' " + empty_message_body: 'Empty message body supplied with %{body_format} content-type' invalid_accept_header: problem: 'Invalid accept header' resolution: '%{message}' invalid_version_header: problem: 'Invalid version header' resolution: '%{message}' - + invalid_response: 'Invalid response' diff --git a/lib/grape/middleware/auth/base.rb b/lib/grape/middleware/auth/base.rb index 1dd9bf66a..081613c92 100644 --- a/lib/grape/middleware/auth/base.rb +++ b/lib/grape/middleware/auth/base.rb @@ -1,18 +1,18 @@ +# frozen_string_literal: true + require 'rack/auth/basic' module Grape module Middleware module Auth class Base + include Helpers + attr_accessor :options, :app, :env - def initialize(app, **options) + def initialize(app, *options) @app = app - @options = options - end - - def context - env[Grape::Env::API_ENDPOINT] + @options = options.shift end def call(env) @@ -23,7 +23,7 @@ def _call(env) self.env = env if options.key?(:type) - auth_proc = options[:proc] + auth_proc = options[:proc] auth_proc_context = context strategy_info = Grape::Middleware::Auth::Strategies[options[:type]] diff --git a/lib/grape/middleware/auth/dsl.rb b/lib/grape/middleware/auth/dsl.rb index 49462d847..1b2e8f456 100644 --- a/lib/grape/middleware/auth/dsl.rb +++ b/lib/grape/middleware/auth/dsl.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/auth/basic' require 'active_support/concern' diff --git a/lib/grape/middleware/auth/strategies.rb b/lib/grape/middleware/auth/strategies.rb index 90375b793..dc36eea48 100644 --- a/lib/grape/middleware/auth/strategies.rb +++ b/lib/grape/middleware/auth/strategies.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Middleware module Auth diff --git a/lib/grape/middleware/auth/strategy_info.rb b/lib/grape/middleware/auth/strategy_info.rb index e04656b76..20eb25b7d 100644 --- a/lib/grape/middleware/auth/strategy_info.rb +++ b/lib/grape/middleware/auth/strategy_info.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Middleware module Auth diff --git a/lib/grape/middleware/base.rb b/lib/grape/middleware/base.rb index 08e26df57..730a6cb3c 100644 --- a/lib/grape/middleware/base.rb +++ b/lib/grape/middleware/base.rb @@ -1,18 +1,23 @@ +# frozen_string_literal: true + require 'grape/dsl/headers' module Grape module Middleware class Base + include Helpers + attr_reader :app, :env, :options - TEXT_HTML = 'text/html'.freeze + + TEXT_HTML = 'text/html' include Grape::DSL::Headers # @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) + def initialize(app, *options) @app = app - @options = default_options.merge(**options) + @options = options.any? ? default_options.merge(options.shift) : default_options @app_response = nil end @@ -21,7 +26,7 @@ def default_options end def call(env) - dup.call!(env) + dup.call!(env).to_a end def call!(env) @@ -70,11 +75,9 @@ def content_type end def mime_types - types_without_params = {} - content_types.each_pair do |k, v| + @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params| types_without_params[v.split(';').first] = k end - types_without_params end private diff --git a/lib/grape/middleware/error.rb b/lib/grape/middleware/error.rb index 0991b4884..20f6a89f3 100644 --- a/lib/grape/middleware/error.rb +++ b/lib/grape/middleware/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' require 'active_support/core_ext/string/output_safety' @@ -17,7 +19,7 @@ def default_options rescue_subclasses: true, # rescue subclasses of exceptions listed rescue_options: { backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions - original_exception: false, # true to display exception + original_exception: false # true to display exception }, rescue_handlers: {}, # rescue handler blocks base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class @@ -25,27 +27,26 @@ def default_options } end - def initialize(app, **options) + def initialize(app, *options) super self.class.send(:include, @options[:helpers]) if @options[:helpers] end def call!(env) @env = env - begin error_response(catch(:error) do return @app.call(@env) end) - rescue Exception => error # rubocop:disable Lint/RescueException + rescue Exception => e # rubocop:disable Lint/RescueException handler = - rescue_handler_for_base_only_class(error.class) || - rescue_handler_for_class_or_its_ancestor(error.class) || - rescue_handler_for_grape_exception(error.class) || - rescue_handler_for_any_class(error.class) || + rescue_handler_for_base_only_class(e.class) || + rescue_handler_for_class_or_its_ancestor(e.class) || + rescue_handler_for_grape_exception(e.class) || + rescue_handler_for_any_class(e.class) || raise - run_rescue_handler(handler, error) + run_rescue_handler(handler, e) end end @@ -64,21 +65,19 @@ def error_response(error = {}) message = error[:message] || options[:default_message] headers = { Grape::Http::Headers::CONTENT_TYPE => content_type } headers.merge!(error[:headers]) if error[:headers].is_a?(Hash) - backtrace = error[:backtrace] || error[:original_exception] && error[:original_exception].backtrace || [] + backtrace = error[:backtrace] || error[:original_exception]&.backtrace || [] original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil rack_response(format_message(message, backtrace, original_exception), status, headers) end def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type }) - if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML - message = ERB::Util.html_escape(message) - end - Rack::Response.new([message], status, headers).finish + message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML + Rack::Response.new([message], status, 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) + formatter = Grape::ErrorFormatter.formatter_for(format, **options) throw :error, status: 406, message: "The requested format '#{format}' is not supported.", @@ -127,7 +126,13 @@ def run_rescue_handler(handler, error) handler = public_method(handler) end - handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler) + response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler) + + if response.is_a?(Rack::Response) + response + else + run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new) + end end end end diff --git a/lib/grape/middleware/filter.rb b/lib/grape/middleware/filter.rb index 2b52701d3..10d044691 100644 --- a/lib/grape/middleware/filter.rb +++ b/lib/grape/middleware/filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Middleware # This is a simple middleware for adding before and after filters diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 4f5fccc2e..bb9888c2a 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'grape/middleware/base' module Grape module Middleware class Formatter < Base - CHUNKED = 'chunked'.freeze + CHUNKED = 'chunked' def default_options { @@ -34,9 +36,9 @@ def after def build_formatted_response(status, headers, bodies) headers = ensure_content_type(headers) - if bodies.is_a?(Grape::ServeFile::FileResponse) - Grape::ServeFile::SendfileResponse.new([], status, headers) do |resp| - resp.body = bodies.file + 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 @@ -52,7 +54,7 @@ def build_formatted_response(status, headers, bodies) def fetch_formatter(headers, options) api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT] - Grape::Formatter.formatter_for(api_format, options) + Grape::Formatter.formatter_for(api_format, **options) end # Set the content type header for the API format if it is not already present. @@ -97,7 +99,7 @@ def read_rack_input(body) unless content_type_for(fmt) throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." end - parser = Grape::Parser.parser_for fmt, options + parser = Grape::Parser.parser_for fmt, **options if parser begin body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env)) diff --git a/lib/grape/middleware/globals.rb b/lib/grape/middleware/globals.rb index f356030e3..850f24196 100644 --- a/lib/grape/middleware/globals.rb +++ b/lib/grape/middleware/globals.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' module Grape diff --git a/lib/grape/middleware/helpers.rb b/lib/grape/middleware/helpers.rb new file mode 100644 index 000000000..013a9587d --- /dev/null +++ b/lib/grape/middleware/helpers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Grape + module Middleware + # Common methods for all types of Grape middleware + module Helpers + def context + env[Grape::Env::API_ENDPOINT] + end + end + end +end diff --git a/lib/grape/middleware/stack.rb b/lib/grape/middleware/stack.rb index 7f3550c47..dab755fe6 100644 --- a/lib/grape/middleware/stack.rb +++ b/lib/grape/middleware/stack.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Middleware # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack @@ -28,6 +30,10 @@ def ==(other) def inspect klass.to_s end + + def use_in(builder) + builder.use(@klass, *@args, &@block) + end end include Enumerable @@ -60,6 +66,7 @@ def insert(index, *args, &block) middleware = self.class::Middleware.new(*args, &block) middlewares.insert(index, middleware) end + ruby2_keywords :insert if respond_to?(:ruby2_keywords, true) alias insert_before insert @@ -67,16 +74,19 @@ def insert_after(index, *args, &block) index = assert_index(index, :after) insert(index + 1, *args, &block) end + ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true) def use(*args, &block) middleware = self.class::Middleware.new(*args, &block) middlewares.push(middleware) end + ruby2_keywords :use if respond_to?(:ruby2_keywords, true) def merge_with(middleware_specs) middleware_specs.each do |operation, *args| if args.last.is_a?(Proc) - public_send(operation, *args, &args.pop) + last_proc = args.pop + public_send(operation, *args, &last_proc) else public_send(operation, *args) end @@ -87,7 +97,7 @@ def merge_with(middleware_specs) def build(builder = Rack::Builder.new) others.shift(others.size).each(&method(:merge_with)) middlewares.each do |m| - m.block ? builder.use(m.klass, *m.args, &m.block) : builder.use(m.klass, *m.args) + m.use_in(builder) end builder end @@ -96,7 +106,7 @@ def build(builder = Rack::Builder.new) # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]]) def concat(other_specs) @others << Array(other_specs).reject { |o| o.first == :use } - merge_with Array(other_specs).select { |o| o.first == :use } + merge_with(Array(other_specs).select { |o| o.first == :use }) end protected diff --git a/lib/grape/middleware/versioner.rb b/lib/grape/middleware/versioner.rb index 82ba1ba74..d40c87a27 100644 --- a/lib/grape/middleware/versioner.rb +++ b/lib/grape/middleware/versioner.rb @@ -1,3 +1,5 @@ +# 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: # diff --git a/lib/grape/middleware/versioner/accept_version_header.rb b/lib/grape/middleware/versioner/accept_version_header.rb index 46cd769c7..953a78392 100644 --- a/lib/grape/middleware/versioner/accept_version_header.rb +++ b/lib/grape/middleware/versioner/accept_version_header.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' module Grape diff --git a/lib/grape/middleware/versioner/header.rb b/lib/grape/middleware/versioner/header.rb index e07cacb2a..bb5fc1671 100644 --- a/lib/grape/middleware/versioner/header.rb +++ b/lib/grape/middleware/versioner/header.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' require 'grape/middleware/versioner/parse_media_type_patch' @@ -24,10 +26,10 @@ module Versioner # route. class Header < Base VENDOR_VERSION_HEADER_REGEX = - /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/ + /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze - HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/ - HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/ + HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze + HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze def before strict_header_checks if strict? @@ -61,7 +63,7 @@ def strict_version_vendor_accept_header_presence_check def an_accept_header_with_version_and_vendor_is_present? header.qvalues.keys.any? do |h| - VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '') + VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', '')) end end @@ -99,7 +101,7 @@ def fail_with_invalid_version_header!(message) def available_media_types available_media_types = [] - content_types.each do |extension, _media_type| + content_types.each_key do |extension| versions.reverse_each do |version| available_media_types += [ "application/vnd.#{vendor}-#{version}+#{extension}", @@ -111,7 +113,7 @@ def available_media_types available_media_types << "application/vnd.#{vendor}" - content_types.each do |_, media_type| + content_types.each_value do |media_type| available_media_types << media_type end @@ -173,7 +175,7 @@ def error_headers # @return [Boolean] whether the content type sets a vendor def vendor?(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) - subtype[HAS_VENDOR_REGEX] + subtype.present? && subtype[HAS_VENDOR_REGEX] end def request_vendor(media_type) @@ -190,7 +192,7 @@ def request_version(media_type) # @return [Boolean] whether the content type sets an API version def version?(media_type) _, subtype = Rack::Accept::Header.parse_media_type(media_type) - subtype[HAS_VERSION_REGEX] + subtype.present? && subtype[HAS_VERSION_REGEX] end end end diff --git a/lib/grape/middleware/versioner/param.rb b/lib/grape/middleware/versioner/param.rb index d51a148c1..8e7b17a4e 100644 --- a/lib/grape/middleware/versioner/param.rb +++ b/lib/grape/middleware/versioner/param.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' module Grape @@ -22,7 +24,7 @@ class Param < Base def default_options { version_options: { - parameter: 'apiver'.freeze + parameter: 'apiver' } } end diff --git a/lib/grape/middleware/versioner/parse_media_type_patch.rb b/lib/grape/middleware/versioner/parse_media_type_patch.rb index cf0c987ef..7098b32c0 100644 --- a/lib/grape/middleware/versioner/parse_media_type_patch.rb +++ b/lib/grape/middleware/versioner/parse_media_type_patch.rb @@ -1,11 +1,14 @@ +# frozen_string_literal: true + module Rack module Accept module Header + ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze class << self # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44 def parse_media_type(media_type) # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names - m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}) + m = media_type&.match(ALLOWED_CHARACTERS) m ? [m[1], m[2], m[3] || ''] : [] end end diff --git a/lib/grape/middleware/versioner/path.rb b/lib/grape/middleware/versioner/path.rb index e57f0e199..b7becc749 100644 --- a/lib/grape/middleware/versioner/path.rb +++ b/lib/grape/middleware/versioner/path.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/middleware/base' module Grape @@ -34,7 +36,7 @@ def before pieces = path.split('/') potential_version = pieces[1] - return unless potential_version =~ options[:pattern] + return unless potential_version&.match?(options[:pattern]) throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version } env[Grape::Env::API_VERSION] = potential_version end diff --git a/lib/grape/namespace.rb b/lib/grape/namespace.rb index 9b3696859..3473d3efb 100644 --- a/lib/grape/namespace.rb +++ b/lib/grape/namespace.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'grape/util/cache' + module Grape # A container for endpoints or other namespaces, which allows for both # logical grouping of endpoints as well as sharing common configuration. @@ -23,13 +27,21 @@ def requirements # (see ::joined_space_path) def self.joined_space(settings) - (settings || []).map(&:space).join('/') + settings&.map(&:space) 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) - Grape::Router.normalize_path(joined_space(settings)) + Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)]) + end + + class JoinedSpaceCache < Grape::Util::Cache + def initialize + @cache = Hash.new do |h, joined_space| + h[joined_space] = -joined_space.join('/') + end + end end end end diff --git a/lib/grape/parser.rb b/lib/grape/parser.rb index 41ad889eb..3676a45a7 100644 --- a/lib/grape/parser.rb +++ b/lib/grape/parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Parser extend Util::Registrable @@ -11,8 +13,8 @@ def builtin_parsers } end - def parsers(options) - builtin_parsers.merge(default_elements).merge(options[:parsers] || {}) + def parsers(**options) + builtin_parsers.merge(default_elements).merge!(options[:parsers] || {}) end def parser_for(api_format, **options) diff --git a/lib/grape/parser/json.rb b/lib/grape/parser/json.rb index 89b50d3f2..4e665a1ec 100644 --- a/lib/grape/parser/json.rb +++ b/lib/grape/parser/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Parser module Json @@ -6,7 +8,7 @@ def 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, 'application/json' + raise Grape::Exceptions::InvalidMessageBody.new('application/json') end end end diff --git a/lib/grape/parser/xml.rb b/lib/grape/parser/xml.rb index 9fadf1c64..930c57f13 100644 --- a/lib/grape/parser/xml.rb +++ b/lib/grape/parser/xml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Parser module Xml @@ -6,7 +8,7 @@ def 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, 'application/xml' + raise Grape::Exceptions::InvalidMessageBody.new('application/xml') end end end diff --git a/lib/grape/path.rb b/lib/grape/path.rb index 86f51f34c..e36684627 100644 --- a/lib/grape/path.rb +++ b/lib/grape/path.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'grape/util/cache' + module Grape # Represents a path to an endpoint. class Path @@ -38,11 +42,11 @@ def uses_path_versioning? end def namespace? - namespace && namespace.to_s =~ /^\S/ && namespace != '/' + namespace&.match?(/^\S/) && namespace != '/' end def path? - raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/' + raw_path&.match?(/^\S/) && raw_path != '/' end def suffix @@ -56,7 +60,7 @@ def suffix end def path - Grape::Router.normalize_path(parts.join('/')) + Grape::Router.normalize_path(PartsCache[parts]) end def path_with_suffix @@ -69,6 +73,14 @@ def to_s private + class PartsCache < Grape::Util::Cache + def initialize + @cache = Hash.new do |h, parts| + h[parts] = -parts.join('/') + end + end + end + def parts parts = [mount_path, root_prefix].compact parts << ':version' if uses_path_versioning? diff --git a/lib/grape/presenters/presenter.rb b/lib/grape/presenters/presenter.rb index 0f653797b..78c812178 100644 --- a/lib/grape/presenters/presenter.rb +++ b/lib/grape/presenters/presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Presenters class Presenter diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 0f11dff9e..ed97f3194 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -1,16 +1,22 @@ +# frozen_string_literal: true + +require 'grape/util/lazy_object' + module Grape class Request < Rack::Request - HTTP_PREFIX = 'HTTP_'.freeze + HTTP_PREFIX = 'HTTP_' alias rack_params params - def initialize(env, options = {}) - extend options[:build_params_with] || Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + def initialize(env, **options) + extend options[:build_params_with] || Grape.config.param_builder super(env) end def params @params ||= build_params + rescue EOFError + raise Grape::Exceptions::EmptyMessageBody.new(content_type) end def headers @@ -28,14 +34,17 @@ def grape_routing_args end def build_headers - headers = {} - env.each_pair do |k, v| - next unless k.to_s.start_with? HTTP_PREFIX - - k = k[5..-1].split('_').each(&:capitalize!).join('-') - headers[k] = v + Grape::Util::LazyObject.new do + env.each_pair.with_object({}) do |(k, v), headers| + next unless k.to_s.start_with? HTTP_PREFIX + transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k) + headers[transformed_header] = v + end end - headers + end + + def transform_header(header) + -header[5..-1].split('_').each(&:capitalize!).join('-') end end end diff --git a/lib/grape/router.rb b/lib/grape/router.rb index 54bcfd1d3..8c5dce872 100644 --- a/lib/grape/router.rb +++ b/lib/grape/router.rb @@ -1,18 +1,14 @@ +# frozen_string_literal: true + require 'grape/router/route' +require 'grape/util/cache' module Grape class Router attr_reader :map, :compiled - class Any < AttributeTranslator - def initialize(pattern, **attributes) - @pattern = pattern - super(attributes) - end - end - def self.normalize_path(path) - path = "/#{path}" + path = +"/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') path = '/' if path == '' @@ -25,18 +21,20 @@ def self.supported_methods 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_map.map(&:regexp)) + @union = Regexp.union(@neutral_regexes) + @neutral_regexes = nil self.class.supported_methods.each do |method| routes = map[method] @optimized_map[method] = routes.map.with_index do |route, index| route.index = index - route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/ + Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})") end @optimized_map[method] = Regexp.union(@optimized_map[method]) end @@ -44,12 +42,12 @@ def compile! end def append(route) - map[route.request_method.to_s.upcase] << route + map[route.request_method] << route end def associate_routes(pattern, **options) - regexp = /(?<_#{@neutral_map.length}>)#{pattern.to_regexp}/ - @neutral_map << Any.new(pattern, regexp: regexp, index: @neutral_map.length, **options) + @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}") + @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length) end def call(env) @@ -93,37 +91,34 @@ def transaction(env) response = yield(input, method) return response if response && !(cascade = cascade?(response)) - neighbor = greedy_match?(input) + last_neighbor_route = greedy_match?(input) - # If neighbor exists and request method is OPTIONS, + # If last_neighbor_route exists and request method is OPTIONS, # return response by using #call_with_allow_headers. - return call_with_allow_headers( - env, - neighbor.allow_header, - neighbor.endpoint - ) if neighbor && method == 'OPTIONS' && !cascade + return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade route = match?(input, '*') - return neighbor.endpoint.call(env) if neighbor && cascade && route + + return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route if route response = process_route(route, env) return response if response && !(cascade = cascade?(response)) end - !cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil + return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route + + nil end def process_route(route, env) - input, = *extract_input_and_method(env) - routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS] - env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input) + prepare_env_from_route(env, route) route.exec(env) end def make_routing_args(default_args, route, input) args = default_args || { route_info: route } - args.merge(route.params(input)) + args.merge(route.params(input) || {}) end def extract_input_and_method(env) @@ -154,9 +149,15 @@ def greedy_match?(input) @neutral_map.detect { |route| last_match["_#{route.index}"] } end - def call_with_allow_headers(env, methods, endpoint) - env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods - endpoint.call(env) + def call_with_allow_headers(env, route) + prepare_env_from_route(env, route) + env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze + route.endpoint.call(env) + end + + def prepare_env_from_route(env, route) + input, = *extract_input_and_method(env) + env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input) end def cascade?(response) diff --git a/lib/grape/router/attribute_translator.rb b/lib/grape/router/attribute_translator.rb index bea51d1f9..93ba4bdcd 100644 --- a/lib/grape/router/attribute_translator.rb +++ b/lib/grape/router/attribute_translator.rb @@ -1,30 +1,63 @@ +# frozen_string_literal: true + module Grape class Router # this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251 class AttributeTranslator - def initialize(attributes = {}) + attr_reader :attributes + + ROUTE_ATTRIBUTES = %i[ + prefix + version + settings + format + description + http_codes + headers + entity + details + requirements + request_method + namespace + ].freeze + + ROUTER_ATTRIBUTES = %i[pattern index].freeze + + def initialize(**attributes) @attributes = attributes end + (ROUTER_ATTRIBUTES + ROUTE_ATTRIBUTES).each do |attr| + define_method attr do + attributes[attr] + end + end + def to_h - @attributes + attributes end - def method_missing(m, *args) - if m[-1] == '=' - @attributes[m[0..-1]] = *args - elsif m[-1] != '=' - @attributes[m] + def method_missing(method_name, *args) + if setter?(method_name[-1]) + attributes[method_name[0..-1]] = *args + else + attributes[method_name] end end def respond_to_missing?(method_name, _include_private = false) - if method_name[-1] == '=' + if setter?(method_name[-1]) true else @attributes.key?(method_name) end end + + private + + def setter?(method_name) + method_name[-1] == '=' + end end end end diff --git a/lib/grape/router/pattern.rb b/lib/grape/router/pattern.rb index 9f326ecc0..e8c108ad8 100644 --- a/lib/grape/router/pattern.rb +++ b/lib/grape/router/pattern.rb @@ -1,34 +1,33 @@ +# frozen_string_literal: true + require 'forwardable' require 'mustermann/grape' +require 'grape/util/cache' module Grape class Router class Pattern - DEFAULT_PATTERN_OPTIONS = { uri_decode: true, type: :grape }.freeze + DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze - attr_reader :origin, :path, :capture, :pattern + attr_reader :origin, :path, :pattern, :to_regexp extend Forwardable def_delegators :pattern, :named_captures, :params - def_delegators :@regexp, :=== + def_delegators :to_regexp, :=== alias match? === def initialize(pattern, **options) @origin = pattern @path = build_path(pattern, **options) - @capture = extract_capture(options) - @pattern = Mustermann.new(@path, pattern_options) - @regexp = to_regexp - end - - def to_regexp - @to_regexp ||= @pattern.to_regexp + @pattern = Mustermann::Grape.new(@path, **pattern_options(options)) + @to_regexp = @pattern.to_regexp end private - def pattern_options + def pattern_options(options) + capture = extract_capture(**options) options = DEFAULT_PATTERN_OPTIONS.dup options[:capture] = capture if capture.present? options @@ -36,27 +35,32 @@ def pattern_options def build_path(pattern, anchor: false, suffix: nil, **_options) unless anchor || pattern.end_with?('*path') + pattern = +pattern pattern << '/' unless pattern.end_with?('/') pattern << '*path' end - pattern = pattern.split('/').tap do |parts| + pattern = -pattern.split('/').tap do |parts| parts[parts.length - 1] = '?' + parts.last end.join('/') if pattern.end_with?('*path') - pattern + suffix.to_s + PatternCache[[pattern, suffix]] end def extract_capture(requirements: {}, **options) requirements = {}.merge(requirements) - supported_capture.each_with_object(requirements) do |field, capture| + DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture| option = Array(options[field]) capture[field] = option.map(&:to_s) if option.present? end end - def supported_capture - DEFAULT_SUPPORTED_CAPTURE + class PatternCache < Grape::Util::Cache + def initialize + @cache = Hash.new do |h, (pattern, suffix)| + h[[pattern, suffix]] = -"#{pattern}#{suffix}" + end + end end end end diff --git a/lib/grape/router/route.rb b/lib/grape/router/route.rb index bf6e47b88..fa940c925 100644 --- a/lib/grape/router/route.rb +++ b/lib/grape/router/route.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'grape/router/pattern' require 'grape/router/attribute_translator' require 'forwardable' @@ -6,16 +8,17 @@ module Grape class Router class Route - ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/ - SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/ + ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze + SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze FIXED_NAMED_CAPTURES = %w[format version].freeze - attr_accessor :pattern, :translator, :app, :index, :regexp, :options + attr_accessor :pattern, :translator, :app, :index, :options alias attributes translator extend Forwardable def_delegators :pattern, :path, :origin + delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes def method_missing(method_id, *arguments) match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s) @@ -29,26 +32,7 @@ def method_missing(method_id, *arguments) end def respond_to_missing?(method_id, _) - ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s) - end - - %i[ - prefix - version - settings - format - description - http_codes - headers - entity - details - requirements - request_method - namespace - ].each do |method_name| - define_method method_name do - attributes.public_send method_name - end + ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s) end def route_method @@ -62,10 +46,12 @@ def route_path end def initialize(method, pattern, **options) - @suffix = options[:suffix] - @options = options.merge(method: method.to_s.upcase) + method_s = method.to_s + method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase + + @options = options.merge(method: method_upcase) @pattern = Pattern.new(pattern, **options) - @translator = AttributeTranslator.new(**options, request_method: method.to_s.upcase) + @translator = AttributeTranslator.new(**options, request_method: method_upcase) end def exec(env) @@ -98,9 +84,9 @@ def warn_route_methods(name, location, expected = nil) path, line = *location.scan(SOURCE_LOCATION_REGEXP).first path = File.realpath(path) if Pathname.new(path).relative? expected ||= name - warn <<-EOS + warn <<-WARNING #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}. - EOS + WARNING end end end diff --git a/lib/grape/serve_file/file_body.rb b/lib/grape/serve_stream/file_body.rb similarity index 91% rename from lib/grape/serve_file/file_body.rb rename to lib/grape/serve_stream/file_body.rb index ba9fa3a1e..b842af661 100644 --- a/lib/grape/serve_file/file_body.rb +++ b/lib/grape/serve_stream/file_body.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Grape - module ServeFile + module ServeStream CHUNK_SIZE = 16_384 # Class helps send file through API diff --git a/lib/grape/serve_file/sendfile_response.rb b/lib/grape/serve_stream/sendfile_response.rb similarity index 88% rename from lib/grape/serve_file/sendfile_response.rb rename to lib/grape/serve_stream/sendfile_response.rb index 17312a644..b46fc102a 100644 --- a/lib/grape/serve_file/sendfile_response.rb +++ b/lib/grape/serve_stream/sendfile_response.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Grape - module ServeFile + module ServeStream # Response should respond to to_path method # for using Rack::SendFile middleware class SendfileResponse < Rack::Response diff --git a/lib/grape/serve_file/file_response.rb b/lib/grape/serve_stream/stream_response.rb similarity index 52% rename from lib/grape/serve_file/file_response.rb rename to lib/grape/serve_stream/stream_response.rb index 07b98cd21..0705fbf7b 100644 --- a/lib/grape/serve_file/file_response.rb +++ b/lib/grape/serve_stream/stream_response.rb @@ -1,20 +1,22 @@ +# frozen_string_literal: true + module Grape - module ServeFile - # A simple class used to identify responses which represent files and do not + 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 FileResponse - attr_reader :file + class StreamResponse + attr_reader :stream - # @param file [Object] - def initialize(file) - @file = file + # @param stream [Object] + def initialize(stream) + @stream = stream end # Equality provided mostly for tests. # # @return [Boolean] def ==(other) - file == other.file + stream == other.stream end end end diff --git a/lib/grape/util/base_inheritable.rb b/lib/grape/util/base_inheritable.rb new file mode 100644 index 000000000..5db6e3455 --- /dev/null +++ b/lib/grape/util/base_inheritable.rb @@ -0,0 +1,43 @@ +# 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(key) + new_values.delete key + 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? + combined = inherited_values.keys + combined.concat(new_values.keys) + combined.uniq! + combined + else + inherited_values.keys + end + end + + def key?(name) + inherited_values.key?(name) || new_values.key?(name) + end + end + end +end diff --git a/lib/grape/util/cache.rb b/lib/grape/util/cache.rb new file mode 100644 index 000000000..3f51148f7 --- /dev/null +++ b/lib/grape/util/cache.rb @@ -0,0 +1,20 @@ +# frozen_String_literal: true + +require 'singleton' +require 'forwardable' + +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 diff --git a/lib/grape/util/content_types.rb b/lib/grape/util/content_types.rb deleted file mode 100644 index 8b2122402..000000000 --- a/lib/grape/util/content_types.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Grape - module ContentTypes - # Content types are listed in order of preference. - CONTENT_TYPES = { # rubocop:disable Style/MutableConstant - xml: 'application/xml', - serializable_hash: 'application/json', - json: 'application/json', - binary: 'application/octet-stream', - txt: 'text/plain' - } - - def self.content_types_for_settings(settings) - return if settings.blank? - - settings.each_with_object({}) { |value, result| result.merge!(value) } - end - - def self.content_types_for(from_settings) - if from_settings.present? - from_settings - else - Grape::ContentTypes::CONTENT_TYPES - end - end - end -end diff --git a/lib/grape/util/endpoint_configuration.rb b/lib/grape/util/endpoint_configuration.rb new file mode 100644 index 000000000..90ad256f8 --- /dev/null +++ b/lib/grape/util/endpoint_configuration.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Grape + module Util + class EndpointConfiguration < LazyValueHash + end + end +end diff --git a/lib/grape/util/env.rb b/lib/grape/util/env.rb index eaa95f917..a6023bcc1 100644 --- a/lib/grape/util/env.rb +++ b/lib/grape/util/env.rb @@ -1,23 +1,25 @@ +# frozen_string_literal: true + module Grape module Env - API_VERSION = 'api.version'.freeze - API_ENDPOINT = 'api.endpoint'.freeze - API_REQUEST_INPUT = 'api.request.input'.freeze - API_REQUEST_BODY = 'api.request.body'.freeze - API_TYPE = 'api.type'.freeze - API_SUBTYPE = 'api.subtype'.freeze - API_VENDOR = 'api.vendor'.freeze - API_FORMAT = 'api.format'.freeze + 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' - RACK_INPUT = 'rack.input'.freeze - RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'.freeze - RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'.freeze - RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'.freeze + RACK_INPUT = 'rack.input' + RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash' + RACK_REQUEST_FORM_HASH = 'rack.request.form_hash' + RACK_REQUEST_FORM_INPUT = 'rack.request.form_input' - GRAPE_REQUEST = 'grape.request'.freeze - GRAPE_REQUEST_HEADERS = 'grape.request.headers'.freeze - GRAPE_REQUEST_PARAMS = 'grape.request.params'.freeze - GRAPE_ROUTING_ARGS = 'grape.routing_args'.freeze - GRAPE_ALLOWED_METHODS = 'grape.allowed_methods'.freeze + 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 diff --git a/lib/grape/util/inheritable_setting.rb b/lib/grape/util/inheritable_setting.rb index 2d3021861..2a6024bca 100644 --- a/lib/grape/util/inheritable_setting.rb +++ b/lib/grape/util/inheritable_setting.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Util # A branchable, inheritable settings object which can store both stackable diff --git a/lib/grape/util/inheritable_values.rb b/lib/grape/util/inheritable_values.rb index 2546075db..6b43f8ca2 100644 --- a/lib/grape/util/inheritable_values.rb +++ b/lib/grape/util/inheritable_values.rb @@ -1,14 +1,10 @@ -module Grape - module Util - class InheritableValues - attr_accessor :inherited_values - attr_accessor :new_values +# frozen_string_literal: true - def initialize(inherited_values = {}) - self.inherited_values = inherited_values - self.new_values = {} - end +require_relative 'base_inheritable' +module Grape + module Util + class InheritableValues < BaseInheritable def [](name) values[name] end @@ -17,26 +13,12 @@ def []=(name, value) new_values[name] = value end - def delete(key) - new_values.delete key - end - def merge(new_hash) - values.merge(new_hash) - end - - def keys - (new_values.keys + inherited_values.keys).sort.uniq + values.merge!(new_hash) end def to_hash - values.clone - end - - def initialize_copy(other) - super - self.inherited_values = other.inherited_values - self.new_values = other.new_values.dup + values end protected diff --git a/lib/grape/util/json.rb b/lib/grape/util/json.rb index b0f5addb0..9381d841a 100644 --- a/lib/grape/util/json.rb +++ b/lib/grape/util/json.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape if Object.const_defined? :MultiJson Json = ::MultiJson diff --git a/lib/grape/util/lazy_block.rb b/lib/grape/util/lazy_block.rb new file mode 100644 index 000000000..6e7d18b8a --- /dev/null +++ b/lib/grape/util/lazy_block.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Grape + module Util + class LazyBlock + 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 diff --git a/lib/grape/util/lazy_object.rb b/lib/grape/util/lazy_object.rb new file mode 100644 index 000000000..22ec7c440 --- /dev/null +++ b/lib/grape/util/lazy_object.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Based on https://github.com/HornsAndHooves/lazy_object + +module Grape + module Util + class LazyObject < BasicObject + attr_reader :callable + + def initialize(&callable) + @callable = callable + end + + def __target_object__ + @__target_object__ ||= callable.call + end + + def ==(other) + __target_object__ == other + end + + def !=(other) + __target_object__ != other + end + + def ! + !__target_object__ + end + + def method_missing(method_name, *args, &block) + if __target_object__.respond_to?(method_name) + __target_object__.send(method_name, *args, &block) + else + super + end + end + + def respond_to_missing?(method_name, include_priv = false) + __target_object__.respond_to?(method_name, include_priv) + end + end + end +end diff --git a/lib/grape/util/lazy_value.rb b/lib/grape/util/lazy_value.rb new file mode 100644 index 000000000..d757ad3e2 --- /dev/null +++ b/lib/grape/util/lazy_value.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Grape + module Util + class LazyValue + 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 + + class LazyValueEnumerable < LazyValue + def [](key) + if @value_hash[key].nil? + LazyValue.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] = if value.is_a?(Hash) + LazyValueHash.new(value) + elsif value.is_a?(Array) + LazyValueArray.new(value) + else + LazyValue.new(value) + end + end + end + + class LazyValueArray < LazyValueEnumerable + def initialize(array) + super + @value_hash = [] + array.each_with_index do |value, index| + self[index] = value + end + end + + def evaluate + evaluated = [] + @value_hash.each_with_index do |value, index| + evaluated[index] = value.evaluate + end + evaluated + end + end + + class LazyValueHash < LazyValueEnumerable + def initialize(hash) + super + @value_hash = {}.with_indifferent_access + hash.each do |key, value| + self[key] = value + end + end + + def evaluate + evaluated = {}.with_indifferent_access + @value_hash.each do |key, value| + evaluated[key] = value.evaluate + end + evaluated + end + end + end +end diff --git a/lib/grape/util/registrable.rb b/lib/grape/util/registrable.rb index 66733a57d..e154456f8 100644 --- a/lib/grape/util/registrable.rb +++ b/lib/grape/util/registrable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Util module Registrable diff --git a/lib/grape/util/reverse_stackable_values.rb b/lib/grape/util/reverse_stackable_values.rb index 474325f72..171f390f7 100644 --- a/lib/grape/util/reverse_stackable_values.rb +++ b/lib/grape/util/reverse_stackable_values.rb @@ -1,45 +1,20 @@ +# frozen_string_literal: true + +require_relative 'stackable_values' + module Grape module Util - class ReverseStackableValues - attr_accessor :inherited_values - attr_accessor :new_values + class ReverseStackableValues < StackableValues + protected - def initialize(inherited_values = {}) - @inherited_values = inherited_values - @new_values = {} - end + def concat_values(inherited_value, new_value) + return inherited_value unless new_value - def [](name) [].tap do |value| - value.concat(@new_values[name] || []) - value.concat(@inherited_values[name] || []) - end - end - - def []=(name, value) - @new_values[name] ||= [] - @new_values[name].push value - end - - def delete(key) - new_values.delete key - end - - def keys - (@new_values.keys + @inherited_values.keys).sort.uniq - end - - def to_hash - keys.each_with_object({}) do |key, result| - result[key] = self[key] + value.concat(new_value) + value.concat(inherited_value) end end - - def initialize_copy(other) - super - self.inherited_values = other.inherited_values - self.new_values = other.new_values.dup - end end end end diff --git a/lib/grape/util/stackable_values.rb b/lib/grape/util/stackable_values.rb index a6c8179bd..01a568196 100644 --- a/lib/grape/util/stackable_values.rb +++ b/lib/grape/util/stackable_values.rb @@ -1,37 +1,23 @@ +# frozen_string_literal: true + +require_relative 'base_inheritable' + module Grape module Util - class StackableValues - attr_accessor :inherited_values - attr_accessor :new_values - attr_reader :frozen_values - - def initialize(inherited_values = {}) - @inherited_values = inherited_values - @new_values = {} - @frozen_values = {} - end - + class StackableValues < BaseInheritable + # Even if there is no value, an empty array will be returned. def [](name) - return @frozen_values[name] if @frozen_values.key? name - - value = [] - value.concat(@inherited_values[name] || []) - value.concat(@new_values[name] || []) - value - end + inherited_value = inherited_values[name] + new_value = new_values[name] - def []=(name, value) - raise if @frozen_values.key? name - @new_values[name] ||= [] - @new_values[name].push value - end + return new_value || [] unless inherited_value - def delete(key) - new_values.delete key + concat_values(inherited_value, new_value) end - def keys - (@new_values.keys + @inherited_values.keys).sort.uniq + def []=(name, value) + new_values[name] ||= [] + new_values[name].push value end def to_hash @@ -40,14 +26,15 @@ def to_hash end end - def freeze_value(key) - @frozen_values[key] = self[key].freeze - end + protected + + def concat_values(inherited_value, new_value) + return inherited_value unless new_value - def initialize_copy(other) - super - self.inherited_values = other.inherited_values - self.new_values = other.new_values.dup + [].tap do |value| + value.concat(inherited_value) + value.concat(new_value) + end end end end diff --git a/lib/grape/util/strict_hash_configuration.rb b/lib/grape/util/strict_hash_configuration.rb index 73cdb9252..91fa41399 100644 --- a/lib/grape/util/strict_hash_configuration.rb +++ b/lib/grape/util/strict_hash_configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Util module StrictHashConfiguration diff --git a/lib/grape/util/xml.rb b/lib/grape/util/xml.rb index dd5d296e6..d948f8012 100644 --- a/lib/grape/util/xml.rb +++ b/lib/grape/util/xml.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape if Object.const_defined? :MultiXml Xml = ::MultiXml diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index c5771b520..bd55c0611 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape # Registry to store and locate known Validators. module Validations diff --git a/lib/grape/validations/attributes_iterator.rb b/lib/grape/validations/attributes_iterator.rb index 18b40cce7..c16d44f3f 100644 --- a/lib/grape/validations/attributes_iterator.rb +++ b/lib/grape/validations/attributes_iterator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Validations class AttributesIterator @@ -29,9 +31,7 @@ def do_each(params_to_process, parent_indicies = [], &block) if @scope.type == Array next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array - inside_array = true - end - if inside_array + # fill current and parent scopes with correct array indicies parent_scope = @scope.parent parent_indicies.each do |parent_index| @@ -41,11 +41,21 @@ def do_each(params_to_process, parent_indicies = [], &block) @scope.index = index end - @attrs.each do |attr_name| - yield resource_params, attr_name, inside_array - end + yield_attributes(resource_params, @attrs, &block) end end + + def yield_attributes(_resource_params, _attrs) + raise NotImplementedError + end + + # This is a special case so that we can ignore tree's where option + # values are missing lower down. Unfortunately we can remove this + # are 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 diff --git a/lib/grape/validations/multiple_attributes_iterator.rb b/lib/grape/validations/multiple_attributes_iterator.rb new file mode 100644 index 000000000..d9ef7264b --- /dev/null +++ b/lib/grape/validations/multiple_attributes_iterator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Grape + module Validations + class MultipleAttributesIterator < AttributesIterator + private + + def yield_attributes(resource_params, _attrs) + yield resource_params, skip?(resource_params) + end + end + end +end diff --git a/lib/grape/validations/params_scope.rb b/lib/grape/validations/params_scope.rb index cad7acb48..a39383d5d 100644 --- a/lib/grape/validations/params_scope.rb +++ b/lib/grape/validations/params_scope.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Validations class ParamsScope @@ -36,23 +38,32 @@ def initialize(opts, &block) configure_declared_params 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) - return false if @optional && (params(parameters).blank? || all_element_blank?(parameters)) - return false unless meets_dependency?(params(parameters), 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 + if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params) return false end - return true unless @dependent_on return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array) - params = params.with_indifferent_access + + # 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) @@ -71,7 +82,7 @@ def meets_dependency?(params, request_params) def full_name(name, index: nil) if nested? # Find our containing element's name, and append ours. - [@parent.full_name(@element), [@index || index, name].map(&method(:brackets))].compact.join + "#{@parent.full_name(@element)}#{brackets(@index || 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. @@ -116,11 +127,12 @@ def required? # @param attrs [Array] (see Grape::DSL::Parameters#requires) def push_declared_params(attrs, **opts) if lateral? - @parent.push_declared_params(attrs, opts) + @parent.push_declared_params(attrs, **opts) else if opts && opts[:as] - @api.route_setting(:aliased_params, @api.route_setting(:aliased_params) || []) - @api.route_setting(:aliased_params) << { attrs.first => opts[:as] } + @api.route_setting(:renamed_params, @api.route_setting(:renamed_params) || []) + @api.route_setting(:renamed_params) << { attrs.first => opts[:as] } + attrs = [opts[:as]] end @declared_params.concat attrs @@ -178,14 +190,12 @@ def new_scope(attrs, optional = false, &block) raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type) end - opts = attrs[1] || { type: Array } - self.class.new( api: @api, - element: attrs.first, + element: attrs[1][:as] || attrs.first, parent: self, optional: optional, - type: opts[:type], + type: type || Array, &block ) end @@ -229,14 +239,14 @@ def configure_declared_params @parent.push_declared_params [element => @declared_params] else @api.namespace_stackable(:declared_params, @declared_params) - - @api.route_setting(:declared_params, []) unless @api.route_setting(:declared_params) - @api.route_setting(:declared_params, @api.namespace_stackable(:declared_params).flatten) end + + # params were stored in settings, it can be cleaned from the params scope + @declared_params = nil end def validates(attrs, validations) - doc_attrs = { required: validations.keys.include?(:presence) } + doc_attrs = { required: validations.key?(:presence) } coerce_type = infer_coercion(validations) @@ -276,9 +286,7 @@ def validates(attrs, validations) full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } } @api.document_attribute(full_attrs, doc_attrs) - # slice out fail_fast attribute - opts = {} - opts[:fail_fast] = validations.delete(:fail_fast) || false + opts = derive_validator_options(validations) # Validate for presence before any other validators if validations.key?(:presence) && validations[:presence] @@ -406,13 +414,15 @@ def validate(type, options, attrs, doc_attrs, opts) raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class - factory = Grape::Validations::ValidatorFactory.new(attributes: attrs, - options: options, - required: doc_attrs[:required], - params_scope: self, - opts: opts, - validator_class: validator_class) - @api.namespace_stackable(:validations, factory) + validator_options = { + attributes: attrs, + options: options, + required: doc_attrs[:required], + params_scope: self, + opts: opts, + validator_class: validator_class + } + @api.namespace_stackable(:validations, validator_options) end def validate_value_coercion(coerce_type, *values_list) @@ -421,8 +431,8 @@ def validate_value_coercion(coerce_type, *values_list) values_list.each do |values| next if !values || values.is_a?(Proc) value_types = values.is_a?(Range) ? [values.begin, values.end] : values - if coerce_type == Virtus::Attribute::Boolean - value_types = value_types.map { |type| Virtus::Attribute.build(type) } + if coerce_type == Grape::API::Boolean + value_types = value_types.map { |type| Grape::API::Boolean.build(type) } end unless value_types.all? { |v| v.is_a? coerce_type } raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) @@ -440,8 +450,19 @@ def options_key?(type, key, validations) validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil? end - def all_element_blank?(parameters) - params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?) + 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 end end diff --git a/lib/grape/validations/single_attribute_iterator.rb b/lib/grape/validations/single_attribute_iterator.rb new file mode 100644 index 000000000..7fd3c3f47 --- /dev/null +++ b/lib/grape/validations/single_attribute_iterator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Grape + module Validations + class SingleAttributeIterator < AttributesIterator + private + + def yield_attributes(val, attrs) + attrs.each do |attr_name| + yield val, attr_name, empty?(val), skip?(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 diff --git a/lib/grape/validations/types.rb b/lib/grape/validations/types.rb index 9320668f0..2240a7fa7 100644 --- a/lib/grape/validations/types.rb +++ b/lib/grape/validations/types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'types/build_coercer' require_relative 'types/custom_type_coercer' require_relative 'types/custom_type_collection_coercer' @@ -5,10 +7,7 @@ require_relative 'types/variant_collection_coercer' require_relative 'types/json' require_relative 'types/file' - -# Patch for Virtus::Attribute::Collection -# See the file for more details -require_relative 'types/virtus_collection_patch' +require_relative 'types/invalid_value' module Grape module Validations @@ -23,12 +22,7 @@ module Validations # and {Grape::Dsl::Parameters#optional}. The main # entry point for this process is {Types.build_coercer}. module Types - # Instances of this class may be used as tokens to denote that - # a parameter value could not be coerced. - class InvalidValue; end - - # Types representing a single value, which are coerced through Virtus - # or special logic in Grape. + # Types representing a single value, which are coerced. PRIMITIVES = [ # Numerical Integer, @@ -42,10 +36,11 @@ class InvalidValue; end Time, # Misc - Virtus::Attribute::Boolean, + Grape::API::Boolean, String, Symbol, - Rack::Multipart::UploadedFile + TrueClass, + FalseClass ].freeze # Types representing data structures. @@ -55,8 +50,7 @@ class InvalidValue; end Set ].freeze - # Types for which Grape provides special coercion - # and type-checking logic. + # Special custom types provided by Grape. SPECIAL = { JSON => Json, Array[JSON] => JsonArray, @@ -86,8 +80,6 @@ def self.primitive?(type) # @param type [Class] type to check # @return [Boolean] whether or not the type is known by Grape as a valid # data structure type - # @note This method does not yet consider 'complex types', which inherit - # Virtus.model. def self.structure?(type) STRUCTURES.include?(type) end @@ -104,25 +96,6 @@ def self.multiple?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1 end - # Does the given class implement a type system that Grape - # (i.e. the underlying virtus attribute system) supports - # out-of-the-box? Currently supported are +axiom-types+ - # and +virtus+. - # - # The type will be passed to +Virtus::Attribute.build+, - # and the resulting attribute object will be expected to - # respond correctly to +coerce+ and +value_coerced?+. - # - # @param type [Class] type to check - # @return [Boolean] +true+ where the type is recognized - def self.recognized?(type) - return false if type.is_a?(Array) || type.is_a?(Set) - - type.is_a?(Virtus::Attribute) || - type.ancestors.include?(Axiom::Types::Type) || - type.include?(Virtus::Model::Core) - end - # Does Grape provide special coercion and validation # routines for the given class? This does not include # automatic handling for primitives, structures and @@ -152,8 +125,6 @@ def self.custom?(type) !primitive?(type) && !structure?(type) && !multiple?(type) && - !recognized?(type) && - !special?(type) && type.respond_to?(:parse) && type.method(:parse).arity == 1 end @@ -166,7 +137,11 @@ def self.custom?(type) def self.collection_of_custom?(type) (type.is_a?(Array) || type.is_a?(Set)) && type.length == 1 && - custom?(type.first) + (custom?(type.first) || special?(type.first)) + end + + def self.map_special(type) + SPECIAL.fetch(type, type) end end end diff --git a/lib/grape/validations/types/array_coercer.rb b/lib/grape/validations/types/array_coercer.rb new file mode 100644 index 000000000..d3aeb2146 --- /dev/null +++ b/lib/grape/validations/types/array_coercer.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative 'dry_type_coercer' + +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/1.2/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 + register_collection Array + + def initialize(type, strict = false) + super + + @coercer = scope::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 diff --git a/lib/grape/validations/types/build_coercer.rb b/lib/grape/validations/types/build_coercer.rb index 2a8e9968c..c55e048db 100644 --- a/lib/grape/validations/types/build_coercer.rb +++ b/lib/grape/validations/types/build_coercer.rb @@ -1,78 +1,72 @@ +# frozen_string_literal: true + +require_relative 'array_coercer' +require_relative 'set_coercer' +require_relative 'primitive_coercer' + module Grape module Validations module Types - # Work out the +Virtus::Attribute+ object to - # use for coercing strings to the given +type+. - # Coercion +method+ will be inferred if none is - # supplied. + # 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+. # - # If a +Virtus::Attribute+ object already built - # with +Virtus::Attribute.build+ is supplied as - # the +type+ it will be returned and +method+ - # will be ignored. + # +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. # - # See {CustomTypeCoercer} for further details - # about coercion and type-checking inference. + # 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 [Virtus::Attribute] object to be used + # @return [Object] object to be used # for coercion and type validation - def self.build_coercer(type, method = nil) - cache_instance(type, method) do - create_coercer_instance(type, method) + def self.build_coercer(type, method: nil, strict: false) + cache_instance(type, method, strict) do + create_coercer_instance(type, method, strict) end end - def self.create_coercer_instance(type, method = nil) - # Accept pre-rolled virtus attributes without interference - return type if type.is_a? Virtus::Attribute - - converter_options = { - nullify_blank: true - } - conversion_type = if method == JSON - Object - # because we want just parsed JSON content: - # if type is Array and data is `"{}"` - # result will be [] because Virtus converts hashes - # to arrays - else - type - end + def self.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) - converter_options[:coercer] = Types::MultipleTypeCoercer.new(type, method) - conversion_type = Object + MultipleTypeCoercer.new(type, method) # Use a special coercer for custom types and coercion methods. elsif method || Types.custom?(type) - converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method) + 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) - converter_options[:coercer] = Types::CustomTypeCollectionCoercer.new( - type.first, type.is_a?(Set) + Types::CustomTypeCollectionCoercer.new( + Types.map_special(type.first), type.is_a?(Set) ) - - # Grape swaps in its own Virtus::Attribute implementations - # for certain special types that merit first-class support - # (but not if a custom coercion method has been supplied). - elsif Types.special?(type) - conversion_type = Types::SPECIAL[type] + else + DryTypeCoercer.coercer_instance_for(type, strict) end - - # Virtus will infer coercion and validation rules - # for many common ruby types. - Virtus::Attribute.build(conversion_type, converter_options) end - def self.cache_instance(type, method, &_block) - key = cache_key(type, method) + def self.cache_instance(type, method, strict, &_block) + key = cache_key(type, method, strict) return @__cache[key] if @__cache.key?(key) @@ -85,8 +79,12 @@ def self.cache_instance(type, method, &_block) instance end - def self.cache_key(type, method) - [type, method].compact.map(&:to_s).join('_') + def self.cache_key(type, method, strict) + [type, method, strict].each_with_object(+'_') do |val, memo| + next if val.nil? + + memo << '_' << val.to_s + end end instance_variable_set(:@__cache, {}) diff --git a/lib/grape/validations/types/custom_type_coercer.rb b/lib/grape/validations/types/custom_type_coercer.rb index 8e3ca2194..be72aff56 100644 --- a/lib/grape/validations/types/custom_type_coercer.rb +++ b/lib/grape/validations/types/custom_type_coercer.rb @@ -1,21 +1,8 @@ +# frozen_string_literal: true + module Grape module Validations module Types - # Instances of this class may be passed to - # +Virtus::Attribute.build+ as the +:coercer+ - # option for custom types that do not otherwise - # satisfy the requirements for +Virtus::Attribute::coerce+ - # and +Virtus::Attribute::value_coerced?+ to work - # as expected. - # - # Subclasses of +Virtus::Attribute+ or +Axiom::Types::Type+ - # (or for which an axiom type can be inferred, i.e. the - # primitives, +Date+, +Time+, etc.) do not need any such - # coercer to be passed with them. - # - # Coercion - # -------- - # # 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 @@ -30,14 +17,14 @@ module Types # Type Checking # ------------- # - # Calls to +value_coerced?+ will consult this class to check + # 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+ iff the value meets type expectations. + # and should return +true+ if the value meets type expectations. # Arbitrary assertions may be made here but the grape validation # system should be preferred. # @@ -46,15 +33,6 @@ module Types # contract as +coerced?+, and must be supplied with a coercion # +method+. class CustomTypeCoercer - # Uses +Virtus::Attribute.build+ to build a new - # attribute that makes use of this class for - # coercion and type validation logic. - # - # @return [Virtus::Attribute] - def self.build(type, method = nil) - Virtus::Attribute.build(type, coercer: new(type, method)) - end - # A new coercer for the given type specification # and coercion method. # @@ -64,37 +42,25 @@ def self.build(type, method = nil) # 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 - # This method is called from somewhere within - # +Virtus::Attribute::coerce+ in order to coerce - # the given value. + # 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(value) - @method.call value + 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 - # This method is called from somewhere within - # +Virtus::Attribute::value_coerced?+ in order to - # assert that the value has been coerced successfully. - # - # @param _primitive [Axiom::Types::Type] primitive type - # for the coercion as detected by axiom-types' inference - # system. For custom types this is typically not much use - # (i.e. it is +Axiom::Types::Object+) unless special - # inference rules have been declared for the type. - # @param value [Object] a coerced result returned from {#call} - # @return [true,false] whether or not the coerced value - # satisfies type requirements. - def success?(_primitive, value) - @type_check.call value + def coerced?(val) + val.nil? || @type_check.call(val) end private @@ -137,13 +103,25 @@ def infer_type_check(type) # passed, or if the type also implements a parse() method. type elsif type.is_a?(Enumerable) - ->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } } + 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 @@ -158,10 +136,10 @@ def infer_type_check(type) # necessary. def enforce_symbolized_keys(type, method) # Collections have all values processed individually - if type == Array || type == Set + if [Array, Set].include?(type) lambda do |val| - method.call(val).tap do |new_value| - new_value.map do |item| + method.call(val).tap do |new_val| + new_val.map do |item| item.is_a?(Hash) ? symbolize_keys(item) : item end end diff --git a/lib/grape/validations/types/custom_type_collection_coercer.rb b/lib/grape/validations/types/custom_type_collection_coercer.rb index 7534420fe..2a5a002f4 100644 --- a/lib/grape/validations/types/custom_type_collection_coercer.rb +++ b/lib/grape/validations/types/custom_type_collection_coercer.rb @@ -1,12 +1,8 @@ +# frozen_string_literal: true + module Grape module Validations module Types - # Instances of this class may be passed to - # +Virtus::Attribute.build+ as the +:coercer+ - # option, to handle collections of types that - # provide their own parsing (and optionally, - # type-checking) functionality. - # # See {CustomTypeCoercer} for details on types # that will be supported by this by this coercer. # This coercer works in the same way as +CustomTypeCoercer+ @@ -38,32 +34,21 @@ def initialize(type, set = false) @set = set end - # This method is called from somewhere within - # +Virtus::Attribute::coerce+ in order to coerce - # the given value. + # 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 { |item| super(item) } + coerced = value.map do |item| + coerced_item = super(item) - @set ? Set.new(coerced) : coerced - end + return coerced_item if coerced_item.is_a?(InvalidValue) - # This method is called from somewhere within - # +Virtus::Attribute::value_coerced?+ in order to assert - # that the all of the values in the array have been coerced - # successfully. - # - # @param primitive [Axiom::Types::Type] primitive type for - # the coercion as deteced by axiom-types' inference system. - # @param value [Enumerable] a coerced result returned from {#call} - # @return [true,false] whether or not all of the coerced values in - # the collection satisfy type requirements. - def success?(primitive, value) - value.is_a?(@set ? Set : Array) && - value.all? { |item| super(primitive, item) } + coerced_item + end + + @set ? Set.new(coerced) : coerced end end end diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb new file mode 100644 index 000000000..0a682e53e --- /dev/null +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'dry-types' + +module DryTypes + # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is + # a container in this case. Check documentation for more information + # https://dry-rb.org/gems/dry-types/1.2/getting-started/ + include Dry.Types() +end + +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/1.2/built-in-types/ + class DryTypeCoercer + class << self + # Registers a collection coercer which could be found by a type, + # see +collection_coercer_for+ method below. This method is meant for inheritors. + def register_collection(type) + DryTypeCoercer.collection_coercers[type] = self + end + + # Returns a collection coercer which corresponds to a given type. + # Example: + # + # collection_coercer_for(Array) + # #=> Grape::Validations::Types::ArrayCoercer + def collection_coercer_for(type) + collection_coercers[type] + end + + # Returns an instance of a coercer for a given type + def coercer_instance_for(type, strict = false) + return PrimitiveCoercer.new(type, strict) if type.class == Class + + # in case of a collection (Array[Integer]) the type is an instance of a collection, + # so we need to figure out the actual type + collection_coercer_for(type.class).new(type, strict) + end + + protected + + def collection_coercers + @collection_coercers ||= {} + end + end + + def initialize(type, strict = false) + @type = type + @strict = strict + @scope = strict ? DryTypes::Strict : DryTypes::Params + 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 => _e + InvalidValue.new + end + + protected + + attr_reader :scope, :type, :strict + end + end + end +end diff --git a/lib/grape/validations/types/file.rb b/lib/grape/validations/types/file.rb index fe1aedc32..8c2f6d924 100644 --- a/lib/grape/validations/types/file.rb +++ b/lib/grape/validations/types/file.rb @@ -1,25 +1,29 @@ +# frozen_string_literal: true + module Grape module Validations module Types - # +Virtus::Attribute+ 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, and to prevent virtus - # from interfering. - class File < Virtus::Attribute - def coerce(input) - # Processing of multipart file objects - # is already taken care of by Rack::Request. - # Nothing to do here. - input - end + # 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 value_coerced?(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) + 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 diff --git a/lib/grape/validations/types/invalid_value.rb b/lib/grape/validations/types/invalid_value.rb new file mode 100644 index 000000000..5c566a642 --- /dev/null +++ b/lib/grape/validations/types/invalid_value.rb @@ -0,0 +1,24 @@ +# 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 + +# only exists to make it shorter for external use +module Grape + module Types + InvalidValue = Class.new(Grape::Validations::Types::InvalidValue) + end +end diff --git a/lib/grape/validations/types/json.rb b/lib/grape/validations/types/json.rb index 220d1db6d..25dded6f0 100644 --- a/lib/grape/validations/types/json.rb +++ b/lib/grape/validations/types/json.rb @@ -1,43 +1,48 @@ +# frozen_string_literal: true + require 'json' module Grape module Validations module Types - # +Virtus::Attribute+ implementation that handles coercion - # and type checking for parameters that are complex types - # given as JSON-encoded strings. It accepts both JSON objects + # 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 < Virtus::Attribute - # Coerce the input into a JSON-like data structure. - # - # @param input [String] a JSON-encoded parameter value - # @return [Hash,Array,nil] - def coerce(input) - # Allow nulls and blank strings - return if input.nil? || input =~ /^\s*$/ - JSON.parse(input, symbolize_names: true) - end + 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) - # Checks that the input was parsed successfully - # and isn't something odd such as an array of primitives. - # - # @param value [Object] result of {#coerce} - # @return [true,false] - def value_coerced?(value) - value.is_a?(::Hash) || coerced_collection?(value) - end + # Allow nulls and blank strings + return if input.nil? || input.match?(/^\s*$/) + JSON.parse(input, symbolize_names: true) + end - protected + # 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 - # Is the value an array of JSON-like objects? - # - # @param value [Object] result of {#coerce} - # @return [true,false] - def coerced_collection?(value) - value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash } + 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? { |i| i.is_a? ::Hash } + end end end @@ -46,18 +51,20 @@ def coerced_collection?(value) # objects and arrays of objects, but wraps single objects # in an Array. class JsonArray < Json - # See {Json#coerce}. Wraps single objects in an array. - # - # @param input [String] JSON-encoded parameter value - # @return [Array] - def coerce(input) - json = super - Array.wrap(json) unless json.nil? - end + 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 value_coerced?(value) - coerced_collection? value + # See {Json#coerced_collection?} + def parsed?(value) + coerced_collection? value + end end end end diff --git a/lib/grape/validations/types/multiple_type_coercer.rb b/lib/grape/validations/types/multiple_type_coercer.rb index e78745382..304746ae2 100644 --- a/lib/grape/validations/types/multiple_type_coercer.rb +++ b/lib/grape/validations/types/multiple_type_coercer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Validations module Types @@ -22,53 +24,32 @@ def initialize(types, method = nil) @type_coercers = types.map do |type| if Types.multiple? type - VariantCollectionCoercer.new type + VariantCollectionCoercer.new type, @method else - Types.build_coercer type + Types.build_coercer type, strict: !@method.nil? end end end - # This method is called from somewhere within - # +Virtus::Attribute::coerce+ in order to coerce - # the given value. + # Coerces the given value. # - # @param value [String] value to be coerced, in grape + # @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(value) - return @method.call(value) if @method + 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 = coercer.coerce(value) + coerced_val = coercer.call(val) - return coerced if coercer.value_coerced? coerced + return coerced_val unless coerced_val.is_a?(InvalidValue) end - # Declare that we couldn't coerce the value in such a way - # that Grape won't ask us again if the value is valid - InvalidValue.new - end - - # This method is called from somewhere within - # +Virtus::Attribute::value_coerced?+ in order to - # assert that the value has been coerced successfully. - # Due to Grape's design this will in fact only be called - # if a custom coercion method is being used, since {#call} - # returns an {InvalidValue} object if the value could not - # be coerced. - # - # @param _primitive [Axiom::Types::Type] primitive type - # for the coercion as detected by axiom-types' inference - # system. For custom types this is typically not much use - # (i.e. it is +Axiom::Types::Object+) unless special - # inference rules have been declared for the type. - # @param value [Object] a coerced result returned from {#call} - # @return [true,false] whether or not the coerced value - # satisfies type requirements. - def success?(_primitive, value) - @type_coercers.any? { |coercer| coercer.value_coerced? value } + coerced_val end end end diff --git a/lib/grape/validations/types/primitive_coercer.rb b/lib/grape/validations/types/primitive_coercer.rb new file mode 100644 index 000000000..7b1e84180 --- /dev/null +++ b/lib/grape/validations/types/primitive_coercer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative 'dry_type_coercer' + +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 + MAPPING = { + Grape::API::Boolean => DryTypes::Params::Bool, + BigDecimal => DryTypes::Params::Decimal, + + # unfortunately, a +Params+ scope doesn't contain String + String => DryTypes::Coercible::String + }.freeze + + STRICT_MAPPING = { + Grape::API::Boolean => DryTypes::Strict::Bool, + BigDecimal => DryTypes::Strict::Decimal + }.freeze + + def initialize(type, strict = false) + super + + @type = type + + @coercer = if strict + STRICT_MAPPING.fetch(type) { scope.const_get(type.name) } + else + MAPPING.fetch(type) { scope.const_get(type.name) } + end + 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 diff --git a/lib/grape/validations/types/set_coercer.rb b/lib/grape/validations/types/set_coercer.rb new file mode 100644 index 000000000..dc76fc773 --- /dev/null +++ b/lib/grape/validations/types/set_coercer.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'set' +require_relative 'array_coercer' + +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 + register_collection Set + + 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 diff --git a/lib/grape/validations/types/variant_collection_coercer.rb b/lib/grape/validations/types/variant_collection_coercer.rb index f387457ee..34982e133 100644 --- a/lib/grape/validations/types/variant_collection_coercer.rb +++ b/lib/grape/validations/types/variant_collection_coercer.rb @@ -1,9 +1,11 @@ +# 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 < Virtus::Attribute + 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 @@ -30,8 +32,8 @@ def initialize(types, method = nil) # @return [Array,Set,InvalidValue] # the coerced result, or an instance # of {InvalidValue} if the value could not be coerced. - def coerce(value) - return InvalidValue.new unless value.is_a? Array + def call(value) + return unless value.is_a? Array value = if @method @@ -43,16 +45,6 @@ def coerce(value) value end - - # Assert that the value has been coerced successfully. - # - # @param value [Object] a coerced result returned from {#coerce} - # @return [true,false] whether or not the coerced value - # satisfies type requirements. - def value_coerced?(value) - value.is_a?(@types.class) && - value.all? { |v| @member_coercer.success?(@types, v) } - end end end end diff --git a/lib/grape/validations/types/virtus_collection_patch.rb b/lib/grape/validations/types/virtus_collection_patch.rb deleted file mode 100644 index eab5208b2..000000000 --- a/lib/grape/validations/types/virtus_collection_patch.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'virtus/attribute/collection' - -# See https://github.com/solnic/virtus/pull/343 -# This monkey-patch fixes type validation for collections, -# ensuring that type assertions are applied to collection -# members. -# -# This patch duplicates the code in the above pull request. -# Once the request, or equivalent functionality, has been -# published into the +virtus+ gem this file should be deleted. -Virtus::Attribute::Collection.class_eval do - # @api public - def value_coerced?(value) - super && value.all? { |item| member_type.value_coerced? item } - end -end diff --git a/lib/grape/validations/validator_factory.rb b/lib/grape/validations/validator_factory.rb index 996e3f065..444fa0421 100644 --- a/lib/grape/validations/validator_factory.rb +++ b/lib/grape/validations/validator_factory.rb @@ -1,17 +1,14 @@ +# frozen_string_literal: true + module Grape module Validations class ValidatorFactory - def initialize(**options) - @validator_class = options.delete(:validator_class) - @options = options - end - - def create_validator - @validator_class.new(@options[:attributes], - @options[:options], - @options[:required], - @options[:params_scope], - @options[:opts]) + def self.create_validator(**options) + options[:validator_class].new(options[:attributes], + options[:options], + options[:required], + options[:params_scope], + **options[:opts]) end end end diff --git a/lib/grape/validations/validators/all_or_none.rb b/lib/grape/validations/validators/all_or_none.rb index 02a06851d..186361f0d 100644 --- a/lib/grape/validations/validators/all_or_none.rb +++ b/lib/grape/validations/validators/all_or_none.rb @@ -1,19 +1,14 @@ +# frozen_string_literal: true + +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations - require 'grape/validations/validators/multiple_params_base' class AllOrNoneOfValidator < MultipleParamsBase - def validate!(params) - super - if scope_requires_params && only_subset_present - raise Grape::Exceptions::Validation, params: all_keys, message: message(:all_or_none) - end - params - end - - private - - def only_subset_present - scoped_params.any? { |resource_params| !keys_in_common(resource_params).empty? && keys_in_common(resource_params).length < attrs.length } + 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 diff --git a/lib/grape/validations/validators/allow_blank.rb b/lib/grape/validations/validators/allow_blank.rb index d0c314e7a..e212c273c 100644 --- a/lib/grape/validations/validators/allow_blank.rb +++ b/lib/grape/validations/validators/allow_blank.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Validations class AllowBlankValidator < Base @@ -9,7 +11,7 @@ def validate_param!(attr_name, params) return if value == false || value.present? - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:blank) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank)) end end end diff --git a/lib/grape/validations/validators/as.rb b/lib/grape/validations/validators/as.rb index d1840539b..77cef5f1c 100644 --- a/lib/grape/validations/validators/as.rb +++ b/lib/grape/validations/validators/as.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + module Grape module Validations class AsValidator < Base - def initialize(attrs, options, required, scope, opts = {}) - @alias = options + def initialize(attrs, options, required, scope, **opts) + @renamed_options = options super end def validate_param!(attr_name, params) - params[@alias] = params[attr_name] - params.delete(attr_name) + params[@renamed_options] = params[attr_name] end end end diff --git a/lib/grape/validations/validators/at_least_one_of.rb b/lib/grape/validations/validators/at_least_one_of.rb index 077393b63..001c784dd 100644 --- a/lib/grape/validations/validators/at_least_one_of.rb +++ b/lib/grape/validations/validators/at_least_one_of.rb @@ -1,19 +1,13 @@ +# frozen_string_literal: true + +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations - require 'grape/validations/validators/multiple_params_base' class AtLeastOneOfValidator < MultipleParamsBase - def validate!(params) - super - if scope_requires_params && no_exclusive_params_are_present - raise Grape::Exceptions::Validation, params: all_keys, message: message(:at_least_one) - end - params - end - - private - - def no_exclusive_params_are_present - scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? } + 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 diff --git a/lib/grape/validations/validators/base.rb b/lib/grape/validations/validators/base.rb index 169e2155a..84584e0b3 100644 --- a/lib/grape/validations/validators/base.rb +++ b/lib/grape/validations/validators/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Grape module Validations class Base @@ -10,13 +12,15 @@ class Base # @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 = {}) + # @param opts [Array] additional validation options + def initialize(attrs, options, required, scope, *opts) @attrs = Array(attrs) @option = options @required = required @scope = scope - @fail_fast = opts[:fail_fast] || false + opts = opts.any? ? opts.shift : {} + @fail_fast = opts.fetch(:fail_fast, false) + @allow_blank = opts.fetch(:allow_blank, false) end # Validates a given request. @@ -35,31 +39,31 @@ def validate(request) # @raise [Grape::Exceptions::Validation] if validation failed # @return [void] def validate!(params) - attributes = AttributesIterator.new(self, @scope, params) + attributes = SingleAttributeIterator.new(self, @scope, params) + # we collect errors inside array because + # there may be more than one error per field array_errors = [] - attributes.each do |resource_params, attr_name| - next if !@scope.required? && resource_params.empty? - next unless @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name)) - next unless @scope.meets_dependency?(resource_params, params) + attributes.each do |val, attr_name, empty_val, skip_value| + next if skip_value + next if !@scope.required? && empty_val + next unless @scope.meets_dependency?(val, params) begin - validate_param!(attr_name, resource_params) + validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name) rescue Grape::Exceptions::Validation => e - # we collect errors inside array because - # there may be more than one error per field array_errors << e end end - raise Grape::Exceptions::ValidationArrayErrors, array_errors if array_errors.any? + raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any? end def self.convert_to_short_name(klass) ret = klass.name.gsub(/::/, '/') - .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') - .gsub(/([a-z\d])([A-Z])/, '\1_\2') - .tr('-', '_') - .downcase + ret.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + ret.gsub!(/([a-z\d])([A-Z])/, '\1_\2') + ret.tr!('-', '_') + ret.downcase! File.basename(ret, '_validator') end diff --git a/lib/grape/validations/validators/coerce.rb b/lib/grape/validations/validators/coerce.rb index 4d1a32e95..f79b4fa6e 100644 --- a/lib/grape/validations/validators/coerce.rb +++ b/lib/grape/validations/validators/coerce.rb @@ -1,13 +1,30 @@ +# frozen_string_literal: true + module Grape class API - Boolean = Virtus::Attribute::Boolean + class Boolean + def self.build(val) + return nil if val != true && val != false + + new + end + end + + class Instance + Boolean = Grape::API::Boolean + end end module Validations class CoerceValidator < Base - def initialize(*_args) + def initialize(attrs, options, required, scope, **opts) super - @converter = Types.build_coercer(type, @option[:method]) + + @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer) + type + else + Types.build_coercer(type, method: @option[:method]) + end end def validate(request) @@ -15,10 +32,23 @@ def validate(request) end def validate_param!(attr_name, params) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless params.is_a? Hash - return unless requires_coercion?(params[attr_name]) + raise validation_exception(attr_name) unless params.is_a? Hash + new_value = coerce_value(params[attr_name]) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:coerce) unless valid_type?(new_value) + + 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].class == new_value.class && params[attr_name] == new_value + params[attr_name] = new_value end @@ -29,32 +59,17 @@ def validate_param!(attr_name, params) # # See {Types.build_coercer} # - # @return [Virtus::Attribute] + # @return [Object] attr_reader :converter def valid_type?(val) - # Special value to denote coercion failure - return false if val.instance_of?(Types::InvalidValue) - - # Allow nil, to ignore when a parameter is absent - return true if val.nil? - - converter.value_coerced? val + !val.is_a?(Types::InvalidValue) end def coerce_value(val) - # Don't coerce things other than nil to Arrays or Hashes - unless (@option[:method] && !val.nil?) || type.is_a?(Virtus::Attribute) - return val || [] if type == Array - return val || Set.new if type == Set - return val || {} if type == Hash - end - - converter.coerce(val) - - # not the prettiest but some invalid coercion can currently trigger - # errors in Virtus (see coerce_spec.rb:75) - rescue + converter.call(val) + # Some custom types might fail, so it should be treated as an invalid value + rescue StandardError Types::InvalidValue.new end @@ -65,9 +80,11 @@ def type @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type] end - def requires_coercion?(value) - # JSON types do not require coercion if value is valid - !valid_type?(value) || converter.coercer.respond_to?(:method) && !converter.is_a?(Grape::Validations::Types::Json) + 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 diff --git a/lib/grape/validations/validators/default.rb b/lib/grape/validations/validators/default.rb index c472cf31b..dbf754ed8 100644 --- a/lib/grape/validations/validators/default.rb +++ b/lib/grape/validations/validators/default.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + module Grape module Validations class DefaultValidator < Base - def initialize(attrs, options, required, scope, opts = {}) + def initialize(attrs, options, required, scope, **opts) @default = options super end def validate_param!(attr_name, params) - return if params.key? attr_name params[attr_name] = if @default.is_a? Proc @default.call elsif @default.frozen? || !duplicatable?(@default) @@ -18,11 +19,10 @@ def validate_param!(attr_name, params) end def validate!(params) - attrs = AttributesIterator.new(self, @scope, params) + attrs = SingleAttributeIterator.new(self, @scope, params) attrs.each do |resource_params, attr_name| - if resource_params.is_a?(Hash) && resource_params[attr_name].nil? - validate_param!(attr_name, resource_params) - end + 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 diff --git a/lib/grape/validations/validators/exactly_one_of.rb b/lib/grape/validations/validators/exactly_one_of.rb index 07283c783..b8e4ecb9c 100644 --- a/lib/grape/validations/validators/exactly_one_of.rb +++ b/lib/grape/validations/validators/exactly_one_of.rb @@ -1,28 +1,15 @@ -module Grape - module Validations - require 'grape/validations/validators/mutual_exclusion' - class ExactlyOneOfValidator < MutualExclusionValidator - def validate!(params) - super - if scope_requires_params && none_of_restricted_params_is_present - raise Grape::Exceptions::Validation, params: all_keys, message: message(:exactly_one) - end - params - end +# frozen_string_literal: true - def message(default_key = nil) - options = instance_variable_get(:@option) - if options_key?(:message) - (options_key?(default_key, options[:message]) ? options[:message][default_key] : options[:message]) - else - default_key - end - end +require 'grape/validations/validators/multiple_params_base' - private - - def none_of_restricted_params_is_present - scoped_params.any? { |resource_params| keys_in_common(resource_params).empty? } +module Grape + module Validations + 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.length.zero? + raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion)) end end end diff --git a/lib/grape/validations/validators/except_values.rb b/lib/grape/validations/validators/except_values.rb index a98fc193d..5ba1e306b 100644 --- a/lib/grape/validations/validators/except_values.rb +++ b/lib/grape/validations/validators/except_values.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Grape module Validations class ExceptValuesValidator < Base - def initialize(attrs, options, required, scope, opts = {}) + def initialize(attrs, options, required, scope, **opts) @except = options.is_a?(Hash) ? options[:value] : options super end @@ -13,7 +15,7 @@ def validate_param!(attr_name, params) return if excepts.nil? param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:except_values) if param_array.any? { |param| excepts.include?(param) } + 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 diff --git a/lib/grape/validations/validators/multiple_params_base.rb b/lib/grape/validations/validators/multiple_params_base.rb index 14209dff8..9ed0b6b96 100644 --- a/lib/grape/validations/validators/multiple_params_base.rb +++ b/lib/grape/validations/validators/multiple_params_base.rb @@ -1,26 +1,33 @@ +# frozen_string_literal: true + module Grape module Validations class MultipleParamsBase < Base - attr_reader :scoped_params - def validate!(params) - @scoped_params = [@scope.params(params)].flatten - params - end + attributes = MultipleAttributesIterator.new(self, @scope, params) + array_errors = [] - private + attributes.each do |resource_params, skip_value| + next if skip_value + begin + validate_params!(resource_params) + rescue Grape::Exceptions::Validation => e + array_errors << e + end + end - def scope_requires_params - @scope.required? || scoped_params.any?(&:any?) + 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.stringify_keys.keys).map(&:to_s) + all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) } end def all_keys - attrs.map(&:to_s) + attrs.map { |attr| @scope.full_name(attr) } end end end diff --git a/lib/grape/validations/validators/mutual_exclusion.rb b/lib/grape/validations/validators/mutual_exclusion.rb index d46d56351..bcd25bcae 100644 --- a/lib/grape/validations/validators/mutual_exclusion.rb +++ b/lib/grape/validations/validators/mutual_exclusion.rb @@ -1,24 +1,14 @@ +# frozen_string_literal: true + +require 'grape/validations/validators/multiple_params_base' + module Grape module Validations - require 'grape/validations/validators/multiple_params_base' class MutualExclusionValidator < MultipleParamsBase - attr_reader :processing_keys_in_common - - def validate!(params) - super - if two_or_more_exclusive_params_are_present - raise Grape::Exceptions::Validation, params: processing_keys_in_common, message: message(:mutual_exclusion) - end - params - end - - private - - def two_or_more_exclusive_params_are_present - scoped_params.any? do |resource_params| - @processing_keys_in_common = keys_in_common(resource_params) - @processing_keys_in_common.length > 1 - end + 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 diff --git a/lib/grape/validations/validators/presence.rb b/lib/grape/validations/validators/presence.rb index eddde01dc..92ec570f4 100644 --- a/lib/grape/validations/validators/presence.rb +++ b/lib/grape/validations/validators/presence.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module Grape module Validations class PresenceValidator < Base def validate_param!(attr_name, params) return if params.respond_to?(:key?) && params.key?(attr_name) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:presence) + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence)) end end end diff --git a/lib/grape/validations/validators/regexp.rb b/lib/grape/validations/validators/regexp.rb index 749a42ce2..23f6a29ad 100644 --- a/lib/grape/validations/validators/regexp.rb +++ b/lib/grape/validations/validators/regexp.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Grape module Validations class RegexpValidator < Base def validate_param!(attr_name, params) return unless params.respond_to?(:key?) && params.key?(attr_name) - return if Array.wrap(params[attr_name]).all? { |param| param.nil? || (param.to_s =~ (options_key?(:value) ? @option[:value] : @option)) } - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:regexp) + return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) } + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp)) end end end diff --git a/lib/grape/validations/validators/same_as.rb b/lib/grape/validations/validators/same_as.rb new file mode 100644 index 000000000..087150f16 --- /dev/null +++ b/lib/grape/validations/validators/same_as.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Grape + module Validations + 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 + format I18n.t(:same_as, scope: 'grape.errors.messages'), parameter: @option + end + end + end + end +end diff --git a/lib/grape/validations/validators/values.rb b/lib/grape/validations/validators/values.rb index 08f019b52..f3d676d0b 100644 --- a/lib/grape/validations/validators/values.rb +++ b/lib/grape/validations/validators/values.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Grape module Validations class ValuesValidator < Base - def initialize(attrs, options, required, scope, opts = {}) + def initialize(attrs, options, required, scope, **opts) if options.is_a?(Hash) @excepts = options[:except] @values = options[:value] @@ -24,17 +26,23 @@ def initialize(attrs, options, required, scope, opts = {}) def validate_param!(attr_name, params) return unless params.is_a?(Hash) - return unless params[attr_name] || required_for_root_scope? - param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name]) + val = params[attr_name] + + return if val.nil? && !required_for_root_scope? + + # don't forget that +false.blank?+ is true + return if val != false && val.blank? && @allow_blank - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: except_message \ + param_array = val.nil? ? [nil] : Array.wrap(val) + + raise validation_exception(attr_name, except_message) \ unless check_excepts(param_array) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \ + raise validation_exception(attr_name, message(:values)) \ unless check_values(param_array, attr_name) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:values) \ + raise validation_exception(attr_name, message(:values)) \ if @proc && !param_array.all? { |param| @proc.call(param) } end @@ -66,6 +74,10 @@ def except_message def required_for_root_scope? @required && @scope.root? end + + def validation_exception(attr_name, message) + Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) + end end end end diff --git a/lib/grape/version.rb b/lib/grape/version.rb index 47a2dda13..9c0689139 100644 --- a/lib/grape/version.rb +++ b/lib/grape/version.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + module Grape # The current version of Grape. - VERSION = '1.1.0'.freeze + VERSION = '1.5.3' end diff --git a/spec/grape/api/custom_validations_spec.rb b/spec/grape/api/custom_validations_spec.rb index 278beb7a9..f69ec5199 100644 --- a/spec/grape/api/custom_validations_spec.rb +++ b/spec/grape/api/custom_validations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations do @@ -8,7 +10,7 @@ class DefaultLength < Grape::Validations::Base 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, params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long" + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "must be at the most #{@option} characters long") end end end @@ -87,7 +89,7 @@ def app module CustomValidationsSpec class WithMessageKey < Grape::Validations::PresenceValidator def validate_param!(attr_name, _params) - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: :presence + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: :presence) end end end @@ -126,7 +128,7 @@ def validate(request) 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, params: @attrs, message: 'Can not set Admin only field.' unless request.headers['X-Access-Token'] == 'admin' + raise Grape::Exceptions::Validation.new(params: @attrs, message: 'Can not set Admin only field.') unless request.headers['X-Access-Token'] == 'admin' end end end diff --git a/spec/grape/api/deeply_included_options_spec.rb b/spec/grape/api/deeply_included_options_spec.rb index 23710e427..71cc1385b 100644 --- a/spec/grape/api/deeply_included_options_spec.rb +++ b/spec/grape/api/deeply_included_options_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module DeeplyIncludedOptionsSpec diff --git a/spec/grape/api/defines_boolean_in_params_spec.rb b/spec/grape/api/defines_boolean_in_params_spec.rb new file mode 100644 index 000000000..8a0302a23 --- /dev/null +++ b/spec/grape/api/defines_boolean_in_params_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::API::Instance do + describe 'boolean constant' do + module DefinesBooleanInstanceSpec + class API < Grape::API + params do + requires :message, type: Boolean + end + post :echo do + { class: params[:message].class.name, value: params[:message] } + end + end + end + + def app + DefinesBooleanInstanceSpec::API + 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 + + context 'Params endpoint type' do + subject { DefinesBooleanInstanceSpec::API.new.router.map['POST'].first.options[:params]['message'][:type] } + it 'params type is a boolean' do + is_expected.to eq 'Grape::API::Boolean' + end + end + end +end diff --git a/spec/grape/api/inherited_helpers_spec.rb b/spec/grape/api/inherited_helpers_spec.rb index a6c0695d8..be2cb9179 100644 --- a/spec/grape/api/inherited_helpers_spec.rb +++ b/spec/grape/api/inherited_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API::Helpers do diff --git a/spec/grape/api/instance_spec.rb b/spec/grape/api/instance_spec.rb new file mode 100644 index 000000000..f0a278a7b --- /dev/null +++ b/spec/grape/api/instance_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' +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 'top level setting' do + it 'does not inherit settings from the superclass (Grape::API::Instance)' do + expect(an_instance.top_level_setting.parent).to be_nil + 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 diff --git a/spec/grape/api/invalid_format_spec.rb b/spec/grape/api/invalid_format_spec.rb index 574769448..e3e78f7be 100644 --- a/spec/grape/api/invalid_format_spec.rb +++ b/spec/grape/api/invalid_format_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do diff --git a/spec/grape/api/namespace_parameters_in_route_spec.rb b/spec/grape/api/namespace_parameters_in_route_spec.rb index 0baa341ba..e8496a4a5 100644 --- a/spec/grape/api/namespace_parameters_in_route_spec.rb +++ b/spec/grape/api/namespace_parameters_in_route_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do diff --git a/spec/grape/api/nested_helpers_spec.rb b/spec/grape/api/nested_helpers_spec.rb index fb3f7d3f0..2f9c61aba 100644 --- a/spec/grape/api/nested_helpers_spec.rb +++ b/spec/grape/api/nested_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API::Helpers do diff --git a/spec/grape/api/optional_parameters_in_route_spec.rb b/spec/grape/api/optional_parameters_in_route_spec.rb index 18aacbd5d..6bc6bc47d 100644 --- a/spec/grape/api/optional_parameters_in_route_spec.rb +++ b/spec/grape/api/optional_parameters_in_route_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do diff --git a/spec/grape/api/parameters_modification_spec.rb b/spec/grape/api/parameters_modification_spec.rb index 7bff02d5b..2d1ea1e20 100644 --- a/spec/grape/api/parameters_modification_spec.rb +++ b/spec/grape/api/parameters_modification_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do @@ -10,7 +12,7 @@ def app before do subject.namespace :test do params do - optional :foo, default: '-abcdef' + optional :foo, default: +'-abcdef' end get do params[:foo].slice!(0) diff --git a/spec/grape/api/patch_method_helpers_spec.rb b/spec/grape/api/patch_method_helpers_spec.rb index 71d27ec7f..a369a284a 100644 --- a/spec/grape/api/patch_method_helpers_spec.rb +++ b/spec/grape/api/patch_method_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API::Helpers do diff --git a/spec/grape/api/recognize_path_spec.rb b/spec/grape/api/recognize_path_spec.rb index 5b86a0789..0d821f57b 100644 --- a/spec/grape/api/recognize_path_spec.rb +++ b/spec/grape/api/recognize_path_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API do diff --git a/spec/grape/api/required_parameters_in_route_spec.rb b/spec/grape/api/required_parameters_in_route_spec.rb index bc0f05e97..63544892a 100644 --- a/spec/grape/api/required_parameters_in_route_spec.rb +++ b/spec/grape/api/required_parameters_in_route_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do diff --git a/spec/grape/api/required_parameters_with_invalid_method_spec.rb b/spec/grape/api/required_parameters_with_invalid_method_spec.rb index 3a8b0856f..0829492cf 100644 --- a/spec/grape/api/required_parameters_with_invalid_method_spec.rb +++ b/spec/grape/api/required_parameters_with_invalid_method_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do diff --git a/spec/grape/api/routes_with_requirements_spec.rb b/spec/grape/api/routes_with_requirements_spec.rb new file mode 100644 index 000000000..83c5b85ec --- /dev/null +++ b/spec/grape/api/routes_with_requirements_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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 diff --git a/spec/grape/api/shared_helpers_exactly_one_of_spec.rb b/spec/grape/api/shared_helpers_exactly_one_of_spec.rb index a6d0cb22f..049fd8706 100644 --- a/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +++ b/spec/grape/api/shared_helpers_exactly_one_of_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API::Helpers do diff --git a/spec/grape/api/shared_helpers_spec.rb b/spec/grape/api/shared_helpers_spec.rb index 3b0a85a9e..68626944d 100644 --- a/spec/grape/api/shared_helpers_spec.rb +++ b/spec/grape/api/shared_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API::Helpers do diff --git a/spec/grape/api_remount_spec.rb b/spec/grape/api_remount_spec.rb new file mode 100644 index 000000000..ff764293c --- /dev/null +++ b/spec/grape/api_remount_spec.rb @@ -0,0 +1,473 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'shared/versioning_examples' + +describe Grape::API do + subject(:a_remounted_api) { Class.new(Grape::API) } + let(:root_api) { Class.new(Grape::API) } + + def app + root_api + end + + 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(Grape::API) 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.status).to eq 404 + end + end + + context 'when using an expression derived from a configuration' do + subject(:a_remounted_api) do + Class.new(Grape::API) 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(Grape::API) 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 executing a standard block within a `mounted` block with all dynamic params' do + subject(:a_remounted_api) do + Class.new(Grape::API) 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(Grape::API) 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(Grape::API) 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.status).to eq 404 + + 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(Grape::API) 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(Grape::API) do + desc 'The description of this' do + tags ['not_configurable_tag', configuration[:a_configurable_tag]] + end + get 'location' do + 'success' + 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' }) + end + end + + context 'on the ParamScope' do + subject(:a_remounted_api) do + Class.new(Grape::API) 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.status).to eq 400 + + get '/integer/location', param_integer: 1 + expect(last_response.body).to eq 'success' + + get '/integer/location', param_integer: 'a' + expect(last_response.status).to eq 400 + end + + context 'on dynamic checks' do + subject(:a_remounted_api) do + Class.new(Grape::API) 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.status).to eq 400 + 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 'will use 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.status).to eq 400 + end + end + + context 'a very complex configuration example' do + before do + top_level_api = Class.new(Grape::API) 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(Grape::API) do + helpers do + def printed_response + configuration[:some_value] + end + end + + get 'location' do + printed_response + end + end + end + + it 'will use 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(Grape::API) do + get 'location' do + configuration[:some_value] + end + end + end + + it 'will use 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 + end +end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index c38205f47..0cb7e1251 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'shared/versioning_examples' @@ -220,6 +222,15 @@ def app 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 @@ -805,6 +816,71 @@ class DummyFormatClass 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.status).to eql 405 + 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.status).to eql 401 + 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.status).to eql 401 + 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.status).to eql 201 + 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.status).to eql 401 + 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.status).to eql 200 + end + end + end + context 'allows HEAD on a GET request that' do before do subject.get 'example' do @@ -876,6 +952,40 @@ class DummyFormatClass end end + describe '.compile!' do + it 'requires the grape/eager_load file' do + expect(app).to receive(:require).with('grape/eager_load') { nil } + app.compile! + end + + it 'compiles the instance for rack!' do + stubbed_object = double(:instance_for_rack) + allow(app).to receive(:instance_for_rack) { stubbed_object } + end + end + + # NOTE: this method is required to preserve the ability of pre-mounting + # the root API into a namespace, it may be deprecated in the future. + describe 'instance_for_rack' do + context 'when the app was not mounted' do + it 'returns the base_instance' do + expect(app.send(:instance_for_rack)).to eq app.base_instance + end + end + + context 'when the app was mounted' do + it 'returns the first mounted instance' do + mounted_app = app + Class.new(Grape::API) do + namespace 'new_namespace' do + mount mounted_app + end + end + expect(app.send(:instance_for_rack)).to eq app.send(:mounted_instances).first + end + end + end + describe 'filters' do it 'adds a before filter' do subject.before { @foo = 'first' } @@ -890,7 +1000,7 @@ class DummyFormatClass it 'adds a before filter to current and child namespaces only' do subject.get '/' do - "root - #{@foo}" + "root - #{instance_variable_defined?(:@foo) ? @foo : nil}" end subject.namespace :blah do before { @foo = 'foo' } @@ -1039,6 +1149,11 @@ class DummyFormatClass expect(last_response.headers['Content-Type']).to eq('text/plain') end + it 'does not set Cache-Control' do + get '/foo' + expect(last_response.headers['Cache-Control']).to eq(nil) + end + it 'sets content type for xml' do get '/foo.xml' expect(last_response.headers['Content-Type']).to eq('application/xml') @@ -1090,7 +1205,7 @@ class DummyFormatClass subject.use Rack::Chunked subject.get('/stream') { stream test_stream } - get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1' + get '/stream', {}, 'HTTP_VERSION' => 'HTTP/1.1', 'SERVER_PROTOCOL' => 'HTTP/1.1' expect(last_response.headers['Content-Type']).to eq('text/plain') expect(last_response.headers['Content-Length']).to eq(nil) @@ -1348,6 +1463,28 @@ def call(env) 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 ApiSpec::PhonyMiddleware, '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 + describe '.http_basic' do it 'protects any resources on the same scope' do subject.http_basic do |u, _p| @@ -1463,6 +1600,11 @@ def self.io expect(subject.io).to receive(:write).with(message) subject.logger.info 'this will be logged' end + + it 'does not unnecessarily retain duplicate setup blocks' do + subject.logger + expect { subject.logger }.to_not change(subject.instance_variable_get(:@setup), :size) + end end describe '.helpers' do @@ -1597,6 +1739,199 @@ def three 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 @@ -1643,9 +1978,9 @@ def foo it 'avoids polluting global namespace' do env = Rack::MockRequest.env_for('/') - expect(a.call(env)[2].body).to eq(['foo']) - expect(b.call(env)[2].body).to eq(['bar']) - expect(a.call(env)[2].body).to eq(['foo']) + expect(read_chunks(a.call(env)[2])).to eq(['foo']) + expect(read_chunks(b.call(env)[2])).to eq(['bar']) + expect(read_chunks(a.call(env)[2])).to eq(['foo']) end end @@ -1682,6 +2017,26 @@ def foo 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 eql 402 + + get '/standard_error' + expect(last_response.status).to eql 401 + end + context 'CustomError subclass of Grape::Exceptions::Base' do before do module ApiSpec @@ -1700,7 +2055,7 @@ class CustomError < Grape::Exceptions::Base; end rack_response('New Error', e.status) end subject.get '/custom_error' do - raise ApiSpec::CustomError, status: 400, message: 'Custom Error' + raise ApiSpec::CustomError.new(status: 400, message: 'Custom Error') end get '/custom_error' @@ -1723,6 +2078,16 @@ class CustomError < Grape::Exceptions::Base; end expect(last_response.status).to eql 500 expect(last_response.body).to eq('Formatter Error') end + + it 'uses default_rescue_handler to handle invalid response from rescue_from' do + subject.rescue_from(:all) { 'error' } + subject.get('/') { raise } + + expect_any_instance_of(Grape::Middleware::Error).to receive(:default_rescue_handler).and_call_original + get '/' + expect(last_response.status).to eql 500 + expect(last_response.body).to eql 'Invalid response' + end end describe '.rescue_from klass, block' do @@ -3177,6 +3542,43 @@ def static expect { a.mount b }.to_not 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 + module ClassMethods + def my_method + @test = true + end + end + end + end + + it 'should correctly include module in nested mount' do + module_to_include = included_module + v1 = Class.new(Grape::API) do + version :v1, using: :path + include module_to_include + my_method + end + v2 = Class.new(Grape::API) do + version :v2, using: :path + end + segment_base = Class.new(Grape::API) do + mount v1 + mount v2 + end + + Class.new(Grape::API) do + mount segment_base + end + + expect(v1.my_method).to be_truthy + end + end end end @@ -3192,7 +3594,7 @@ def static it 'sets the instance' do expect(subject.instance).to be_nil subject.compile - expect(subject.instance).to be_kind_of(subject) + expect(subject.instance).to be_kind_of(subject.base_instance) end end @@ -3350,12 +3752,13 @@ def before end end context ':serializable_hash' do - before(:each) do - class SerializableHashExample - def serializable_hash - { abc: 'def' } - end + class SerializableHashExample + def serializable_hash + { abc: 'def' } end + end + + before(:each) do subject.format :serializable_hash end it 'instance' do @@ -3449,6 +3852,44 @@ def before 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(Grape::API) @@ -3580,4 +4021,116 @@ def before end end end + + describe 'normal class methods' do + subject(:grape_api) { Class.new(Grape::API) } + + 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 'const_missing' do + subject(:grape_api) { Class.new(Grape::API) } + let(:mounted) do + Class.new(Grape::API) 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 + 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(Grape::API) do + include shared + + namespace(:orders) do + mount find + end + end + end + let(:orders_find_endpoint) do + shared = shared_api_definitions + Class.new(Grape::API) 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 + subject(:grape_api) do + Class.new(Grape::API) do + version 'v1', using: :path + 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 be_eql('id is invalid') + end + + it 'returns the given id when it is valid' do + get '/v1/orders/1-2' + expect(last_response.body).to be_eql('1-2') + end + end end diff --git a/spec/grape/config_spec.rb b/spec/grape/config_spec.rb new file mode 100644 index 000000000..07bed04a3 --- /dev/null +++ b/spec/grape/config_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '.configure' do + before do + Grape.configure do |config| + config.param_builder = 42 + end + end + + after do + Grape.config.reset + end + + it 'is configured to the new value' do + expect(Grape.config.param_builder).to eq 42 + end +end diff --git a/spec/grape/dsl/callbacks_spec.rb b/spec/grape/dsl/callbacks_spec.rb index db3fb7952..73dbc259e 100644 --- a/spec/grape/dsl/callbacks_spec.rb +++ b/spec/grape/dsl/callbacks_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/configuration_spec.rb b/spec/grape/dsl/configuration_spec.rb index 6216b007c..32b015f75 100644 --- a/spec/grape/dsl/configuration_spec.rb +++ b/spec/grape/dsl/configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/desc_spec.rb b/spec/grape/dsl/desc_spec.rb index a7ff7da33..9822620c0 100644 --- a/spec/grape/dsl/desc_spec.rb +++ b/spec/grape/dsl/desc_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -21,36 +23,60 @@ class Dummy it 'can be set with a block' do expected_options = { + summary: 'summary', description: 'The description', detail: 'more details', params: { first: :param }, entity: Object, http_codes: [[401, 'Unauthorized', 'Entities::Error']], named: 'My named route', - headers: [XAuthToken: { - description: 'Valdates your identity', - required: true - }, - XOptionalHeader: { - description: 'Not really needed', - required: false - }] + 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 failure [[401, 'Unauthorized', 'Entities::Error']] named 'My named route' - headers [XAuthToken: { - description: 'Valdates your identity', - required: true - }, - XOptionalHeader: { - description: 'Not really needed', - required: false - }] + 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) diff --git a/spec/grape/dsl/headers_spec.rb b/spec/grape/dsl/headers_spec.rb index de5763f10..d23652d07 100644 --- a/spec/grape/dsl/headers_spec.rb +++ b/spec/grape/dsl/headers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/helpers_spec.rb b/spec/grape/dsl/helpers_spec.rb index 6e24cfe00..2f7bbf5a1 100644 --- a/spec/grape/dsl/helpers_spec.rb +++ b/spec/grape/dsl/helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -75,9 +77,9 @@ def test end context 'with an external file' do - it 'sets Boolean as a Virtus::Attribute::Boolean' do + it 'sets Boolean as a Grape::API::Boolean' do subject.helpers BooleanParam - expect(subject.first_mod::Boolean).to eq Virtus::Attribute::Boolean + expect(subject.first_mod::Boolean).to eq Grape::API::Boolean end end diff --git a/spec/grape/dsl/inside_route_spec.rb b/spec/grape/dsl/inside_route_spec.rb index 928510f75..9d83c148d 100644 --- a/spec/grape/dsl/inside_route_spec.rb +++ b/spec/grape/dsl/inside_route_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -201,80 +203,229 @@ def initialize end describe '#file' do + before do + allow(subject).to receive(:warn) + end + describe 'set' do context 'as file path' do let(:file_path) { '/some/file/path' } - let(:file_response) do - file_body = Grape::ServeFile::FileBody.new(file_path) - Grape::ServeFile::FileResponse.new(file_body) - end + it 'emits a warning that this method is deprecated' do + expect(subject).to receive(:warn).with(/Use sendfile or stream/) - before do subject.file file_path end - it 'returns value wrapped in FileResponse' do - expect(subject.file).to eq file_response + it 'forwards the call to sendfile' do + expect(subject).to receive(:sendfile).with(file_path) + + subject.file file_path end end context 'as object (backward compatibility)' do - let(:file_object) { Class.new } + let(:file_object) { double('StreamerObject', each: nil) } + + it 'emits a warning that this method is deprecated' do + expect(subject).to receive(:warn).with(/Use stream to use a Stream object/) + + subject.file file_object + end + + it 'forwards the call to stream' do + expect(subject).to receive(:stream).with(file_object) + + subject.file file_object + end + end + end + + describe 'get' do + it 'emits a warning that this method is deprecated' do + expect(subject).to receive(:warn).with(/Use sendfile or stream/) + + subject.file + end + + it 'fowards call to sendfile' do + expect(subject).to receive(:sendfile) + + subject.file + end + end + end + + describe '#sendfile' do + describe 'set' do + context 'as file path' do + let(:file_path) { '/some/file/path' } let(:file_response) do - Grape::ServeFile::FileResponse.new(file_object) + file_body = Grape::ServeStream::FileBody.new(file_path) + Grape::ServeStream::StreamResponse.new(file_body) end before do - subject.file file_object + subject.header 'Cache-Control', 'cache' + subject.header 'Content-Length', 123 + subject.header 'Transfer-Encoding', 'base64' + end + + it 'sends no deprecation warnings' do + expect(subject).to_not receive(:warn) + + subject.sendfile file_path + end + + it 'returns value wrapped in StreamResponse' do + subject.sendfile file_path + + expect(subject.sendfile).to eq file_response + end + + it 'does not change the Cache-Control header' do + subject.sendfile file_path + + expect(subject.header['Cache-Control']).to eq 'cache' + end + + it 'does not change the Content-Length header' do + subject.sendfile file_path + + expect(subject.header['Content-Length']).to eq 123 + end + + it 'does not change the Transfer-Encoding header' do + subject.sendfile file_path + + expect(subject.header['Transfer-Encoding']).to eq 'base64' end + end - it 'returns value wrapped in FileResponse' do - expect(subject.file).to eq file_response + 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.file).to be nil + expect(subject.sendfile).to be nil end end describe '#stream' do describe 'set' do - let(:file_object) { Class.new } + context 'as a file path' do + let(:file_path) { '/some/file/path' } - before do - subject.header 'Cache-Control', 'cache' - subject.header 'Content-Length', 123 - subject.header 'Transfer-Encoding', 'base64' - subject.stream file_object - end + let(:file_response) do + file_body = Grape::ServeStream::FileBody.new(file_path) + Grape::ServeStream::StreamResponse.new(file_body) + end - it 'returns value wrapped in FileResponse' do - expect(subject.stream).to eq Grape::ServeFile::FileResponse.new(file_object) - end + before do + subject.header 'Cache-Control', 'cache' + subject.header 'Content-Length', 123 + subject.header 'Transfer-Encoding', 'base64' + end - it 'also sets result of file to value wrapped in FileResponse' do - expect(subject.file).to eq Grape::ServeFile::FileResponse.new(file_object) - end + it 'emits no deprecation warnings' do + expect(subject).to_not receive(:warn) + + subject.stream file_path + end + + it 'returns file body wrapped in StreamResponse' do + subject.stream file_path + + expect(subject.stream).to eq file_response + end + + it 'sets Cache-Control header to no-cache' do + subject.stream file_path + + expect(subject.header['Cache-Control']).to eq 'no-cache' + end + + it 'does not change Cache-Control header' do + subject.stream + + expect(subject.header['Cache-Control']).to eq 'cache' + end + + it 'sets Content-Length header to nil' do + subject.stream file_path + + expect(subject.header['Content-Length']).to eq nil + end - it 'sets Cache-Control header to no-cache' do - expect(subject.header['Cache-Control']).to eq 'no-cache' + it 'sets Transfer-Encoding header to nil' do + subject.stream file_path + + expect(subject.header['Transfer-Encoding']).to eq nil + end end - it 'sets Content-Length header to nil' do - expect(subject.header['Content-Length']).to eq nil + 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 'Cache-Control', 'cache' + subject.header 'Content-Length', 123 + subject.header 'Transfer-Encoding', 'base64' + end + + it 'emits no deprecation warnings' do + expect(subject).to_not receive(:warn) + + subject.stream stream_object + end + + it 'returns value wrapped in StreamResponse' do + subject.stream stream_object + + expect(subject.stream).to eq stream_response + end + + it 'sets Cache-Control header to no-cache' do + subject.stream stream_object + + expect(subject.header['Cache-Control']).to eq 'no-cache' + end + + it 'sets Content-Length header to nil' do + subject.stream stream_object + + expect(subject.header['Content-Length']).to eq nil + end + + it 'sets Transfer-Encoding header to nil' do + subject.stream stream_object + + expect(subject.header['Transfer-Encoding']).to eq nil + end end - it 'sets Transfer-Encoding header to nil' do - expect(subject.header['Transfer-Encoding']).to eq nil + 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.file).to be nil + expect(subject.stream).to be nil + expect(subject.header['Cache-Control']).to eq nil end end diff --git a/spec/grape/dsl/logger_spec.rb b/spec/grape/dsl/logger_spec.rb index 4a4ce831c..1992e3277 100644 --- a/spec/grape/dsl/logger_spec.rb +++ b/spec/grape/dsl/logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/middleware_spec.rb b/spec/grape/dsl/middleware_spec.rb index b9bf96094..b116fb735 100644 --- a/spec/grape/dsl/middleware_spec.rb +++ b/spec/grape/dsl/middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -22,6 +24,14 @@ class Dummy end end + describe '.insert' do + it 'adds a middleware with the right operation' do + expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert, 0, :arg1, proc]) + + subject.insert 0, :arg1, &proc + end + end + describe '.insert_before' do it 'adds a middleware with the right operation' do expect(subject).to receive(:namespace_stackable).with(:middleware, [:insert_before, foo_middleware, :arg1, proc]) diff --git a/spec/grape/dsl/parameters_spec.rb b/spec/grape/dsl/parameters_spec.rb index 27af242a2..bd60195e1 100644 --- a/spec/grape/dsl/parameters_spec.rb +++ b/spec/grape/dsl/parameters_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/request_response_spec.rb b/spec/grape/dsl/request_response_spec.rb index 7974537fb..00d9e3a6a 100644 --- a/spec/grape/dsl/request_response_spec.rb +++ b/spec/grape/dsl/request_response_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/routing_spec.rb b/spec/grape/dsl/routing_spec.rb index 60bd00bf9..e26d039d3 100644 --- a/spec/grape/dsl/routing_spec.rb +++ b/spec/grape/dsl/routing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -70,6 +72,16 @@ class Dummy expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2']) end + + it 'mounts multiple routes at once' do + base_app = Class.new(Grape::API) + app1 = Class.new(Grape::API) + app2 = Class.new(Grape::API) + base_app.mount(app1 => '/app1', app2 => '/app2') + + expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1']) + expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app2']) + end end describe '.route' do diff --git a/spec/grape/dsl/settings_spec.rb b/spec/grape/dsl/settings_spec.rb index 1620c4f7d..b2520d36f 100644 --- a/spec/grape/dsl/settings_spec.rb +++ b/spec/grape/dsl/settings_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/dsl/validations_spec.rb b/spec/grape/dsl/validations_spec.rb index a97bac5ed..e39069266 100644 --- a/spec/grape/dsl/validations_spec.rb +++ b/spec/grape/dsl/validations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/endpoint/declared_spec.rb b/spec/grape/endpoint/declared_spec.rb new file mode 100644 index 000000000..6d85dd19b --- /dev/null +++ b/spec/grape/endpoint/declared_spec.rb @@ -0,0 +1,601 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Endpoint do + subject { Class.new(Grape::API) } + + def app + subject + end + + 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_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 Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder + 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 - hashie mash' do + subject.params do + build_with Grape::Extensions::Hashie::Mash::ParamBuilder + 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 + + it 'returns an object that corresponds with the params class - hash' do + subject.params do + build_with Grape::Extensions::Hash::ParamBuilder + 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 'should show 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.status).to eq(200) + expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil + end + + it 'should show 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.status).to eq(200) + 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.status).to eq(200) + expect(JSON.parse(last_response.body).keys.size).to eq(11) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(201) + + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + + body = JSON.parse(last_response.body) + expect(body['empty_hash']).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.status).to eq(200) + + body = JSON.parse(last_response.body) + expect(['#', []]).to include(body['empty_set']) + expect(['#', []]).to include(body['empty_typed_set']) + expect(['#', []]).to include(body['nested']['empty_set']) + expect(['#', []]).to include(body['nested']['empty_typed_set']) + end + + it 'sets objects with type=Array to be an array' do + get '/declared?first=present' + expect(last_response.status).to eq(200) + + 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.status).to eq(200) + + 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.status).to eq(200) + expect(JSON.parse(last_response.body).key?(:other)).to eq 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(201) + 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.status).to eq(201) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq 200 } + + 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.status).to eq 200 + 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.status).to eq 200 + 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 eq 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.key?(:id)).to be_truthy + expect(json.key?(:artist_id)).not_to be_truthy + end + + it 'return only :artist_id without :id' do + get '/artists/1/compositions' + json = JSON.parse(last_response.body, symbolize_names: true) + + expect(json.key?(:artist_id)).to be_truthy + expect(json.key?(:id)).not_to be_truthy + 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.key?(:filter)).to be_truthy + expect(json.key?(:id)).to be_truthy + expect(json.key?(:artist_id)).not_to be_truthy + 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.key?(:compositor_id)).to be_truthy + expect(json.key?(:id)).not_to be_truthy + expect(json.key?(:artist_id)).not_to be_truthy + end + end +end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 5614eea76..487fbb0d3 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Endpoint do @@ -8,7 +10,7 @@ def app end describe '.before_each' do - after { Grape::Endpoint.before_each(nil) } + after { Grape::Endpoint.before_each.clear } it 'is settable via block' do block = ->(_endpoint) { 'noop' } @@ -149,7 +151,7 @@ def app it 'includes headers passed as symbols' do env = Rack::MockRequest.env_for('/headers') env['HTTP_SYMBOL_HEADER'.to_sym] = 'Goliath passes symbols' - body = subject.call(env)[2].body.first + body = read_chunks(subject.call(env)[2]).join expect(JSON.parse(body)['Symbol-Header']).to eq('Goliath passes symbols') end end @@ -278,527 +280,6 @@ def app end end - describe '#declared' do - before do - subject.format :json - subject.params do - requires :first - optional :second - optional :third, default: 'third-default' - 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 - end - optional :nested_arr, type: Array do - optional :eighth - end - 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 Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder - 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 - hashie mash' do - subject.params do - build_with Grape::Extensions::Hashie::Mash::ParamBuilder - 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 - - it 'returns an object that corresponds with the params class - hash' do - subject.params do - build_with Grape::Extensions::Hash::ParamBuilder - 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 'should show 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.status).to eq(200) - expect(JSON.parse(last_response.body)['nested']['fourth']).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.status).to eq(200) - expect(JSON.parse(last_response.body).keys.size).to eq(5) - 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.status).to eq(200) - 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.status).to eq(200) - expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 3 - 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.status).to eq(200) - expect(JSON.parse(last_response.body)['nested'].size).to eq 2 - end - - context 'sets nested objects when the param is missing' do - it 'to be a hash when include_missing is true' do - subject.get '/declared' do - declared(params, include_missing: true) - end - - get '/declared?first=present' - expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)['nested']).to be_a(Hash) - end - - it 'to be an array when include_missing is true' do - subject.get '/declared' do - declared(params, include_missing: true) - end - - get '/declared?first=present' - expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)['nested_arr']).to be_a(Array) - end - - it 'to be nil when include_missing is false' do - subject.get '/declared' do - declared(params, include_missing: false) - end - - get '/declared?first=present' - expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)['nested']).to be_nil - 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.status).to eq(200) - expect(JSON.parse(last_response.body).key?(:other)).to eq 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.status).to eq(200) - 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.status).to eq(200) - end - - it 'does not include aliased missing attributes if that option is passed' do - subject.params do - optional :aliased_original, as: :aliased - end - subject.get '/declared' do - error! 'expected nil', 400 if declared(params, include_missing: false).key?(:aliased) - '' - end - - get '/declared?first=one&other=two' - expect(last_response.status).to eq(200) - 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.status).to eq(201) - 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.status).to eq(201) - 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.status).to eq(200) - 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.status).to eq(200) - 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.status).to eq 200 } - - 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.status).to eq 200 - 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.status).to eq 200 - 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 eq 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.key?(:id)).to be_truthy - expect(json.key?(:artist_id)).not_to be_truthy - end - - it 'return only :artist_id without :id' do - get '/artists/1/compositions' - json = JSON.parse(last_response.body, symbolize_names: true) - - expect(json.key?(:artist_id)).to be_truthy - expect(json.key?(:id)).not_to be_truthy - 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.key?(:filter)).to be_truthy - expect(json.key?(:id)).to be_truthy - expect(json.key?(:artist_id)).not_to be_truthy - 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.key?(:compositor_id)).to be_truthy - expect(json.key?(:id)).not_to be_truthy - expect(json.key?(:artist_id)).not_to be_truthy - end - end - describe '#params' do it 'is available to the caller' do subject.get('/hey') do @@ -939,6 +420,19 @@ def app 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('Empty message body supplied with multipart/form-data; boundary=foobar content-type') + end end it 'responds with a 415 for an unsupported content-type' do @@ -1089,6 +583,36 @@ def app 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 @@ -1492,6 +1016,9 @@ def memoized have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + filters: [], + type: :finally }), have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), env: an_instance_of(Hash) }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), @@ -1518,6 +1045,9 @@ def memoized have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), filters: [], type: :after }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(Grape::Endpoint), + filters: [], + type: :finally }), have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash), formatter: a_kind_of(Module) }) ) diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 35299dfb9..beb304061 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'grape_entity' @@ -114,7 +116,7 @@ def first expect(last_response.body).to eq('Auto-detect!') end - it 'does not run autodetection for Entity when explicitely provided' do + it 'does not run autodetection for Entity when explicitly provided' do entity = Class.new(Grape::Entity) some_array = [] @@ -179,6 +181,7 @@ def first subject.get '/example' do c = Class.new do attr_reader :id + def initialize(id) @id = id end @@ -200,6 +203,7 @@ def initialize(id) subject.get '/examples' do c = Class.new do attr_reader :id + def initialize(id) @id = id end @@ -224,6 +228,7 @@ def initialize(id) subject.get '/example' do c = Class.new do attr_reader :name + def initialize(args) @name = args[:name] || 'no name set' end @@ -253,6 +258,7 @@ def initialize(args) subject.get '/example' do c = Class.new do attr_reader :name + def initialize(args) @name = args[:name] || 'no name set' end @@ -282,6 +288,7 @@ def initialize(args) subject.get '/example' do c = Class.new do attr_reader :name + def initialize(args) @name = args[:name] || 'no name set' end @@ -300,6 +307,7 @@ def initialize(args) 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 diff --git a/spec/grape/exceptions/base_spec.rb b/spec/grape/exceptions/base_spec.rb new file mode 100644 index 000000000..db970a74d --- /dev/null +++ b/spec/grape/exceptions/base_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Exceptions::Base do + 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 do + I18n.enforce_available_locales = true + I18n.available_locales = %i[en] + I18n.locale = :en + I18n.default_locale = :en + I18n.reload! + end + + context 'when I18n enforces available locales' do + before { I18n.enforce_available_locales = true } + + context 'when the fallback locale is available' do + before do + I18n.available_locales = %i[de en] + I18n.default_locale = :de + 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 + before do + I18n.available_locales = %i[de jp] + I18n.locale = :de + I18n.default_locale = :de + end + + it 'returns the translation string' do + expect(subject).to eq("grape.errors.messages.#{key}") + end + end + end + + context 'when I18n does not enforce available locales' do + before { I18n.enforce_available_locales = false } + + context 'when the fallback locale is available' do + before { I18n.available_locales = %i[de en] } + + 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 + before { I18n.available_locales = %i[de jp] } + + it 'returns the translated message' do + expect(subject).to eq('cannot convert String to xml') + end + end + end + end +end diff --git a/spec/grape/exceptions/body_parse_errors_spec.rb b/spec/grape/exceptions/body_parse_errors_spec.rb index 422e6719e..990758a5f 100644 --- a/spec/grape/exceptions/body_parse_errors_spec.rb +++ b/spec/grape/exceptions/body_parse_errors_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::ValidationErrors do diff --git a/spec/grape/exceptions/invalid_accept_header_spec.rb b/spec/grape/exceptions/invalid_accept_header_spec.rb index d308620cc..3a93ce3e1 100644 --- a/spec/grape/exceptions/invalid_accept_header_spec.rb +++ b/spec/grape/exceptions/invalid_accept_header_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::InvalidAcceptHeader do diff --git a/spec/grape/exceptions/invalid_formatter_spec.rb b/spec/grape/exceptions/invalid_formatter_spec.rb index b63280d67..3c60e3d13 100644 --- a/spec/grape/exceptions/invalid_formatter_spec.rb +++ b/spec/grape/exceptions/invalid_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::InvalidFormatter do diff --git a/spec/grape/exceptions/invalid_response_spec.rb b/spec/grape/exceptions/invalid_response_spec.rb new file mode 100644 index 000000000..8a7c6879b --- /dev/null +++ b/spec/grape/exceptions/invalid_response_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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 diff --git a/spec/grape/exceptions/invalid_versioner_option_spec.rb b/spec/grape/exceptions/invalid_versioner_option_spec.rb index 5ef5124f6..a17079349 100644 --- a/spec/grape/exceptions/invalid_versioner_option_spec.rb +++ b/spec/grape/exceptions/invalid_versioner_option_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::InvalidVersionerOption do diff --git a/spec/grape/exceptions/missing_mime_type_spec.rb b/spec/grape/exceptions/missing_mime_type_spec.rb index 5ae6970a8..b5545cbcc 100644 --- a/spec/grape/exceptions/missing_mime_type_spec.rb +++ b/spec/grape/exceptions/missing_mime_type_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::MissingMimeType do diff --git a/spec/grape/exceptions/missing_option_spec.rb b/spec/grape/exceptions/missing_option_spec.rb index c8f638a78..1ae125134 100644 --- a/spec/grape/exceptions/missing_option_spec.rb +++ b/spec/grape/exceptions/missing_option_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::MissingOption do diff --git a/spec/grape/exceptions/unknown_options_spec.rb b/spec/grape/exceptions/unknown_options_spec.rb index 4b535cef2..743553719 100644 --- a/spec/grape/exceptions/unknown_options_spec.rb +++ b/spec/grape/exceptions/unknown_options_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::UnknownOptions do diff --git a/spec/grape/exceptions/unknown_validator_spec.rb b/spec/grape/exceptions/unknown_validator_spec.rb index 810f0d84f..2ef256f37 100644 --- a/spec/grape/exceptions/unknown_validator_spec.rb +++ b/spec/grape/exceptions/unknown_validator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::UnknownValidator do diff --git a/spec/grape/exceptions/validation_errors_spec.rb b/spec/grape/exceptions/validation_errors_spec.rb index cbf1a1853..cf7e1e540 100644 --- a/spec/grape/exceptions/validation_errors_spec.rb +++ b/spec/grape/exceptions/validation_errors_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'ostruct' @@ -77,10 +79,12 @@ def app end get '/exactly_one_of', beer: 'string', wine: 'anotherstring' expect(last_response.status).to eq(400) - expect(JSON.parse(last_response.body)).to eq([ - 'params' => %w[beer wine], - 'messages' => ['are mutually exclusive'] - ]) + expect(JSON.parse(last_response.body)).to eq( + [ + 'params' => %w[beer wine], + 'messages' => ['are mutually exclusive'] + ] + ) end end end diff --git a/spec/grape/exceptions/validation_spec.rb b/spec/grape/exceptions/validation_spec.rb index c1b5fba25..5486cbf64 100644 --- a/spec/grape/exceptions/validation_spec.rb +++ b/spec/grape/exceptions/validation_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Exceptions::Validation do it 'fails when params are missing' do - expect { Grape::Exceptions::Validation.new(message: 'presence') }.to raise_error(ArgumentError, 'missing keyword: params') + expect { Grape::Exceptions::Validation.new(message: 'presence') }.to raise_error(ArgumentError, /missing keyword:.+?params/) end context 'when message is a symbol' do it 'stores message_key' do diff --git a/spec/grape/extensions/param_builders/hash_spec.rb b/spec/grape/extensions/param_builders/hash_spec.rb index 27371fd98..2b68397d0 100644 --- a/spec/grape/extensions/param_builders/hash_spec.rb +++ b/spec/grape/extensions/param_builders/hash_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Extensions::Hash::ParamBuilder do diff --git a/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb b/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb index 1c2ea2c4f..4e5b8e6b1 100644 --- a/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +++ b/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do diff --git a/spec/grape/extensions/param_builders/hashie/mash_spec.rb b/spec/grape/extensions/param_builders/hashie/mash_spec.rb index 0a5975f2d..b533f5657 100644 --- a/spec/grape/extensions/param_builders/hashie/mash_spec.rb +++ b/spec/grape/extensions/param_builders/hashie/mash_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Extensions::Hashie::Mash::ParamBuilder do diff --git a/spec/grape/integration/global_namespace_function_spec.rb b/spec/grape/integration/global_namespace_function_spec.rb index 014280cd6..32a55f7de 100644 --- a/spec/grape/integration/global_namespace_function_spec.rb +++ b/spec/grape/integration/global_namespace_function_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # see https://github.com/ruby-grape/grape/issues/1348 require 'spec_helper' diff --git a/spec/grape/integration/rack_sendfile_spec.rb b/spec/grape/integration/rack_sendfile_spec.rb index 3136f7f21..ab7cb1ba0 100644 --- a/spec/grape/integration/rack_sendfile_spec.rb +++ b/spec/grape/integration/rack_sendfile_spec.rb @@ -1,13 +1,19 @@ +# frozen_string_literal: true + require 'spec_helper' describe Rack::Sendfile do subject do - send_file = file_streamer + content_object = file_object app = Class.new(Grape::API) do use Rack::Sendfile format :json get do - file send_file + if content_object.is_a?(String) + sendfile content_object + else + stream content_object + end end end @@ -20,9 +26,9 @@ app.call(env) end - context do - let(:file_streamer) do - double(:file_streamer, to_path: '/accel/mapping/some/path') + context 'when calling sendfile' do + let(:file_object) do + '/accel/mapping/some/path' end it 'contains Sendfile headers' do @@ -31,9 +37,9 @@ end end - context do - let(:file_streamer) do - double(:file_streamer) + context 'when streaming non file content' do + let(:file_object) do + double(:file_object, each: nil) end it 'not contains Sendfile headers' do diff --git a/spec/grape/integration/rack_spec.rb b/spec/grape/integration/rack_spec.rb index e7c9f5665..e1c46762f 100644 --- a/spec/grape/integration/rack_spec.rb +++ b/spec/grape/integration/rack_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Rack do @@ -19,16 +21,32 @@ } env = Rack::MockRequest.env_for('/', options) - unless RUBY_PLATFORM == 'java' - major, minor, patch = Rack.release.split('.').map(&:to_i) - patch ||= 0 # rack <= 1.5.2 does not specify patch version - pending 'Rack 1.5.3 or 1.6.1 required' unless major >= 2 || (major >= 1 && ((minor == 5 && patch >= 3) || (minor >= 6))) - end - - expect(JSON.parse(app.call(env)[2].body.first)['params_keys']).to match_array('test') + expect(JSON.parse(read_chunks(app.call(env)[2]).join)['params_keys']).to match_array('test') ensure input.close input.unlink end end + + context 'when the app is mounted' do + def app + @main_app ||= Class.new(Grape::API) do + get 'ping' + end + end + + let!(:base) do + app_to_mount = app + 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.status).to eq 200 + end + end end diff --git a/spec/grape/loading_spec.rb b/spec/grape/loading_spec.rb index a3ed88759..e28891cce 100644 --- a/spec/grape/loading_spec.rb +++ b/spec/grape/loading_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::API do diff --git a/spec/grape/middleware/auth/base_spec.rb b/spec/grape/middleware/auth/base_spec.rb index 2a2a6ab91..d18433698 100644 --- a/spec/grape/middleware/auth/base_spec.rb +++ b/spec/grape/middleware/auth/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'base64' diff --git a/spec/grape/middleware/auth/dsl_spec.rb b/spec/grape/middleware/auth/dsl_spec.rb index 4f969b44e..338d17c7c 100644 --- a/spec/grape/middleware/auth/dsl_spec.rb +++ b/spec/grape/middleware/auth/dsl_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Auth::DSL do @@ -15,15 +17,15 @@ describe '.auth' do it 'stets auth parameters' do - expect(subject).to receive(:use).with(Grape::Middleware::Auth::Base, settings) + 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).to receive(:use).with(Grape::Middleware::Auth::Base, settings) - expect(subject).to receive(:use).with(Grape::Middleware::Auth::Base, settings.merge(realm: 'super_secret')) + 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 diff --git a/spec/grape/middleware/auth/strategies_spec.rb b/spec/grape/middleware/auth/strategies_spec.rb index 9a43e7bcd..954c99631 100644 --- a/spec/grape/middleware/auth/strategies_spec.rb +++ b/spec/grape/middleware/auth/strategies_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'base64' @@ -34,7 +36,7 @@ def app RSpec::Matchers.define :be_challenge do match do |actual_response| actual_response.status == 401 && - actual_response['WWW-Authenticate'] =~ /^Digest / && + actual_response['WWW-Authenticate'].start_with?('Digest ') && actual_response.body.empty? end end diff --git a/spec/grape/middleware/base_spec.rb b/spec/grape/middleware/base_spec.rb index 8163fd1f1..02a745e11 100644 --- a/spec/grape/middleware/base_spec.rb +++ b/spec/grape/middleware/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Base do @@ -114,6 +116,14 @@ end end + describe '#context' do + subject { Grape::Middleware::Base.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(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true diff --git a/spec/grape/middleware/error_spec.rb b/spec/grape/middleware/error_spec.rb index b71fe0f22..d586b9820 100644 --- a/spec/grape/middleware/error_spec.rb +++ b/spec/grape/middleware/error_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'grape-entity' @@ -28,7 +30,7 @@ def app opts = options Rack::Builder.app do use Spec::Support::EndpointFaker - use Grape::Middleware::Error, opts + use Grape::Middleware::Error, **opts run ErrorSpec::ErrApp end end diff --git a/spec/grape/middleware/exception_spec.rb b/spec/grape/middleware/exception_spec.rb index c11b49649..11aa8e5aa 100644 --- a/spec/grape/middleware/exception_spec.rb +++ b/spec/grape/middleware/exception_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Error do @@ -53,7 +55,7 @@ class CustomError < Grape::Exceptions::Base class CustomErrorApp class << self def call(_env) - raise CustomError, status: 400, message: 'failed validation' + raise CustomError.new(status: 400, message: 'failed validation') end end end @@ -128,7 +130,7 @@ def app subject do Rack::Builder.app do use Spec::Support::EndpointFaker - use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => -> { [200, {}, 'rescued'] } } + use Grape::Middleware::Error, rescue_handlers: { NotImplementedError => -> { Rack::Response.new('rescued', 200, {}) } } run ExceptionSpec::OtherExceptionApp end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index aaa0488ac..9f6333cd0 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Formatter do @@ -41,7 +43,7 @@ def to_json end context 'xml' do - let(:body) { 'string' } + let(:body) { +'string' } it 'calls #to_xml if the content type is xml' do body.instance_eval do def to_xml @@ -194,26 +196,32 @@ def to_xml subject.options[:content_types][:custom] = "don't care" subject.options[:formatters][:custom] = ->(_obj, _env) { 'CUSTOM FORMAT' } _, _, body = subject.call('PATH_INFO' => '/info.custom') - expect(body.body).to eq(['CUSTOM FORMAT']) + expect(read_chunks(body)).to eq(['CUSTOM FORMAT']) end context 'default' do let(:body) { ['blah'] } it 'uses default json formatter' do _, _, body = subject.call('PATH_INFO' => '/info.json') - expect(body.body).to eq(['["blah"]']) + expect(read_chunks(body)).to eq(['["blah"]']) end end it 'uses custom json formatter' do subject.options[:formatters][:json] = ->(_obj, _env) { 'CUSTOM JSON FORMAT' } _, _, body = subject.call('PATH_INFO' => '/info.json') - expect(body.body).to eq(['CUSTOM JSON FORMAT']) + expect(read_chunks(body)).to eq(['CUSTOM JSON FORMAT']) end end context 'no content responses' do let(:no_content_response) { ->(status) { [status, {}, ['']] } } - Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.each do |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) @@ -247,7 +255,7 @@ def to_xml let(:content_type) { 'application/atom+xml' } it 'returns a 415 HTTP error status' do - error = catch(:error) { + error = catch(:error) do subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => method, @@ -255,7 +263,7 @@ def to_xml 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) - } + end expect(error[:status]).to eq(415) expect(error[:message]).to eq("The provided content-type 'application/atom+xml' is not supported.") end @@ -371,12 +379,17 @@ def to_xml end context 'send file' do - let(:body) { Grape::ServeFile::FileResponse.new('file') } - let(:app) { ->(_env) { [200, {}, body] } } + let(:file) { double(File) } + let(:file_body) { Grape::ServeStream::StreamResponse.new(file) } + let(:app) { ->(_env) { [200, {}, file_body] } } - it 'returns Grape::Uril::SendFileReponse' do + it 'returns a file response' do + expect(file).to receive(:each).and_yield('data') env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' } - expect(subject.call(env)).to be_a(Grape::ServeFile::SendfileResponse) + status, headers, body = subject.call(env) + expect(status).to be == 200 + expect(headers).to be == { 'Content-Type' => 'application/json' } + expect(read_chunks(body)).to be == ['data'] end end @@ -389,13 +402,17 @@ def self.call(_, _) let(:app) { ->(_env) { [200, {}, ['']] } } before do Grape::Formatter.register :invalid, InvalidFormatter - Grape::ContentTypes::CONTENT_TYPES[:invalid] = 'application/x-invalid' + Grape::ContentTypes.register :invalid, 'application/x-invalid' + end + after do + Grape::ContentTypes.default_elements.delete(:invalid) + Grape::Formatter.default_elements.delete(:invalid) end it 'returns response by invalid formatter' do env = { 'PATH_INFO' => '/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid' } - _, _, bodies = *subject.call(env) - expect(bodies.body.first).to eq({ message: 'invalid' }.to_json) + _, _, body = *subject.call(env) + expect(read_chunks(body).join).to eq({ message: 'invalid' }.to_json) end end @@ -407,7 +424,7 @@ def self.call(_, _) parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } } ) io = StringIO.new('{invalid}') - error = catch(:error) { + error = catch(:error) do subject.call( 'PATH_INFO' => '/info', 'REQUEST_METHOD' => 'POST', @@ -415,7 +432,7 @@ def self.call(_, _) 'rack.input' => io, 'CONTENT_LENGTH' => io.length ) - } + end expect(error[:message]).to eq 'fail' expect(error[:backtrace].size).to be >= 1 diff --git a/spec/grape/middleware/globals_spec.rb b/spec/grape/middleware/globals_spec.rb index d63c289e3..e50eff171 100644 --- a/spec/grape/middleware/globals_spec.rb +++ b/spec/grape/middleware/globals_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Globals do diff --git a/spec/grape/middleware/stack_spec.rb b/spec/grape/middleware/stack_spec.rb index ff0062fba..b7ac1b149 100644 --- a/spec/grape/middleware/stack_spec.rb +++ b/spec/grape/middleware/stack_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Stack do @@ -6,6 +8,7 @@ class FooMiddleware; end class BarMiddleware; end class BlockMiddleware attr_reader :block + def initialize(&block) @block = block end @@ -109,6 +112,15 @@ def initialize(&block) expect(subject[1]).to eq(StackSpec::BlockMiddleware) expect(subject[2]).to eq(StackSpec::BarMiddleware) end + + context 'middleware spec with proc declaration exists' do + let(:middleware_spec_with_proc) { [:use, StackSpec::FooMiddleware, proc] } + + it 'properly forwards spec arguments' do + expect(subject).to receive(:use).with(StackSpec::FooMiddleware) + subject.merge_with([middleware_spec_with_proc]) + end + end end describe '#build' do diff --git a/spec/grape/middleware/versioner/accept_version_header_spec.rb b/spec/grape/middleware/versioner/accept_version_header_spec.rb index efd95f344..f13d44175 100644 --- a/spec/grape/middleware/versioner/accept_version_header_spec.rb +++ b/spec/grape/middleware/versioner/accept_version_header_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Versioner::AcceptVersionHeader do let(:app) { ->(env) { [200, env, env] } } - subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, @options || {}) } + subject { Grape::Middleware::Versioner::AcceptVersionHeader.new(app, **(@options || {})) } before do @options = { diff --git a/spec/grape/middleware/versioner/header_spec.rb b/spec/grape/middleware/versioner/header_spec.rb index 5befcc6a7..b2349a712 100644 --- a/spec/grape/middleware/versioner/header_spec.rb +++ b/spec/grape/middleware/versioner/header_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Versioner::Header do let(:app) { ->(env) { [200, env, env] } } - subject { Grape::Middleware::Versioner::Header.new(app, @options || {}) } + subject { Grape::Middleware::Versioner::Header.new(app, **(@options || {})) } before do @options = { @@ -160,6 +162,12 @@ 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'] diff --git a/spec/grape/middleware/versioner/param_spec.rb b/spec/grape/middleware/versioner/param_spec.rb index cdf3d23a8..328696128 100644 --- a/spec/grape/middleware/versioner/param_spec.rb +++ b/spec/grape/middleware/versioner/param_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Versioner::Param do let(:app) { ->(env) { [200, env, env['api.version']] } } let(:options) { {} } - subject { Grape::Middleware::Versioner::Param.new(app, options) } + subject { Grape::Middleware::Versioner::Param.new(app, **options) } it 'sets the API version based on the default param (apiver)' do env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' }) diff --git a/spec/grape/middleware/versioner/path_spec.rb b/spec/grape/middleware/versioner/path_spec.rb index c5e9995d8..e703931f3 100644 --- a/spec/grape/middleware/versioner/path_spec.rb +++ b/spec/grape/middleware/versioner/path_spec.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Versioner::Path do let(:app) { ->(env) { [200, env, env['api.version']] } } let(:options) { {} } - subject { Grape::Middleware::Versioner::Path.new(app, options) } + subject { Grape::Middleware::Versioner::Path.new(app, **options) } it 'sets the API version based on the first path' do expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1') diff --git a/spec/grape/middleware/versioner_spec.rb b/spec/grape/middleware/versioner_spec.rb index d3f84cd35..198f15a3a 100644 --- a/spec/grape/middleware/versioner_spec.rb +++ b/spec/grape/middleware/versioner_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Middleware::Versioner do diff --git a/spec/grape/named_api_spec.rb b/spec/grape/named_api_spec.rb new file mode 100644 index 000000000..145066612 --- /dev/null +++ b/spec/grape/named_api_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'A named 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 + + before { stub_const('NamedAPI', api) } + + it 'can access the name of the API' do + expect(api_name).to eq 'NamedAPI' + end +end diff --git a/spec/grape/parser_spec.rb b/spec/grape/parser_spec.rb index f9b6c1d52..ace8954fa 100644 --- a/spec/grape/parser_spec.rb +++ b/spec/grape/parser_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Parser do @@ -15,11 +17,11 @@ describe '.parsers' do it 'returns an instance of Hash' do - expect(subject.parsers({})).to be_an_instance_of(Hash) + expect(subject.parsers(**{})).to be_an_instance_of(Hash) end it 'includes built-in parsers' do - expect(subject.parsers({})).to include(subject.builtin_parsers) + expect(subject.parsers(**{})).to include(subject.builtin_parsers) end context 'with :parsers option' do @@ -33,7 +35,7 @@ let(:added_parser) { Class.new } before { subject.register :added, added_parser } it 'includes added parser' do - expect(subject.parsers({})).to include(added: added_parser) + expect(subject.parsers(**{})).to include(added: added_parser) end end end @@ -42,8 +44,8 @@ let(:options) { {} } it 'calls .parsers' do - expect(subject).to receive(:parsers).with(options).and_return(subject.builtin_parsers) - subject.parser_for(:json, options) + expect(subject).to receive(:parsers).with(any_args).and_return(subject.builtin_parsers) + subject.parser_for(:json, **options) end it 'returns parser correctly' do diff --git a/spec/grape/path_spec.rb b/spec/grape/path_spec.rb index 721b69f24..e43e1df4a 100644 --- a/spec/grape/path_spec.rb +++ b/spec/grape/path_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -85,12 +87,12 @@ module Grape describe '#namespace?' do it 'is false when the namespace is nil' do path = Path.new(anything, nil, anything) - expect(path.namespace?).to be nil + expect(path.namespace?).to be_falsey end it 'is false when the namespace starts with whitespace' do path = Path.new(anything, ' /foo', anything) - expect(path.namespace?).to be nil + expect(path.namespace?).to be_falsey end it 'is false when the namespace is the root path' do @@ -107,12 +109,12 @@ module Grape describe '#path?' do it 'is false when the path is nil' do path = Path.new(nil, anything, anything) - expect(path.path?).to be nil + expect(path.path?).to be_falsey end it 'is false when the path starts with whitespace' do path = Path.new(' /foo', anything, anything) - expect(path.path?).to be nil + expect(path.path?).to be_falsey end it 'is false when the path is the root path' do diff --git a/spec/grape/presenters/presenter_spec.rb b/spec/grape/presenters/presenter_spec.rb index 71bb6533c..4e73e8e5b 100644 --- a/spec/grape/presenters/presenter_spec.rb +++ b/spec/grape/presenters/presenter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape diff --git a/spec/grape/request_spec.rb b/spec/grape/request_spec.rb index b763532ca..1bfce035e 100644 --- a/spec/grape/request_spec.rb +++ b/spec/grape/request_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape @@ -62,6 +64,30 @@ module Grape end end + describe 'when the param_builder is set to Hashie' do + before do + Grape.configure do |config| + config.param_builder = Grape::Extensions::Hashie::Mash::ParamBuilder + end + end + + after do + Grape.config.reset + end + + subject(:request_params) { Grape::Request.new(env, **opts).params } + + context 'when the API does not include a specific param builder' do + let(:opts) { {} } + it { is_expected.to be_a(Hashie::Mash) } + end + + context 'when the API includes a specific param builder' do + let(:opts) { { build_params_with: Grape::Extensions::Hash::ParamBuilder } } + it { is_expected.to be_a(Hash) } + end + end + describe '#headers' do let(:options) do default_options.merge(request_headers) diff --git a/spec/grape/util/inheritable_setting_spec.rb b/spec/grape/util/inheritable_setting_spec.rb index 866a7bbd3..0e0b672e7 100644 --- a/spec/grape/util/inheritable_setting_spec.rb +++ b/spec/grape/util/inheritable_setting_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape module Util diff --git a/spec/grape/util/inheritable_values_spec.rb b/spec/grape/util/inheritable_values_spec.rb index af2eb9d5a..a5003d875 100644 --- a/spec/grape/util/inheritable_values_spec.rb +++ b/spec/grape/util/inheritable_values_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape module Util diff --git a/spec/grape/util/reverse_stackable_values_spec.rb b/spec/grape/util/reverse_stackable_values_spec.rb index 6c2d6b543..c5ffbd293 100644 --- a/spec/grape/util/reverse_stackable_values_spec.rb +++ b/spec/grape/util/reverse_stackable_values_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape module Util diff --git a/spec/grape/util/stackable_values_spec.rb b/spec/grape/util/stackable_values_spec.rb index 82a327a56..c7defdf75 100644 --- a/spec/grape/util/stackable_values_spec.rb +++ b/spec/grape/util/stackable_values_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape module Util @@ -6,7 +8,7 @@ module Util subject { StackableValues.new(parent) } describe '#keys' do - it 'returns all key' 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 diff --git a/spec/grape/util/strict_hash_configuration_spec.rb b/spec/grape/util/strict_hash_configuration_spec.rb index 8798d095f..c55fd616b 100644 --- a/spec/grape/util/strict_hash_configuration_spec.rb +++ b/spec/grape/util/strict_hash_configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' module Grape module Util diff --git a/spec/grape/validations/attributes_iterator_spec.rb b/spec/grape/validations/attributes_iterator_spec.rb index 7a8e4ec31..594c8ca19 100644 --- a/spec/grape/validations/attributes_iterator_spec.rb +++ b/spec/grape/validations/attributes_iterator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::AttributesIterator do diff --git a/spec/grape/validations/instance_behaivour_spec.rb b/spec/grape/validations/instance_behaivour_spec.rb index 1c823b5ac..9f2038dce 100644 --- a/spec/grape/validations/instance_behaivour_spec.rb +++ b/spec/grape/validations/instance_behaivour_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'Validator with instance variables' do let(:validator_type) do Class.new(Grape::Validations::Base) do def validate_param!(_attr_name, _params) - if @instance_variable - raise Grape::Exceptions::Validation, params: ['params'], - message: 'This should never happen' + if instance_variable_defined?(:@instance_variable) && @instance_variable + raise Grape::Exceptions::Validation.new(params: ['params'], + message: 'This should never happen') end @instance_variable = true end diff --git a/spec/grape/validations/multiple_attributes_iterator_spec.rb b/spec/grape/validations/multiple_attributes_iterator_spec.rb new file mode 100644 index 000000000..1508a76ed --- /dev/null +++ b/spec/grape/validations/multiple_attributes_iterator_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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 and the skipped flag without the list of attrs' do + expect { |b| iterator.each(&b) }.to yield_with_args(params, false) + 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], false], [params[1], false]) + end + end + + context 'when params is empty optional placeholder' do + let(:params) do + [Grape::DSL::Parameters::EmptyOptionalValue, { 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([Grape::DSL::Parameters::EmptyOptionalValue, true], [params[1], false]) + end + end + end +end diff --git a/spec/grape/validations/params_scope_spec.rb b/spec/grape/validations/params_scope_spec.rb index fff6f5df3..9ef14af9f 100644 --- a/spec/grape/validations/params_scope_spec.rb +++ b/spec/grape/validations/params_scope_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::ParamsScope do @@ -30,7 +32,7 @@ def app context 'when the default value is false' do before do subject.params do - optional :bool, type: Virtus::Attribute::Boolean, default: false + optional :bool, type: Grape::API::Boolean, default: false end subject.get end @@ -121,14 +123,14 @@ def initialize(value) end end - context 'param alias' do + context 'param renaming' do it do subject.params do requires :foo, as: :bar optional :super, as: :hiper end - subject.get('/alias') { "#{declared(params)['bar']}-#{declared(params)['hiper']}" } - get '/alias', foo: 'any', super: 'any2' + 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') @@ -138,8 +140,8 @@ def initialize(value) subject.params do requires :foo, as: :bar, type: String, coerce_with: ->(c) { c.strip } end - subject.get('/alias-coerced') { "#{params['bar']}-#{params['foo']}" } - get '/alias-coerced', foo: ' there we go ' + 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-') @@ -149,12 +151,35 @@ def initialize(value) subject.params do requires :foo, as: :bar, allow_blank: false end - subject.get('/alias-not-blank') {} - get '/alias-not-blank', foo: '' + 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 end context 'array without coerce type explicitly given' do @@ -479,7 +504,50 @@ def initialize(value) end.to_not raise_error end - it 'allows aliasing of dependent parameters' do + 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.to_not 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 @@ -497,6 +565,34 @@ def initialize(value) expect(body.keys).to_not include('b') end + it 'allows renaming of dependent on parameter' do + subject.params do + optional :a, as: :b + given b: ->(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 'raises an error 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.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] @@ -537,6 +633,32 @@ def initialize(value) 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 + it 'includes the parameter within #declared(params)' do get '/test', a: true, b: true @@ -592,7 +714,53 @@ def initialize(value) 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 'should pass 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 @@ -948,4 +1116,40 @@ def initialize(value) 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 diff --git a/spec/grape/validations/single_attribute_iterator_spec.rb b/spec/grape/validations/single_attribute_iterator_spec.rb new file mode 100644 index 000000000..31a51dc47 --- /dev/null +++ b/spec/grape/validations/single_attribute_iterator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Validations::SingleAttributeIterator 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]) } + + 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, false], [params, :second, false, 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, false], [params[0], :second, false, false], + [params[1], :first, false, false], [params[1], :second, false, 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, false], [params[0], :second, true, false], + [params[1], :first, true, false], [params[1], :second, true, false], + [params[2], :first, false, false], [params[2], :second, false, false] + ) + end + end + + context 'when missing optional value' do + let(:params) { [Grape::DSL::Parameters::EmptyOptionalValue, 10] } + + it 'marks params with skipped values' do + expect { |b| iterator.each(&b) }.to yield_successive_args( + [params[0], :first, false, true], [params[0], :second, false, true], + [params[1], :first, false, false], [params[1], :second, false, false], + ) + end + end + end + end +end diff --git a/spec/grape/validations/types/array_coercer_spec.rb b/spec/grape/validations/types/array_coercer_spec.rb new file mode 100644 index 000000000..f2bfb6c6d --- /dev/null +++ b/spec/grape/validations/types/array_coercer_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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 diff --git a/spec/grape/validations/types/primitive_coercer_spec.rb b/spec/grape/validations/types/primitive_coercer_spec.rb new file mode 100644 index 000000000..df375ac37 --- /dev/null +++ b/spec/grape/validations/types/primitive_coercer_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Validations::Types::PrimitiveCoercer do + let(:strict) { false } + + subject { described_class.new(type, strict) } + + 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 eq(true) + end + end + + [false, 'false', 0].each do |val| + it "coerces '#{val}' to false" do + expect(subject.call(val)).to eq(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 + end + + context 'Numeric' do + let(:type) { Numeric } + + it 'coerces an empty string to nil' do + expect(subject.call('')).to be_nil + 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 '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 eq(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 diff --git a/spec/grape/validations/types/set_coercer_spec.rb b/spec/grape/validations/types/set_coercer_spec.rb new file mode 100644 index 000000000..d78f5f511 --- /dev/null +++ b/spec/grape/validations/types/set_coercer_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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 diff --git a/spec/grape/validations/types_spec.rb b/spec/grape/validations/types_spec.rb index 353d75fda..1bee89e77 100644 --- a/spec/grape/validations/types_spec.rb +++ b/spec/grape/validations/types_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::Types do @@ -11,30 +13,11 @@ def self.parse; end end end - VirtusA = Virtus::Attribute.build(String) - - module VirtusModule - include Virtus.module - end - - class VirtusB - include VirtusModule - end - - class VirtusC - include Virtus.model - end - - MyAxiom = Axiom::Types::String.new do - minimum_length 1 - maximum_length 30 - end - describe '::primitive?' do [ Integer, Float, Numeric, BigDecimal, - Virtus::Attribute::Boolean, String, Symbol, - Date, DateTime, Time, Rack::Multipart::UploadedFile + Grape::API::Boolean, String, Symbol, + Date, DateTime, Time ].each do |type| it "recognizes #{type} as a primitive" do expect(described_class.primitive?(type)).to be_truthy @@ -57,16 +40,6 @@ class VirtusC end end - describe '::recognized?' do - [ - VirtusA, VirtusB, VirtusC, MyAxiom - ].each do |type| - it "recognizes #{type}" do - expect(described_class.recognized?(type)).to be_truthy - end - end - end - describe '::special?' do [ JSON, Array[JSON], File, Rack::Multipart::UploadedFile @@ -97,14 +70,14 @@ class VirtusC expect(described_class.instance_variable_get(:@__cache_write_lock)).to be_a(Mutex) end - it 'caches the result of the Virtus::Attribute.build method' do + it 'caches the result of the build_coercer method' do original_cache = described_class.instance_variable_get(:@__cache) described_class.instance_variable_set(:@__cache, {}) - coercer = 'TestCoercer' - expect(Virtus::Attribute).to receive(:build).once.and_return(coercer) - expect(described_class.build_coercer(Array[String])).to eq(coercer) - expect(described_class.build_coercer(Array[String])).to eq(coercer) + 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) described_class.instance_variable_set(:@__cache, original_cache) end diff --git a/spec/grape/validations/validators/all_or_none_spec.rb b/spec/grape/validations/validators/all_or_none_spec.rb index 98d82ce8f..ce4124946 100644 --- a/spec/grape/validations/validators/all_or_none_spec.rb +++ b/spec/grape/validations/validators/all_or_none_spec.rb @@ -1,59 +1,169 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::AllOrNoneOfValidator do describe '#validate!' do - let(:scope) do - Struct.new(:opts) do - def params(arg) - arg - end + subject(:validate) { post path, params } + + module ValidationsSpec + module AllOrNoneOfValidatorSpec + class API < Grape::API + rescue_from Grape::Exceptions::ValidationErrors do |e| + error!(e.errors.transform_keys! { |key| key.join(',') }, 400) + end + + params do + optional :beer, :wine, type: Boolean + all_or_none_of :beer, :wine + end + post do + end + + params do + optional :beer, :wine, :other, type: Boolean + all_or_none_of :beer, :wine + end + post 'mixed-params' do + end + + params do + optional :beer, :wine, type: Boolean + all_or_none_of :beer, :wine, message: 'choose all or none' + end + post '/custom-message' do + end + + params do + requires :item, type: Hash do + optional :beer, :wine, type: Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-hash' do + end - def required?; end + params do + requires :items, type: Array do + optional :beer, :wine, type: Boolean + all_or_none_of :beer, :wine + end + end + post '/nested-array' do + end + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, type: Boolean + all_or_none_of :beer, :wine + end + end + end + post '/deeply-nested-array' do + end + end end end - let(:all_or_none_params) { %i[beer wine grapefruit] } - let(:validator) { described_class.new(all_or_none_params, {}, false, scope.new) } + + def app + ValidationsSpec::AllOrNoneOfValidatorSpec::API + end context 'when all restricted params are present' do - let(:params) { { beer: true, wine: true, grapefruit: true } } + let(:path) { '/' } + let(:params) { { beer: true, wine: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end context 'mixed with other params' do - let(:mixed_params) { params.merge!(other: true, andanother: true) } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, other: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(mixed_params)).to eql mixed_params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end end - context 'when none of the restricted params is selected' do + 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 custom message is specified' do + 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 + + context 'when no restricted params are present' do + let(:path) { '/' } let(:params) { { somethingelse: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - context 'when only a subset of restricted params are present' do - let(:params) { { beer: true, grapefruit: true } } + context 'when restricted params are nested inside required hash' do + let(:path) { '/nested-hash' } + let(:params) { { item: { beer: true } } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 - context 'mixed with other params' do - let(:mixed_params) { params.merge!(other: true, andanother: true) } + end - it 'raise a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) - end + context 'when mutually exclusive params are nested inside array' do + 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 + + context 'when mutually exclusive params are deeply nested' do + 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 diff --git a/spec/grape/validations/validators/allow_blank_spec.rb b/spec/grape/validations/validators/allow_blank_spec.rb index 643ba04be..cb74e332c 100644 --- a/spec/grape/validations/validators/allow_blank_spec.rb +++ b/spec/grape/validations/validators/allow_blank_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::AllowBlankValidator do diff --git a/spec/grape/validations/validators/at_least_one_of_spec.rb b/spec/grape/validations/validators/at_least_one_of_spec.rb index 7a331fe9b..b6468e3e5 100644 --- a/spec/grape/validations/validators/at_least_one_of_spec.rb +++ b/spec/grape/validations/validators/at_least_one_of_spec.rb @@ -1,66 +1,212 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::AtLeastOneOfValidator do describe '#validate!' do - let(:scope) do - Struct.new(:opts) do - def params(arg) - arg - end + subject(:validate) { post path, params } + + module ValidationsSpec + module AtLeastOneOfValidatorSpec + class API < Grape::API + 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 + + params do + optional :beer, :wine, :grapefruit, :other + at_least_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + 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 - def required?; 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 + + 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 + + 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 end - let(:at_least_one_of_params) { %i[beer wine grapefruit] } - let(:validator) { described_class.new(at_least_one_of_params, {}, false, scope.new) } + + def app + ValidationsSpec::AtLeastOneOfValidatorSpec::API + end context 'when all restricted params are present' do + let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end context 'mixed with other params' do - let(:mixed_params) { params.merge!(other: true, andanother: true) } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(mixed_params)).to eql mixed_params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end end context 'when a subset of restricted params are present' do + let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 end end - context 'when params keys come as strings' do - let(:params) { { 'beer' => true, 'grapefruit' => true } } + context 'when none of the restricted params is selected' do + let(:path) { '/' } + let(:params) { { other: true } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + 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 none of the restricted params is selected' do - let(:params) { { somethingelse: true } } + context 'when custom message is specified' do + let(:path) { '/custom-message' } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 end context 'when exactly one of the restricted params is selected' do - let(:params) { { beer: true, somethingelse: true } } + let(:path) { '/' } + let(:params) { { beer: true } } + + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end + end + + context 'when restricted params are nested inside hash' do + 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 - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + 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 + + context 'when restricted params are nested inside array' do + 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 + + context 'when restricted params are deeply nested' do + 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 diff --git a/spec/grape/validations/validators/coerce_spec.rb b/spec/grape/validations/validators/coerce_spec.rb index 34406c389..3f8046f8a 100644 --- a/spec/grape/validations/validators/coerce_spec.rb +++ b/spec/grape/validations/validators/coerce_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::CoerceValidator do @@ -10,20 +12,25 @@ def app end describe 'coerce' do - module CoerceValidatorSpec - class User - include Virtus.model - attribute :id, Integer - attribute :name, String + class SecureURIOnly + def self.parse(value) + URI.parse(value) + end + + def self.parsed?(value) + value.is_a? URI::HTTPS end end context 'i18n' do after :each do + I18n.available_locales = %i[en] I18n.locale = :en + I18n.default_locale = :en end it 'i18n error on malformed input' do + I18n.available_locales = %i[en zh-CN] I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__) I18n.reload! I18n.locale = 'zh-CN'.to_sym @@ -40,6 +47,7 @@ class User end it 'gives an english fallback error when default locale message is blank' do + I18n.available_locales = %i[en pt-BR] I18n.locale = 'pt-BR'.to_sym subject.params do requires :age, type: Integer @@ -92,6 +100,7 @@ class User it 'respects :coerce_with' do get '/', a: 'yup' + expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') end @@ -144,26 +153,50 @@ class User expect(last_response.body).to eq('array int works') end - context 'complex objects' do - it 'error on malformed input for complex objects' do - subject.params do - requires :user, type: CoerceValidatorSpec::User + 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.status).to eq(201) + expect(last_response.body).to eq('BigDecimal 45.1') end - subject.get '/user' do - 'complex works' + + it 'Boolean' do + subject.params do + requires :boolean, type: Boolean + end + subject.post '/boolean' do + params[:boolean] + end + + post '/boolean', { boolean: 'true' }.to_json, headers + expect(last_response.status).to eq(201) + expect(last_response.body).to eq('true') end + end - get '/user', user: '32' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('user is invalid') + it 'BigDecimal' do + subject.params do + requires :bigdecimal, coerce: BigDecimal + end + subject.get '/bigdecimal' do + params[:bigdecimal].class + end - get '/user', user: { id: 32, name: 'Bob' } + get '/bigdecimal', bigdecimal: '45' expect(last_response.status).to eq(200) - expect(last_response.body).to eq('complex works') + expect(last_response.body).to eq('BigDecimal') end - end - context 'coerces' do it 'Integer' do subject.params do requires :int, coerce: Integer @@ -177,6 +210,70 @@ class User 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.status).to eq(200) + expect(last_response.body).to eq('String') + + get '/string', string: nil + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('NilClass') + end + + context 'a custom type' do + it 'coerces the given value' do + subject.params do + requires :uri, coerce: SecureURIOnly + end + subject.get '/secure_uri' do + params[:uri].class + end + + get 'secure_uri', uri: 'https://www.example.com' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('URI::HTTPS') + + get 'secure_uri', uri: 'http://www.example.com' + + expect(last_response.status).to eq(400) + 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::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.status).to eq(400) + expect(last_response.body).to eq('name must be unique') + end + end + end + context 'Array' do it 'Array of Integers' do subject.params do @@ -193,7 +290,7 @@ class User it 'Array of Bools' do subject.params do - requires :arry, coerce: Array[Virtus::Attribute::Boolean] + requires :arry, coerce: Array[Grape::API::Boolean] end subject.get '/array' do params[:arry][0].class @@ -204,27 +301,6 @@ class User expect(last_response.body).to eq('TrueClass') end - it 'Array of Complex' do - subject.params do - requires :arry, coerce: Array[CoerceValidatorSpec::User] - end - subject.get '/array' do - params[:arry].size - end - - get 'array', arry: [31] - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('arry is invalid') - - get 'array', arry: { id: 31, name: 'Alice' } - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('arry is invalid') - - get 'array', arry: [{ id: 31, name: 'Alice' }] - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('1') - end - it 'Array of type implementing parse' do subject.params do requires :uri, type: Array[URI] @@ -249,17 +325,7 @@ class User expect(last_response.body).to eq('Set,URI::HTTP,1') end - it 'Array of class implementing parse and parsed?' do - class SecureURIOnly - def self.parse(value) - URI.parse(value) - end - - def self.parsed?(value) - value.is_a? URI::HTTPS - end - end - + it 'Array of a custom type' do subject.params do requires :uri, type: Array[SecureURIOnly] end @@ -291,7 +357,7 @@ def self.parsed?(value) it 'Set of Bools' do subject.params do - requires :set, coerce: Set[Virtus::Attribute::Boolean] + requires :set, coerce: Set[Grape::API::Boolean] end subject.get '/set' do params[:set].first.class @@ -303,100 +369,73 @@ def self.parsed?(value) end end - it 'Bool' do - subject.params do - requires :bool, coerce: Virtus::Attribute::Boolean - end - subject.get '/bool' do - params[:bool].class - end - - get '/bool', bool: 1 - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('TrueClass') - - get '/bool', bool: 0 - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('FalseClass') - - get '/bool', bool: 'false' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('FalseClass') - - get '/bool', bool: 'true' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('TrueClass') - end - it 'Boolean' do subject.params do - optional :boolean, type: Boolean, default: true + requires :boolean, type: Boolean end subject.get '/boolean' do params[:boolean].class end - get '/boolean' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('TrueClass') - - get '/boolean', boolean: true - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('TrueClass') - - get '/boolean', boolean: false - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('FalseClass') - - get '/boolean', boolean: 'true' + get '/boolean', boolean: 1 expect(last_response.status).to eq(200) expect(last_response.body).to eq('TrueClass') + end - get '/boolean', boolean: 'false' - expect(last_response.status).to eq(200) - expect(last_response.body).to eq('FalseClass') + context 'File' do + let(:file) { Rack::Test::UploadedFile.new(__FILE__) } + let(:filename) { File.basename(__FILE__).to_s } - get '/boolean', boolean: 123 - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('boolean is invalid') + it 'Rack::Multipart::UploadedFile' do + subject.params do + requires :file, type: Rack::Multipart::UploadedFile + end + subject.post '/upload' do + params[:file][:filename] + end - get '/boolean', boolean: '123' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('boolean is invalid') - end + post '/upload', file: file + expect(last_response.status).to eq(201) + expect(last_response.body).to eq(filename) - it 'Rack::Multipart::UploadedFile' do - subject.params do - requires :file, type: Rack::Multipart::UploadedFile - end - subject.post '/upload' do - params[:file][:filename] + post '/upload', file: 'not a file' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('file is invalid') end - post '/upload', file: Rack::Test::UploadedFile.new(__FILE__) - expect(last_response.status).to eq(201) - expect(last_response.body).to eq(File.basename(__FILE__).to_s) + it 'File' do + subject.params do + requires :file, coerce: File + end + subject.post '/upload' do + params[:file][:filename] + end - post '/upload', file: 'not a file' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('file is invalid') - end + post '/upload', file: file + expect(last_response.status).to eq(201) + expect(last_response.body).to eq(filename) - it 'File' do - subject.params do - requires :file, coerce: File - end - subject.post '/upload' do - params[:file][:filename] + post '/upload', file: 'not a file' + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('file is invalid') + + post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' } + expect(last_response.status).to eq(400) + expect(last_response.body).to eq('file is invalid') end - post '/upload', file: Rack::Test::UploadedFile.new(__FILE__) - expect(last_response.status).to eq(201) - expect(last_response.body).to eq(File.basename(__FILE__).to_s) + it 'collection' do + subject.params do + requires :files, type: Array[File] + end + subject.post '/upload' do + params[:files].first[:filename] + end - post '/upload', file: 'not a file' - expect(last_response.status).to eq(400) - expect(last_response.body).to eq('file is invalid') + post '/upload', files: [file] + expect(last_response.status).to eq(201) + expect(last_response.body).to eq(filename) + end end it 'Nests integers' do @@ -413,6 +452,165 @@ def self.parsed?(value) expect(last_response.status).to eq(200) 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + 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.status).to eq(200) + expect(last_response.body).to eq('NilClass') + end + end + end + end + end end context 'using coerce_with' do @@ -435,19 +633,43 @@ def self.parsed?(value) it 'parses parameters with Array[String] type' do subject.params do - requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) } + requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) } end - subject.get '/ints' do + subject.get '/strings' do params[:values] end - get '/ints', values: '1 2 3 4' + get '/strings', values: '1 2 3 4' expect(last_response.status).to eq(200) expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4]) - get '/ints', values: 'a b c d' + get '/strings', values: 'a b c d' expect(last_response.status).to eq(200) - expect(JSON.parse(last_response.body)).to eq(%w[0 0 0 0]) + 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.status).to eq(201) + 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.status).to eq(201) + 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.status).to eq(201) + 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.status).to eq(400) end it 'parses parameters with Array[Integer] type' do @@ -484,6 +706,44 @@ def self.parsed?(value) 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.status).to eq(200) + expect(last_response.body).to eq('Array') + end + + it 'not coerce missing field' do + get '/' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('NilClass') + end + + it 'coerce array as array' do + get '/', arr: [] + + expect(last_response.status).to eq(200) + 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 @@ -532,6 +792,84 @@ def self.parsed?(value) 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.status).to eq(200) + expect(last_response.body).to eq('Integer') + end + + it 'not coerce missing field' do + get '/' + + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('NilClass') + end + + it 'coerce integer as integer' do + get '/', int: 1 + + expect(last_response.status).to eq(200) + 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| + if val == '0' + nil + elsif val.match?(/^-?\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.status).to eq(200) + expect(last_response.body).to eq('NilClass') + end + + it 'coerces to Integer' do + get '/', int: '1' + + expect(last_response.status).to eq(200) + 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.status).to eq(400) + 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 @@ -572,7 +910,7 @@ def self.parsed?(value) expect(last_response.status).to eq(200) expect(last_response.body).to eq('arrays work') - get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }] + get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }] expect(last_response.status).to eq(200) expect(last_response.body).to eq('arrays work') @@ -588,7 +926,7 @@ def self.parsed?(value) expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') - get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }] + get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }] expect(last_response.status).to eq(400) expect(last_response.body).to eq('splines[x] does not have a valid value') end @@ -906,14 +1244,17 @@ def self.parsed?(value) end context 'converter' do - it 'does not build Virtus::Attribute multiple times' do + it 'does not build a coercer multiple times' do subject.params do requires :something, type: Array[String] end subject.get do end - expect(Virtus::Attribute).to receive(:build).at_most(2).times.and_call_original + expect(Grape::Validations::Types::ArrayCoercer).to( + receive(:new).at_most(:once).and_call_original + ) + 10.times { get '/' } end end diff --git a/spec/grape/validations/validators/default_spec.rb b/spec/grape/validations/validators/default_spec.rb index 3b6dea50e..ae16445eb 100644 --- a/spec/grape/validations/validators/default_spec.rb +++ b/spec/grape/validations/validators/default_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::DefaultValidator do @@ -296,4 +298,174 @@ def app 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 diff --git a/spec/grape/validations/validators/exactly_one_of_spec.rb b/spec/grape/validations/validators/exactly_one_of_spec.rb index 8bdb23073..87eba59d3 100644 --- a/spec/grape/validations/validators/exactly_one_of_spec.rb +++ b/spec/grape/validations/validators/exactly_one_of_spec.rb @@ -1,74 +1,240 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::ExactlyOneOfValidator do describe '#validate!' do - let(:scope) do - Struct.new(:opts) do - def params(arg) - arg - end + subject(:validate) { post path, params } + + module ValidationsSpec + module ExactlyOneOfValidatorSpec + class API < Grape::API + 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 + + params do + optional :beer + optional :wine + optional :grapefruit + optional :other + exactly_one_of :beer, :wine, :grapefruit + end + post 'mixed-params' do + 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 + + 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 + + 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 + + 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 - def required?; end + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Boolean + exactly_one_of :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end + end end end - let(:exactly_one_of_params) { %i[beer wine grapefruit] } - let(:validator) { described_class.new(exactly_one_of_params, {}, false, scope.new) } - context 'when all restricted params are present' do + def app + ValidationsSpec::ExactlyOneOfValidatorSpec::API + end + + context 'when all params are present' do + let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 context 'mixed with other params' do - let(:mixed_params) { params.merge!(other: true, andanother: true) } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } - it 'still raises a validation exception' do - expect do - validator.validate! mixed_params - end.to raise_error(Grape::Exceptions::Validation) + 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 end - context 'when a subset of restricted params are present' do + context 'when a subset of params are present' do + let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 params keys come as strings' do - let(:params) { { 'beer' => true, 'grapefruit' => true } } + context 'when custom message is specified' do + let(:path) { '/custom-message' } + let(:params) { { beer: true, wine: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 - context 'when none of the restricted params is selected' do + 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 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 - context 'when exactly one of the restricted params is selected' do - let(:params) { { beer: true, somethingelse: true } } + context 'when params are nested inside required hash' do + 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 + + context 'when params are nested inside optional hash' do + 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 + + context 'when params are nested inside array' do + 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 + + context 'when params are deeply nested' do + let(:path) { '/deeply-nested-array' } + let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } } - it 'does not raise a validation exception' do - expect(validator.validate!(params)).to eql params + 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 diff --git a/spec/grape/validations/validators/except_values_spec.rb b/spec/grape/validations/validators/except_values_spec.rb index 7030774cc..4757cff8a 100644 --- a/spec/grape/validations/validators/except_values_spec.rb +++ b/spec/grape/validations/validators/except_values_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::ExceptValuesValidator do @@ -6,6 +8,7 @@ class ExceptValuesModel DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze class << self attr_accessor :excepts + def excepts @excepts ||= [] [DEFAULT_EXCEPTS + @excepts].flatten.uniq @@ -110,7 +113,7 @@ def excepts 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 has a value not allowed' }.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 }, diff --git a/spec/grape/validations/validators/mutual_exclusion_spec.rb b/spec/grape/validations/validators/mutual_exclusion_spec.rb index 9fa6375b5..ac1b46989 100644 --- a/spec/grape/validations/validators/mutual_exclusion_spec.rb +++ b/spec/grape/validations/validators/mutual_exclusion_spec.rb @@ -1,62 +1,221 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::MutualExclusionValidator do describe '#validate!' do - let(:scope) do - Struct.new(:opts) do - def params(arg) - arg + subject(:validate) { post path, params } + + module ValidationsSpec + module MutualExclusionValidatorSpec + class API < Grape::API + 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 + + params do + optional :beer + optional :wine + optional :grapefruit + optional :other + mutually_exclusive :beer, :wine, :grapefruit + end + post 'mixed-params' do + 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 + + 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 + + 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 + + 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 + + params do + requires :items, type: Array do + requires :nested_items, type: Array do + optional :beer, :wine, :grapefruit, type: Boolean + mutually_exclusive :beer, :wine, :grapefruit + end + end + end + post '/deeply-nested-array' do + end end end end - let(:mutually_exclusive_params) { %i[beer wine grapefruit] } - let(:validator) { described_class.new(mutually_exclusive_params, {}, false, scope.new) } + + def app + ValidationsSpec::MutualExclusionValidatorSpec::API + end context 'when all mutually exclusive params are present' do + let(:path) { '/' } let(:params) { { beer: true, wine: true, grapefruit: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 context 'mixed with other params' do - let(:mixed_params) { params.merge!(other: true, andanother: true) } + let(:path) { '/mixed-params' } + let(:params) { { beer: true, wine: true, grapefruit: true, other: true } } - it 'still raises a validation exception' do - expect do - validator.validate! mixed_params - end.to raise_error(Grape::Exceptions::Validation) + 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 end context 'when a subset of mutually exclusive params are present' do + let(:path) { '/' } let(:params) { { beer: true, grapefruit: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 params keys come as strings' do - let(:params) { { 'beer' => true, 'grapefruit' => true } } + context 'when custom message is specified' do + let(:path) { '/custom-message' } + let(:params) { { beer: true, wine: true } } - it 'raises a validation exception' do - expect do - validator.validate! params - end.to raise_error(Grape::Exceptions::Validation) + 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 context 'when no mutually exclusive params are present' do + let(:path) { '/' } let(:params) { { beer: true, somethingelse: true } } - it 'params' do - expect(validator.validate!(params)).to eql params + it 'does not return a validation error' do + validate + expect(last_response.status).to eq 201 + end + end + + context 'when mutually exclusive params are nested inside required hash' do + 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 + + context 'when mutually exclusive params are nested inside optional hash' do + 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 + + context 'when mutually exclusive params are nested inside array' do + 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 + + context 'when mutually exclusive params are deeply nested' do + 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 diff --git a/spec/grape/validations/validators/presence_spec.rb b/spec/grape/validations/validators/presence_spec.rb index d00163ead..5eaabe72a 100644 --- a/spec/grape/validations/validators/presence_spec.rb +++ b/spec/grape/validations/validators/presence_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::PresenceValidator do @@ -269,4 +271,32 @@ def app 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 + class CustomType + def self.parse(value) + return if value.blank? + + new + end + end + + subject.params do + requires :custom, type: CustomType + 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 diff --git a/spec/grape/validations/validators/regexp_spec.rb b/spec/grape/validations/validators/regexp_spec.rb index cc2970a88..b4ffc99df 100644 --- a/spec/grape/validations/validators/regexp_spec.rb +++ b/spec/grape/validations/validators/regexp_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::RegexpValidator do diff --git a/spec/grape/validations/validators/same_as_spec.rb b/spec/grape/validations/validators/same_as_spec.rb new file mode 100644 index 000000000..da4c945c0 --- /dev/null +++ b/spec/grape/validations/validators/same_as_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grape::Validations::SameAsValidator do + module ValidationsSpec + module SameAsValidatorSpec + class API < Grape::API + params do + requires :password + requires :password_confirmation, same_as: :password + end + post do + end + + params do + requires :password + requires :password_confirmation, same_as: { value: :password, message: 'not match' } + end + post '/custom-message' do + end + end + end + end + + def app + ValidationsSpec::SameAsValidatorSpec::API + end + + describe '/' do + 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 + 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 diff --git a/spec/grape/validations/validators/values_spec.rb b/spec/grape/validations/validators/values_spec.rb index 2317cf555..491b32834 100644 --- a/spec/grape/validations/validators/values_spec.rb +++ b/spec/grape/validations/validators/values_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations::ValuesValidator do @@ -222,6 +224,11 @@ class API < Grape::API requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' } end get '/proc/message' + + params do + optional :name, type: String, values: %w[a b], allow_blank: true + end + get '/allow_blank' end end end @@ -312,7 +319,7 @@ def app expect(last_response.status).to eq 200 end - it 'allows for an optional param with a list of values' do + it 'accepts 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 @@ -431,11 +438,21 @@ def app end.to raise_error Grape::Exceptions::IncompatibleOptionValues end - it 'allows values to be true or false when setting the type to boolean' do - get('/values/optional_boolean', type: true) - expect(last_response.status).to eq 200 - expect(last_response.body).to eq({ type: true }.to_json) + context 'boolean values' do + 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 + 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 @@ -462,6 +479,14 @@ def app end.to raise_error Grape::Exceptions::IncompatibleOptionValues 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 + context 'with a lambda values' do subject do Class.new(Grape::API) do diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb index a5b79be9e..d325634f6 100644 --- a/spec/grape/validations_spec.rb +++ b/spec/grape/validations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Validations do @@ -7,16 +9,23 @@ def app subject end + def declared_params + subject.namespace_stackable(:declared_params).flatten + end + describe 'params' do context 'optional' do - it 'validates when params is present' 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') @@ -27,14 +36,7 @@ def app end it "doesn't validate when param not present" do - subject.params do - optional :a_number, regexp: /^[0-9]+$/ - end - subject.get '/optional' do - 'optional works!' - end - - get '/optional' + get '/optional', a_number: nil, attachment: nil expect(last_response.status).to eq(200) expect(last_response.body).to eq('optional works!') end @@ -43,7 +45,7 @@ def app subject.params do optional :some_param end - expect(subject.route_setting(:declared_params)).to eq([:some_param]) + expect(declared_params).to eq([:some_param]) end end @@ -63,7 +65,7 @@ def define_optional_using it 'adds entity documentation to declared params' do define_optional_using - expect(subject.route_setting(:declared_params)).to eq(%i[field_a field_b]) + expect(declared_params).to eq(%i[field_a field_b]) end it 'works when field_a and field_b are not present' do @@ -110,7 +112,7 @@ def define_optional_using subject.params do requires :some_param end - expect(subject.route_setting(:declared_params)).to eq([:some_param]) + expect(declared_params).to eq([:some_param]) end it 'works when required field is present but nil' do @@ -120,6 +122,62 @@ def define_optional_using 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 = { @@ -139,7 +197,7 @@ def define_requires_all it 'adds entity documentation to declared params' do define_requires_all - expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field]) + expect(declared_params).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do @@ -174,7 +232,7 @@ def define_requires_none it 'adds entity documentation to declared params' do define_requires_none - expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field]) + expect(declared_params).to eq(%i[required_field optional_field]) end it 'errors when required_field is not present' do @@ -204,7 +262,7 @@ def define_requires_all it 'adds only the entity documentation to declared params, nothing more' do define_requires_all - expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field]) + expect(declared_params).to eq(%i[required_field optional_field]) end end @@ -270,7 +328,7 @@ def define_requires_none requires :key end end - expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) + expect(declared_params).to eq([items: [:key]]) end end @@ -342,7 +400,7 @@ def define_requires_none requires :key end end - expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) + expect(declared_params).to eq([items: [:key]]) end end @@ -405,7 +463,7 @@ def define_requires_none requires :key end end - expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) + expect(declared_params).to eq([items: [:key]]) end end @@ -436,7 +494,7 @@ module DateRangeValidations class DateRangeValidator < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name][:from] <= params[attr_name][:to] - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'" + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'") end end end @@ -520,7 +578,7 @@ def validate_param!(attr_name, params) # 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') + 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 @@ -540,7 +598,10 @@ def validate_param!(attr_name, params) ] expect(last_response.status).to eq(400) - expect(last_response.body).to eq('children[0][parents] is missing, children[1][parents] is missing') + 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 @@ -548,10 +609,17 @@ def validate_param!(attr_name, params) # 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 is missing') + 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') + 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 @@ -699,7 +767,7 @@ def validate_param!(attr_name, params) 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') + expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing') end end @@ -749,7 +817,7 @@ def validate_param!(attr_name, params) requires :key end end - expect(subject.route_setting(:declared_params)).to eq([items: [:key]]) + expect(declared_params).to eq([items: [:key]]) end end @@ -774,7 +842,7 @@ def validate_param!(attr_name, params) 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') + 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) @@ -794,7 +862,7 @@ def validate_param!(attr_name, params) 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') + 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) @@ -813,7 +881,275 @@ def validate_param!(attr_name, params) requires(:required_subitems, type: Array) { requires :value } end end - expect(subject.route_setting(:declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]]) + expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [: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 match_array([ + "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 @@ -841,7 +1177,7 @@ module CustomValidations class Customvalidator < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name] == 'im custom' - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'is not custom!' + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'is not custom!') end end end @@ -989,7 +1325,7 @@ module CustomValidations class CustomvalidatorWithOptions < Grape::Validations::Base def validate_param!(attr_name, params) return if params[attr_name] == @option[:text] - raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message + raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message) end end end @@ -1013,7 +1349,7 @@ def validate_param!(attr_name, params) expect(last_response.body).to eq('custom is not custom with options!') end end - end # end custom validation + end context 'named' do context 'can be defined' do @@ -1058,14 +1394,14 @@ def validate_param!(attr_name, params) subject.params do use :pagination end - expect(subject.route_setting(:declared_params)).to eq %i[page per_page] + expect(declared_params).to eq %i[page per_page] end it 'by #use with multiple params' do subject.params do use :pagination, :period end - expect(subject.route_setting(:declared_params)).to eq %i[page per_page start_date end_date] + expect(declared_params).to eq %i[page per_page start_date end_date] end end @@ -1223,7 +1559,9 @@ def validate_param!(attr_name, params) 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, scotch, aquavit are mutually exclusive pass only one, scotch2, aquavit2 are mutually exclusive pass only one' + 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 @@ -1249,7 +1587,7 @@ def validate_param!(attr_name, params) 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, scotch, aquavit are mutually exclusive, scotch2, aquavit2 are mutually exclusive' + 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 @@ -1318,7 +1656,7 @@ def validate_param!(attr_name, params) 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' } + exactly_one_of :beer, :wine, :juice, message: 'are missing, exactly one parameter is required' end get '/exactly_one_of' do 'exactly_one_of works!' @@ -1352,7 +1690,7 @@ def validate_param!(attr_name, params) 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 mutually exclusive, exactly one parameter is required' + expect(last_response.body).to eq 'beer, wine are missing, exactly one parameter is required' end end @@ -1399,7 +1737,7 @@ def validate_param!(attr_name, params) 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, beer_nested, wine_nested, juice_nested are missing, exactly one parameter must be provided' + 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 @@ -1411,7 +1749,7 @@ def validate_param!(attr_name, params) 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 'beer_nested2, wine_nested2 are mutually exclusive' + expect(last_response.body).to eq 'nested2[0][beer_nested2], nested2[0][wine_nested2] are mutually exclusive' end end end @@ -1485,16 +1823,16 @@ def validate_param!(attr_name, params) before :each do subject.params do requires :nested, type: Hash do - optional :beer_nested - optional :wine_nested - optional :juice_nested - at_least_one_of :beer_nested, :wine_nested, :juice_nested + optional :beer + optional :wine + optional :juice + at_least_one_of :beer, :wine, :juice end optional :nested2, type: Array do - optional :beer_nested2 - optional :wine_nested2 - optional :juice_nested2 - at_least_one_of :beer_nested2, :wine_nested2, :juice_nested2 + optional :beer + optional :wine + optional :juice + at_least_one_of :beer, :wine, :juice end end subject.get '/at_least_one_of_nested' do @@ -1505,17 +1843,17 @@ def validate_param!(attr_name, params) 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, beer_nested, wine_nested, juice_nested are missing, at least one parameter must be provided' + 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_nested: 'string' }, nested2: [{ beer_nested2: 'string' }] + 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_nested: 'string', wine_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'string' }] + 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 diff --git a/spec/integration/eager_load/eager_load_spec.rb b/spec/integration/eager_load/eager_load_spec.rb new file mode 100644 index 000000000..94b78e3e0 --- /dev/null +++ b/spec/integration/eager_load/eager_load_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) +require 'grape' + +describe Grape do + it 'eager_load!' do + require 'grape/eager_load' + expect { Grape.eager_load! }.to_not raise_error + end + + it 'compile!' do + expect { Class.new(Grape::API).compile! }.to_not raise_error + end +end diff --git a/spec/integration/multi_json/json_spec.rb b/spec/integration/multi_json/json_spec.rb index 11ae85132..fc06602fd 100644 --- a/spec/integration/multi_json/json_spec.rb +++ b/spec/integration/multi_json/json_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Json do diff --git a/spec/integration/multi_xml/xml_spec.rb b/spec/integration/multi_xml/xml_spec.rb index fce3c51ed..dde1fec5c 100644 --- a/spec/integration/multi_xml/xml_spec.rb +++ b/spec/integration/multi_xml/xml_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Grape::Xml do diff --git a/spec/shared/versioning_examples.rb b/spec/shared/versioning_examples.rb index 47113c5ec..ebc0742d4 100644 --- a/spec/shared/versioning_examples.rb +++ b/spec/shared/versioning_examples.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + shared_examples_for 'versioning' do it 'sets the API version' do subject.format :txt @@ -5,7 +7,7 @@ subject.get :hello do "Version: #{request.env['api.version']}" end - versioned_get '/hello', 'v1', macro_options + versioned_get '/hello', 'v1', **macro_options expect(last_response.body).to eql 'Version: v1' end @@ -16,7 +18,7 @@ subject.get :hello do "Version: #{request.env['api.version']}" end - versioned_get '/hello', 'v1', macro_options.merge(prefix: 'api') + versioned_get '/hello', 'v1', **macro_options.merge(prefix: 'api') expect(last_response.body).to eql 'Version: v1' end @@ -32,14 +34,14 @@ end end - versioned_get '/awesome', 'v1', macro_options + versioned_get '/awesome', 'v1', **macro_options expect(last_response.status).to eql 404 - versioned_get '/awesome', 'v2', macro_options + versioned_get '/awesome', 'v2', **macro_options expect(last_response.status).to eql 200 - versioned_get '/legacy', 'v1', macro_options + versioned_get '/legacy', 'v1', **macro_options expect(last_response.status).to eql 200 - versioned_get '/legacy', 'v2', macro_options + versioned_get '/legacy', 'v2', **macro_options expect(last_response.status).to eql 404 end @@ -49,11 +51,11 @@ 'I exist' end - versioned_get '/awesome', 'v1', macro_options + versioned_get '/awesome', 'v1', **macro_options expect(last_response.status).to eql 200 - versioned_get '/awesome', 'v2', macro_options + versioned_get '/awesome', 'v2', **macro_options expect(last_response.status).to eql 200 - versioned_get '/awesome', 'v3', macro_options + versioned_get '/awesome', 'v3', **macro_options expect(last_response.status).to eql 404 end @@ -72,10 +74,10 @@ end end - versioned_get '/version', 'v2', macro_options + 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 + versioned_get '/version', 'v1', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to eq('version v1') end @@ -96,11 +98,11 @@ end end - versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix) + 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) + 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 @@ -129,11 +131,11 @@ end end - versioned_get '/version', 'v1', macro_options.merge(prefix: subject.prefix) + 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) + 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 @@ -146,7 +148,7 @@ subject.get :api_version_with_version_param do params[:version] end - versioned_get '/api_version_with_version_param?version=1', 'v1', macro_options + versioned_get '/api_version_with_version_param?version=1', 'v1', **macro_options expect(last_response.body).to eql '1' end @@ -181,13 +183,13 @@ context 'v1' do it 'finds endpoint' do - versioned_get '/version', 'v1', macro_options + 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 + versioned_get '/whatever', 'v1', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end @@ -195,13 +197,13 @@ context 'v2' do it 'finds endpoint' do - versioned_get '/version', 'v2', macro_options + 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 + versioned_get '/whatever', 'v2', **macro_options expect(last_response.status).to eq(200) expect(last_response.body).to end_with 'whatever' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e00fba228..d0bb66554 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support')) @@ -12,14 +14,23 @@ require file end -I18n.enforce_available_locales = false +eager_load! + +# 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.warnings = true config.before(:each) { Grape::Util::InheritableSetting.reset_global! } + + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' end require 'coveralls' diff --git a/spec/support/basic_auth_encode_helpers.rb b/spec/support/basic_auth_encode_helpers.rb index 0a841aa4a..c58acada6 100644 --- a/spec/support/basic_auth_encode_helpers.rb +++ b/spec/support/basic_auth_encode_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spec module Support module Helpers diff --git a/spec/support/chunks.rb b/spec/support/chunks.rb new file mode 100644 index 000000000..0506cb7ce --- /dev/null +++ b/spec/support/chunks.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Chunks + def read_chunks(body) + buffer = [] + body.each { |chunk| buffer << chunk } + + buffer + end +end + +RSpec.configure do |config| + config.include Chunks +end diff --git a/spec/support/content_type_helpers.rb b/spec/support/content_type_helpers.rb index 5a23f0260..9721edacd 100644 --- a/spec/support/content_type_helpers.rb +++ b/spec/support/content_type_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spec module Support module Helpers diff --git a/spec/support/eager_load.rb b/spec/support/eager_load.rb new file mode 100644 index 000000000..c55e4f243 --- /dev/null +++ b/spec/support/eager_load.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Grape uses autoload https://api.rubyonrails.org/classes/ActiveSupport/Autoload.html. +# When a class/module get added to the list, ActiveSupport doesn't check whether it really exists. +# This method loads all classes/modules defined via autoload to be sure only existing +# classes/modules were listed. +def eager_load!(scope = Grape) + # get modules + scope.constants.each do |const_name| + const = scope.const_get(const_name) + + next unless const.respond_to?(:eager_load!) + + const.eager_load! + + # check its modules, they might need to be loaded as well. + eager_load!(const) + end +end diff --git a/spec/support/endpoint_faker.rb b/spec/support/endpoint_faker.rb index 2d9c82f8a..1cf02ef1d 100644 --- a/spec/support/endpoint_faker.rb +++ b/spec/support/endpoint_faker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spec module Support class EndpointFaker diff --git a/spec/support/file_streamer.rb b/spec/support/file_streamer.rb index 8a9f24d09..b640fba27 100644 --- a/spec/support/file_streamer.rb +++ b/spec/support/file_streamer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class FileStreamer def initialize(file_path) @file_path = file_path diff --git a/spec/support/integer_helpers.rb b/spec/support/integer_helpers.rb index 88fba900f..670cb52a6 100644 --- a/spec/support/integer_helpers.rb +++ b/spec/support/integer_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Spec module Support module Helpers diff --git a/spec/support/versioned_helpers.rb b/spec/support/versioned_helpers.rb index 7423bdf92..ea78013d4 100644 --- a/spec/support/versioned_helpers.rb +++ b/spec/support/versioned_helpers.rb @@ -1,10 +1,12 @@ +# 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 = {}) + def versioned_path(**options) case options[:using] when :path File.join('/', options[:prefix] || '', options[:version], options[:path]) @@ -19,7 +21,7 @@ def versioned_path(options = {}) end end - def versioned_headers(options) + def versioned_headers(**options) case options[:using] when :path {} # no-op @@ -41,13 +43,11 @@ def versioned_headers(options) 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)) + 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 = {} - if version_options[:using] == :param - params = { version_options[:parameter] => version_name } - end + params = { version_options[:parameter] => version_name } if version_options[:using] == :param get path, params, headers end end